@codyswann/lisa 4.26.5 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) 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 +9 -5
  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-analyze-claude-remote/SKILL.md +2 -2
  9. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +10 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  11. package/plugins/lisa/scripts/install-remote-agent-aws.mjs +52 -6
  12. package/plugins/lisa/scripts/remote-agent-aws-setup.sh +319 -15
  13. package/plugins/lisa/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  14. package/plugins/lisa/skills/lisa-implement/SKILL.md +10 -0
  15. package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/scripts/install-remote-agent-aws.mjs +52 -6
  18. package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +319 -15
  19. package/plugins/lisa-agy/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  20. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +10 -0
  21. package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  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/install-remote-agent-aws.mjs +52 -6
  29. package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +319 -15
  30. package/plugins/lisa-copilot/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  31. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +10 -0
  32. package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  33. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cursor/scripts/install-remote-agent-aws.mjs +52 -6
  35. package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +319 -15
  36. package/plugins/lisa-cursor/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  37. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +10 -0
  38. package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +33 -2
  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/install-remote-agent-aws.mjs +52 -6
  80. package/plugins/src/base/scripts/remote-agent-aws-setup.sh +319 -15
  81. package/plugins/src/base/skills/lisa-analyze-claude-remote/SKILL.md +2 -2
  82. package/plugins/src/base/skills/lisa-implement/SKILL.md +10 -0
  83. package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +33 -2
@@ -313,14 +313,32 @@ Generated for: \`${platform}\`.
313
313
 
314
314
  ## Implementation
315
315
 
316
- The bootstrap bundle defines the available profile names. \`dev\` is the
317
- default when present. Select another account explicitly, for example:
316
+ Profiles are named \`<project>-agent-<stage>\`, not bare \`dev\` or
317
+ \`production\`. The bundle supplies the stage; the project component comes from
318
+ \`LISA_AWS_PROFILE_NAMESPACE\`, then the bundle's \`namespace\`, then
319
+ \`<owner>-<repository>\` from this checkout's git origin. Select an account
320
+ explicitly, for example:
318
321
 
319
322
  \`\`\`bash
320
- aws --profile staging sts get-caller-identity
321
- aws --profile production cloudformation describe-stacks
323
+ aws --profile <project>-agent-staging sts get-caller-identity
324
+ aws --profile <project>-agent-production cloudformation describe-stacks
322
325
  \`\`\`
323
326
 
327
+ The setup line prints the exact names it wrote; use those.
328
+
329
+ A bare stage name states a stage but not an owner. Two organisations' bundles
330
+ declaring the same stages write the same names into the same shared
331
+ \`~/.aws/config\`, the last bootstrap silently wins, and every later command
332
+ runs in the other tenant's account while reporting success. Nothing about the
333
+ resulting profile is malformed, so no check on the profile can catch it — only
334
+ a name carrying the owner prevents it.
335
+
336
+ \`default\` is the one name that cannot be namespaced. The setup script writes
337
+ it for this project only when nothing else already owns it; if another Lisa
338
+ project or the operator's own configuration holds it, the script stops and says
339
+ so rather than repointing every unqualified \`aws\` command. Pass
340
+ \`LISA_AWS_CLAIM_DEFAULT_PROFILE=1\` to take it over deliberately.
341
+
324
342
  Production and shared profiles are observer-only. Production repair remains a
325
343
  human-driven local-workstation operation.
326
344
 
@@ -340,8 +358,36 @@ this project requires a named source profile to read the emission; \`shared\` is
340
358
  not an implicit requirement.
341
359
 
342
360
  Store that exact output as the masked secret \`LISA_AWS_BOOTSTRAP_JSON\` on each
343
- remote-agent platform. Run the setup command and require its live
344
- \`sts:GetCallerIdentity\` check to pass before considering the platform ready.
361
+ remote-agent platform, then run the setup command.
362
+
363
+ ## Readiness
364
+
365
+ \`sts:GetCallerIdentity\` succeeding proves only that the credentials work. It
366
+ does not prove they reached the intended account, and a check that never looks
367
+ at what the call returned passes on any credential that authenticates —
368
+ including one that authenticates into somebody else's account.
369
+
370
+ The setup script therefore compares the returned account id against the account
371
+ named in the role ARN it just configured, and refuses to report ready on a
372
+ mismatch, quoting both ids. A bundle may also declare \`expectedAccountId\` per
373
+ stage; a declared id that disagrees with its own role ARN fails before anything
374
+ is written. \`LISA_AWS_EXPECTED_ACCOUNT_ID\` states the same expectation from
375
+ outside the bundle, and \`LISA_AWS_VERIFY_ALL_PROFILES=1\` proves every stage
376
+ rather than only the default.
377
+
378
+ Treat the setup command's own exit status as the readiness gate. Do not
379
+ substitute a hand-run \`aws sts get-caller-identity\`: it answers "do these
380
+ credentials work", which is not the question.
381
+
382
+ ## Migration
383
+
384
+ Workstations bootstrapped before profiles were namespaced still carry the old
385
+ bare names. The setup script detects them — by the retired
386
+ \`lisa-remote-agent-bootstrap\` source profile, so an operator's own \`dev\` is
387
+ never mistaken for one of ours — and names them on stderr on every run. They are
388
+ left in place, because deleting a profile somebody may still be using is its own
389
+ silent surprise. Re-run with \`LISA_AWS_PRUNE_LEGACY_PROFILES=1\` to delete the
390
+ ones Lisa wrote; it reports exactly what it removed.
345
391
 
346
392
  ## Notes
347
393
 
@@ -13,13 +13,40 @@
13
13
  # contains.
14
14
  # Optional plain variables:
15
15
  # LISA_REMOTE_AGENT claude | codex | cursor | copilot | agy | opencode
16
- # LISA_AWS_DEFAULT_PROFILE Defaults to dev, then the first available profile.
16
+ # LISA_AWS_PROFILE_NAMESPACE
17
+ # The project component every written profile is
18
+ # scoped by. Falls back to the bundle's `namespace`
19
+ # (or `project`), then to `<owner>-<repo>` from the
20
+ # git origin remote. Required when none resolves.
21
+ # LISA_AWS_DEFAULT_PROFILE Stage selected as the default. Defaults to dev,
22
+ # then the first stage in the bundle.
23
+ # LISA_AWS_EXPECTED_ACCOUNT_ID
24
+ # Operator-declared account for the default stage.
25
+ # Checked against the bundle before anything is
26
+ # written, and against the live identity after.
27
+ # LISA_AWS_VERIFY_ALL_PROFILES
28
+ # 1 to prove every stage's account, not just the
29
+ # default's.
30
+ # LISA_AWS_CLAIM_DEFAULT_PROFILE
31
+ # 1 to take over a `[default]` this project does not
32
+ # already own. Off by default: silently repointing
33
+ # `default` is how one tenant's commands end up in
34
+ # another tenant's account.
35
+ # LISA_AWS_PRUNE_LEGACY_PROFILES
36
+ # 1 to delete the unnamespaced profiles an earlier
37
+ # Lisa bootstrap wrote. Off by default; they are
38
+ # reported either way, never silently orphaned.
17
39
  # LISA_AWS_SKIP_VERIFY Set to 1 only for an offline image build.
18
40
 
19
41
  set -euo pipefail
20
42
  umask 077
21
43
 
22
- BOOTSTRAP_PROFILE="lisa-remote-agent-bootstrap"
44
+ # Profiles are scoped `<namespace>-agent-<stage>`, matching the convention every
45
+ # other profile on a multi-organisation workstation already follows. The old
46
+ # name is retained only so an earlier bootstrap's output can be recognised and
47
+ # migrated rather than left behind unexplained.
48
+ BOOTSTRAP_PROFILE_SUFFIX="-agent-bootstrap"
49
+ LEGACY_BOOTSTRAP_PROFILE="lisa-remote-agent-bootstrap"
23
50
  AWS_CLI_VERSION="2.36.2"
24
51
 
25
52
  fail() {
@@ -141,6 +168,84 @@ remove_profile_setting() {
141
168
  mv "$temporary_file" "$credentials_file"
142
169
  }
143
170
 
171
+ # Delete one whole ini section, header and body, from an AWS shared-config file.
172
+ #
173
+ # Used only for the migration path: profiles a PREVIOUS Lisa bootstrap wrote
174
+ # under the old unnamespaced convention. Sections this script never wrote are
175
+ # never candidates — see legacy_profile_sections, which identifies ours by the
176
+ # retired source_profile name rather than by guessing from the profile name.
177
+ remove_profile_section() {
178
+ local file section temporary_file
179
+ file="$1"
180
+ section="$2"
181
+ [ -f "$file" ] || return 0
182
+
183
+ temporary_file="$(mktemp "${file}.XXXXXX")"
184
+ awk -v section="$section" '
185
+ /^\[[^]]*\][[:space:]]*$/ {
186
+ current = $0
187
+ sub(/^\[[[:space:]]*/, "", current)
188
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
189
+ in_section = current == section
190
+ if (in_section) next
191
+ }
192
+ !in_section { print }
193
+ ' "$file" >"$temporary_file"
194
+ chmod 600 "$temporary_file"
195
+ mv "$temporary_file" "$file"
196
+ }
197
+
198
+ # Read one setting out of one ini section, or print nothing.
199
+ profile_setting_value() {
200
+ local file section setting
201
+ file="$1"
202
+ section="$2"
203
+ setting="$3"
204
+ [ -f "$file" ] || return 0
205
+
206
+ awk -v section="$section" -v setting="$setting" '
207
+ /^\[[^]]*\][[:space:]]*$/ {
208
+ current = $0
209
+ sub(/^\[[[:space:]]*/, "", current)
210
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
211
+ in_section = current == section
212
+ next
213
+ }
214
+ in_section && $0 ~ "^[[:space:]]*" setting "[[:space:]]*=" {
215
+ sub(/^[^=]*=[[:space:]]*/, "")
216
+ sub(/[[:space:]]*$/, "")
217
+ print
218
+ exit
219
+ }
220
+ ' "$file"
221
+ }
222
+
223
+ # Every section written by the OLD, unnamespaced convention.
224
+ #
225
+ # Identified by the retired bootstrap profile name, which only Lisa ever wrote,
226
+ # so an operator's own `[profile dev]` can never be mistaken for ours.
227
+ legacy_profile_sections() {
228
+ local file
229
+ file="$1"
230
+ [ -f "$file" ] || return 0
231
+
232
+ awk -v bootstrap="$LEGACY_BOOTSTRAP_PROFILE" '
233
+ /^\[[^]]*\][[:space:]]*$/ {
234
+ current = $0
235
+ sub(/^\[[[:space:]]*/, "", current)
236
+ sub(/[[:space:]]*\][[:space:]]*$/, "", current)
237
+ section = current
238
+ next
239
+ }
240
+ $0 ~ "^[[:space:]]*source_profile[[:space:]]*=" {
241
+ value = $0
242
+ sub(/^[^=]*=[[:space:]]*/, "", value)
243
+ sub(/[[:space:]]*$/, "", value)
244
+ if (value == bootstrap && section != "") print section
245
+ }
246
+ ' "$file"
247
+ }
248
+
144
249
  sanitize_session_name() {
145
250
  local candidate
146
251
  candidate="$(printf '%s' "${LISA_REMOTE_AGENT:-remote-agent}" \
@@ -150,6 +255,125 @@ sanitize_session_name() {
150
255
  printf '%s\n' "$candidate"
151
256
  }
152
257
 
258
+ # Reduce any project identifier to the safe, stable component of a profile name.
259
+ sanitize_namespace() {
260
+ printf '%s' "$1" \
261
+ | tr '[:upper:]' '[:lower:]' \
262
+ | sed -e 's/[^a-z0-9]\{1,\}/-/g' -e 's/^-*//' -e 's/-*$//' \
263
+ | cut -c1-48
264
+ }
265
+
266
+ # `<owner>-<repository>` for the checkout this script was run from.
267
+ #
268
+ # The forge owner is what makes the result distinguishing: two organisations
269
+ # routinely have a repository of the same NAME, which is the collision this
270
+ # whole change exists to remove, and they cannot share an owner.
271
+ namespace_from_git() {
272
+ need git || return 0
273
+
274
+ local url slug
275
+ url="$(git config --get remote.origin.url 2>/dev/null || true)"
276
+ [ -n "$url" ] || return 0
277
+ url="${url%.git}"
278
+ url="${url%/}"
279
+ slug="$(printf '%s' "$url" \
280
+ | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#^[^@/]*@##' -e 's#^[^/:]*[:/]##')"
281
+ printf '%s' "$slug" | tr '/' '-'
282
+ }
283
+
284
+ # The project component every profile this script writes is scoped by.
285
+ #
286
+ # Bare stage names (`dev`, `production`) name a stage but not an OWNER. On a
287
+ # workstation carrying more than one organisation two bundles then write the
288
+ # same names into the same shared `~/.aws/config`, the last bootstrap wins, and
289
+ # every later command resolves another tenant's account while reporting success.
290
+ # Nothing about the resulting profile is malformed, so no check on the profile
291
+ # can catch it; only a name that carries the owner can prevent it.
292
+ # Prints `<namespace>\t<where it came from>`. Both halves are returned together
293
+ # because this runs in a command substitution: a variable assigned inside a
294
+ # subshell never reaches the caller, and reporting the wrong provenance is how
295
+ # an operator ends up unable to explain why their profiles are named what they
296
+ # are named.
297
+ resolve_namespace() {
298
+ local candidate sanitized namespace_source
299
+ namespace_source="LISA_AWS_PROFILE_NAMESPACE"
300
+ candidate="${LISA_AWS_PROFILE_NAMESPACE:-}"
301
+ if [ -z "$candidate" ]; then
302
+ namespace_source="the bootstrap bundle"
303
+ candidate="$(printf '%s' "$bootstrap_json" | jq -r '.namespace // .project // empty')"
304
+ fi
305
+ if [ -z "$candidate" ]; then
306
+ namespace_source="git remote origin"
307
+ candidate="$(namespace_from_git)"
308
+ fi
309
+ [ -n "$candidate" ] || fail \
310
+ "cannot determine which project these AWS profiles belong to. Set LISA_AWS_PROFILE_NAMESPACE (for example the repository slug), add a \"namespace\" to the bootstrap bundle, or run this from a checkout with an origin remote. Unnamespaced profiles collide across organisations on a shared workstation."
311
+
312
+ sanitized="$(sanitize_namespace "$candidate")"
313
+ printf '%s' "$sanitized" | grep -qE '^[a-z0-9][a-z0-9-]{1,47}$' || fail \
314
+ "profile namespace from ${namespace_source} (\"${candidate}\") does not reduce to a usable name; set LISA_AWS_PROFILE_NAMESPACE to two or more alphanumeric characters"
315
+ printf '%s\t%s\n' "$sanitized" "$namespace_source"
316
+ }
317
+
318
+ # Prove which ACCOUNT a profile reaches, not merely that it authenticates.
319
+ #
320
+ # `sts:GetCallerIdentity` succeeding proves the credentials work. It says
321
+ # nothing about whose account they work in, and the old code never looked at
322
+ # what it returned — a lookup whose success was read as the answer. Comparing
323
+ # the returned account against the one named in the role ARN just configured is
324
+ # what turns a liveness probe into an identity check.
325
+ assert_profile_account() {
326
+ local profile expected stage identity actual
327
+ profile="$1"
328
+ expected="$2"
329
+ stage="$3"
330
+
331
+ identity="$(AWS_PAGER="" aws sts get-caller-identity --profile "$profile" --output json)" || fail \
332
+ "sts:GetCallerIdentity failed for profile ${profile} (stage ${stage})"
333
+ actual="$(printf '%s' "$identity" | jq -r '.Account // empty')"
334
+ [ -n "$actual" ] || fail \
335
+ "sts:GetCallerIdentity returned no account id for profile ${profile} (stage ${stage}); cannot prove which account it reached"
336
+ [ "$actual" = "$expected" ] || fail \
337
+ "profile ${profile} (stage ${stage}) resolved to AWS account ${actual}, but this project expects ${expected}. The credentials authenticate; they are not this project's account. Refusing to report ready."
338
+ }
339
+
340
+ # Who currently owns `[default]`.
341
+ #
342
+ # `default` is the one name that cannot be namespaced, so it is the one place
343
+ # two projects still contend. Taking it over silently is the whole defect in
344
+ # miniature: every unqualified `aws` command would change accounts with nothing
345
+ # said. An inert `[default]` holding only region/output is not a claim on it,
346
+ # and a container that ships one must still be able to bootstrap.
347
+ default_profile_owner() {
348
+ local source_value setting
349
+ source_value="$(profile_setting_value "$config_file" "default" "source_profile")"
350
+ if [ "$source_value" = "$BOOTSTRAP_PROFILE" ]; then
351
+ printf 'ours\n'
352
+ return 0
353
+ fi
354
+ if [ "$source_value" = "$LEGACY_BOOTSTRAP_PROFILE" ]; then
355
+ printf 'legacy\n'
356
+ return 0
357
+ fi
358
+ case "$source_value" in
359
+ *"$BOOTSTRAP_PROFILE_SUFFIX")
360
+ printf 'another Lisa project\n'
361
+ return 0
362
+ ;;
363
+ esac
364
+ for setting in role_arn credential_process sso_start_url sso_session aws_access_key_id; do
365
+ if [ -n "$(profile_setting_value "$config_file" "default" "$setting")" ]; then
366
+ printf 'this workstation operator\n'
367
+ return 0
368
+ fi
369
+ done
370
+ if [ -n "$(profile_setting_value "$credentials_file" "default" "aws_access_key_id")" ]; then
371
+ printf 'this workstation operator\n'
372
+ return 0
373
+ fi
374
+ printf 'unclaimed\n'
375
+ }
376
+
153
377
  install_base_tools
154
378
  install_aws_cli
155
379
 
@@ -181,21 +405,89 @@ profiles_json="$(printf '%s' "$bootstrap_json" | jq -c '
181
405
  end
182
406
  ')"
183
407
 
408
+ namespace_resolution="$(resolve_namespace)"
409
+ PROFILE_NAMESPACE="${namespace_resolution%%$'\t'*}"
410
+ namespace_source="${namespace_resolution#*$'\t'}"
411
+ BOOTSTRAP_PROFILE="${PROFILE_NAMESPACE}${BOOTSTRAP_PROFILE_SUFFIX}"
412
+
413
+ # The role ARN carries the account the role lives in, so every bundle already
414
+ # declares an expected account per stage whether or not it says so explicitly.
415
+ # Requiring the full ARN shape is what makes that field readable at all.
184
416
  printf '%s' "$profiles_json" | jq -e --arg bootstrap_profile "$BOOTSTRAP_PROFILE" '
185
417
  type == "object" and length > 0 and
186
418
  all(to_entries[];
187
419
  (.key != $bootstrap_profile) and
420
+ (.key != "bootstrap") and
188
421
  (.key | test("^[A-Za-z0-9_-]+$")) and
189
- (.value.roleArn | type == "string" and startswith("arn:aws:iam::")) and
422
+ (.value.roleArn | type == "string" and test("^arn:aws:iam::[0-9]{12}:role/.+")) and
190
423
  (.value.region | type == "string" and length > 0)
191
424
  )
192
- ' >/dev/null || fail "bootstrap profiles must map safe names to roleArn and region"
425
+ ' >/dev/null || fail "bootstrap profiles must map safe names (not \"bootstrap\") to a full arn:aws:iam::<account>:role/<name> roleArn and a region"
426
+
427
+ # An explicitly declared account that disagrees with the role ARN is a bundle
428
+ # that cannot be believed. Fail here, before anything is written, naming both.
429
+ while IFS="$(printf '\t')" read -r stage declared_account arn_account; do
430
+ [ "$declared_account" = "$arn_account" ] || fail \
431
+ "bundle stage ${stage} declares expectedAccountId ${declared_account} but its roleArn names account ${arn_account}"
432
+ done < <(printf '%s' "$profiles_json" | jq -r '
433
+ to_entries[]
434
+ | select(.value.expectedAccountId != null)
435
+ | [.key, (.value.expectedAccountId | tostring), (.value.roleArn | split(":")[4])]
436
+ | @tsv
437
+ ')
438
+
439
+ default_stage="${LISA_AWS_DEFAULT_PROFILE:-dev}"
440
+ if ! printf '%s' "$profiles_json" | jq -e --arg stage "$default_stage" 'has($stage)' >/dev/null; then
441
+ default_stage="$(printf '%s' "$profiles_json" | jq -r 'keys[0]')"
442
+ fi
443
+
444
+ default_role_arn="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '.[$stage].roleArn')"
445
+ default_region="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '.[$stage].region')"
446
+ default_expected_account="$(printf '%s' "$profiles_json" | jq -er --arg stage "$default_stage" '
447
+ (.[$stage].expectedAccountId // (.[$stage].roleArn | split(":")[4])) | tostring
448
+ ')"
449
+
450
+ if [ -n "${LISA_AWS_EXPECTED_ACCOUNT_ID:-}" ] \
451
+ && [ "$LISA_AWS_EXPECTED_ACCOUNT_ID" != "$default_expected_account" ]; then
452
+ fail "LISA_AWS_EXPECTED_ACCOUNT_ID is ${LISA_AWS_EXPECTED_ACCOUNT_ID} but the bundle's ${default_stage} role is in account ${default_expected_account}"
453
+ fi
193
454
 
194
455
  credentials_file="${AWS_SHARED_CREDENTIALS_FILE:-$HOME/.aws/credentials}"
195
456
  config_file="${AWS_CONFIG_FILE:-$HOME/.aws/config}"
196
457
  mkdir -p "$HOME/.aws" "$(dirname "$credentials_file")" "$(dirname "$config_file")"
197
458
  chmod 700 "$HOME/.aws"
198
459
 
460
+ profile_names="$(printf '%s' "$profiles_json" \
461
+ | jq -r --arg namespace "$PROFILE_NAMESPACE" 'keys | map($namespace + "-agent-" + .) | join(", ")')"
462
+
463
+ # Migration: the previous convention's profiles are still on this workstation.
464
+ #
465
+ # They are reported rather than removed, because deleting a profile an operator
466
+ # may still be using is its own silent surprise. Pruning is opt-in and says
467
+ # exactly what it removed.
468
+ legacy_sections="$(legacy_profile_sections "$config_file")"
469
+ legacy_names=""
470
+ if [ -n "$legacy_sections" ]; then
471
+ legacy_names="$(printf '%s\n' "$legacy_sections" | sed 's/^profile //' | paste -sd ',' - | sed 's/,/, /g')"
472
+ if [ "${LISA_AWS_PRUNE_LEGACY_PROFILES:-0}" = "1" ]; then
473
+ printf '%s\n' "$legacy_sections" | while IFS= read -r section; do
474
+ [ -n "$section" ] || continue
475
+ remove_profile_section "$config_file" "$section"
476
+ done
477
+ remove_profile_section "$config_file" "profile ${LEGACY_BOOTSTRAP_PROFILE}"
478
+ remove_profile_section "$credentials_file" "$LEGACY_BOOTSTRAP_PROFILE"
479
+ fi
480
+ fi
481
+
482
+ default_owner="$(default_profile_owner)"
483
+ case "$default_owner" in
484
+ ours | legacy | unclaimed) ;;
485
+ *)
486
+ [ "${LISA_AWS_CLAIM_DEFAULT_PROFILE:-0}" = "1" ] || fail \
487
+ "refusing to overwrite the [default] AWS profile in ${config_file}: it belongs to ${default_owner}, not to ${PROFILE_NAMESPACE}. Overwriting it would silently repoint every unqualified aws command at this project's accounts. Use the named profiles instead (${profile_names}), or set LISA_AWS_CLAIM_DEFAULT_PROFILE=1 to take it over deliberately."
488
+ ;;
489
+ esac
490
+
199
491
  aws configure set aws_access_key_id "$access_key_id" --profile "$BOOTSTRAP_PROFILE"
200
492
  aws configure set aws_secret_access_key "$secret_access_key" --profile "$BOOTSTRAP_PROFILE"
201
493
  if [ -n "$session_token" ]; then
@@ -208,7 +500,8 @@ else
208
500
  fi
209
501
 
210
502
  session_name="$(sanitize_session_name)"
211
- while IFS=$'\t' read -r profile_name role_arn region; do
503
+ while IFS="$(printf '\t')" read -r stage role_arn region; do
504
+ profile_name="${PROFILE_NAMESPACE}-agent-${stage}"
212
505
  aws configure set role_arn "$role_arn" --profile "$profile_name"
213
506
  aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile "$profile_name"
214
507
  aws configure set external_id "$external_id" --profile "$profile_name"
@@ -216,13 +509,7 @@ while IFS=$'\t' read -r profile_name role_arn region; do
216
509
  aws configure set region "$region" --profile "$profile_name"
217
510
  done < <(printf '%s' "$profiles_json" | jq -r 'to_entries[] | [.key, .value.roleArn, .value.region] | @tsv')
218
511
 
219
- default_profile="${LISA_AWS_DEFAULT_PROFILE:-dev}"
220
- if ! printf '%s' "$profiles_json" | jq -e --arg profile "$default_profile" 'has($profile)' >/dev/null; then
221
- default_profile="$(printf '%s' "$profiles_json" | jq -r 'keys[0]')"
222
- fi
223
-
224
- default_role_arn="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].roleArn')"
225
- default_region="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].region')"
512
+ default_profile="${PROFILE_NAMESPACE}-agent-${default_stage}"
226
513
  aws configure set role_arn "$default_role_arn" --profile default
227
514
  aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile default
228
515
  aws configure set external_id "$external_id" --profile default
@@ -232,8 +519,25 @@ aws configure set region "$default_region" --profile default
232
519
  chmod 600 "$credentials_file" "$config_file"
233
520
 
234
521
  if [ "${LISA_AWS_SKIP_VERIFY:-0}" != "1" ]; then
235
- AWS_PAGER="" aws sts get-caller-identity --profile "$default_profile" >/dev/null
522
+ if [ "${LISA_AWS_VERIFY_ALL_PROFILES:-0}" = "1" ]; then
523
+ while IFS="$(printf '\t')" read -r stage expected_account; do
524
+ assert_profile_account "${PROFILE_NAMESPACE}-agent-${stage}" "$expected_account" "$stage"
525
+ done < <(printf '%s' "$profiles_json" | jq -r '
526
+ to_entries[]
527
+ | [.key, ((.value.expectedAccountId // (.value.roleArn | split(":")[4])) | tostring)]
528
+ | @tsv
529
+ ')
530
+ else
531
+ assert_profile_account "$default_profile" "$default_expected_account" "$default_stage"
532
+ fi
533
+ fi
534
+
535
+ if [ -n "$legacy_names" ]; then
536
+ if [ "${LISA_AWS_PRUNE_LEGACY_PROFILES:-0}" = "1" ]; then
537
+ echo "remote-agent-aws-setup: removed unnamespaced profiles left by an earlier bootstrap: ${legacy_names}" >&2
538
+ else
539
+ echo "remote-agent-aws-setup: ${config_file} still contains unnamespaced profiles written by an earlier Lisa bootstrap: ${legacy_names}. They name a stage but no owner, so on a workstation carrying more than one organisation they can resolve to another tenant's account. Review them, or re-run with LISA_AWS_PRUNE_LEGACY_PROFILES=1 to delete the ones Lisa wrote." >&2
540
+ fi
236
541
  fi
237
542
 
238
- profile_names="$(printf '%s' "$profiles_json" | jq -r 'keys | join(", ")')"
239
- echo "remote-agent-aws-setup: ready (${session_name}; default=${default_profile}; profiles=${profile_names})"
543
+ echo "remote-agent-aws-setup: ready (${session_name}; namespace=${PROFILE_NAMESPACE} via ${namespace_source}; default=${default_profile} in account ${default_expected_account}; profiles=${profile_names})"
@@ -369,9 +369,9 @@ so the generator can render acquisition comments into its template:
369
369
  ],
370
370
  "awsProfiles": [
371
371
  {
372
- "name": "dev",
372
+ "name": "<project>-agent-dev",
373
373
  "roleArn": "arn:aws:iam::<account-id>:role/<role-name>",
374
- "credentialSource": "lisa-remote-agent-bootstrap",
374
+ "credentialSource": "<project>-agent-bootstrap",
375
375
  "externalId": "<project-external-id>",
376
376
  "region": "us-east-1"
377
377
  }
@@ -346,6 +346,16 @@ Before shutting down the team, execute the Verify flow:
346
346
  4. Record Implement usage on the originating work artifact via `lisa-usage-accounting` so the work item (or other implementation-owned artifact) gains a direct `lisa-implement` usage entry in the canonical `## Lisa Usage` section. If the parent / child graph is already known, prefer `record_and_rollup` so ancestor totals refresh in the same write; otherwise still write the direct entry, and if runtime usage is unavailable, use `source: unavailable` with nullable token/cost fields instead of skipping the row.
347
347
  5. Commit ALL outstanding changes in logical batches on the branch (minus sensitive data/information) — not just changes made by the agent team. This includes pre-existing uncommitted changes that were on the branch before the plan started. Do NOT filter commits to only "task-related" files. If it shows up in git status, it gets committed (unless it contains secrets).
348
348
  6. Push the changes - if any pre-push hook blocks you, create a task for the agent team to fix the error/problem whether it was pre-existing or not
349
+ 6a. **Review the branch locally BEFORE opening the pull request, every time.** Invoke `lisa-review-local` over the branch diff, triage what it finds under the `convergent-review` rule (same contract as any other reviewer — fix what is valid, refute what is not, on the record), and commit the fixes. Only then proceed to step 7.
350
+
351
+ **Unconditional, not a fallback.** Do not gate this on whether a third-party reviewer is available, rate limited, or skipped. Measured on this repository, 47 of the last 60 merged pull requests carried a review context reporting `SUCCESS` while having read nothing — 29 rate limited, 16 skipped because the vendor does not auto-review public repositories, one skipped on size. A rule that fires only when the vendor is absent has to detect four distinct vacuity strings correctly, forever, and still leaves the vendor being *present but wrong* unguarded. Reviewing always needs no detection at all and makes the third-party review **additive**: its findings still arrive at step 8 and are handled there exactly as today.
352
+
353
+ **Before the pull request, not after it.** Auto-merge is armed at submit time, so the pull request can merge the moment CI goes green — a review that starts after step 7 is racing a latch that is already set. Running here has nothing to race.
354
+
355
+ **A local review is a weaker claim than an independent one, and the report must say so.** The agents reviewing the branch are the same team that wrote it. Record the outcome as *self-reviewed* — never as *reviewed* — and never describe it as satisfying the third-party review check, which is ruleset-required and can only be posted by that vendor's app. Collapsing the two would be the same vacuous-success failure this step exists to remove, one level up.
356
+
357
+ **If no subagent delegation is available in this runtime**, say so explicitly in the completion summary and proceed — but record that no local review was possible. "Could not review" must never be reported as "reviewed and found nothing"; an unavailable check is not a passing one.
358
+
349
359
  7. Open a pull request with auto-merge on via `lisa-git-submit-pr`, targeting the **base branch resolved from the ticket's environment** (`target_branch=<base>`, per the branch step above), and including the mandatory work-item ref so the PR can be linked natively to the source issue.
350
360
  7a. Confirm two-way linkage before treating PR submission as complete: the PR body/title/branch must reference the work item, and the work item must have either a verified native PR link or the single managed `[lisa-pr-link]` comment. Establish that comment with `node scripts/lisa-work-item.mjs backlink --ref <work_item_ref> --pr-url <url>` rather than posting it by hand — it is idempotent, and it is the same file that enforces the check, so producer and consumer cannot drift.
351
361
  8. PR Watch Loop: Drive the PR to merge via the `drive-pr-to-merge` skill — the single source of truth for clearing every blocker (auto-merge with direct-merge fallback, `BEHIND` re-sync, conflict resolution, failing-check fixes, human + bot review-comment handling with thread resolution, stale `CHANGES_REQUESTED` dismissal, and post-merge ancestry verification). `git-submit-pr` already invokes it; if you reach this step with a PR already open, invoke `drive-pr-to-merge` directly with the PR number. For a large review backlog you may fan the code-fix work out to the agent team, but `drive-pr-to-merge` owns the loop and the terminal conditions — do not re-implement them.
@@ -30,14 +30,45 @@ 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 bundle in repository files. Tell
33
+ 5. Confirm the operator knows which project component the profiles will carry.
34
+ The script resolves it from `LISA_AWS_PROFILE_NAMESPACE`, then the bundle's
35
+ `namespace`, then `<owner>-<repository>` from the git origin remote, and
36
+ refuses to write anything when none resolves. Set the variable explicitly
37
+ whenever the setup hook runs outside a checkout.
38
+ 6. Never write or request the actual bootstrap bundle in repository files. Tell
34
39
  the operator where to obtain it and where to put it — see **Where the bundle
35
40
  lives** below. Pass `--secret-name` when the source names it something other
36
41
  than `remote-agent-credentials`, so the generated runbook contains the exact
37
42
  retrieval command.
38
- 6. Report the external step that remains: configure that one secret in the
43
+ 7. Report the external step that remains: configure that one secret in the
39
44
  remote environment and launch a smoke session.
40
45
 
46
+ ## Profile naming and readiness
47
+
48
+ Every profile the script writes is scoped `<project>-agent-<stage>`, and the
49
+ private source profile is `<project>-agent-bootstrap`. Bare stage names state a
50
+ stage but not an owner, so on a workstation carrying more than one organisation
51
+ two bundles declaring the same stages overwrite each other in the shared
52
+ `~/.aws/config` — silently, because nothing about the surviving profile is
53
+ malformed. The name is the only place the owner can be recorded.
54
+
55
+ `default` cannot be namespaced. The script claims it only when nothing else
56
+ owns it, and otherwise stops and names the current owner;
57
+ `LISA_AWS_CLAIM_DEFAULT_PROFILE=1` takes it over deliberately.
58
+
59
+ Readiness is an account check, not a liveness check. `sts:GetCallerIdentity`
60
+ succeeding proves the credentials work, not that they reached the intended
61
+ account — a check that ignores the returned value passes on any credential that
62
+ authenticates anywhere. The script compares the returned account id against the
63
+ account named in the role ARN it configured and refuses to report ready on a
64
+ mismatch, quoting both. Do not substitute a hand-run `get-caller-identity` for
65
+ the script's own exit status.
66
+
67
+ Workstations bootstrapped under the previous unnamespaced convention keep their
68
+ old profiles. They are reported by name on every run and removed only when
69
+ `LISA_AWS_PRUNE_LEGACY_PROFILES=1` is passed, so the migration is explicit in
70
+ both directions.
71
+
41
72
  ## Where the bundle lives
42
73
 
43
74
  `LISA_AWS_BOOTSTRAP_JSON` is **just another secret**, and it belongs wherever
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "4.26.5",
3
+ "version": "4.27.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"