@codyswann/lisa 2.225.5 → 2.226.1

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 (90) hide show
  1. package/README.md +7 -0
  2. package/all/create-only/scripts/remote-agent-aws-setup.sh +11 -0
  3. package/package.json +1 -1
  4. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  5. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  7. package/plugins/lisa/.codex-plugin/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/agents/openai.yaml +4 -0
  10. package/plugins/lisa/commands/setup-remote-aws.md +6 -0
  11. package/plugins/lisa/scripts/install-remote-agent-aws.mjs +347 -0
  12. package/plugins/lisa/scripts/remote-agent-aws-setup.sh +204 -0
  13. package/plugins/lisa/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  14. package/plugins/lisa/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  15. package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  16. package/plugins/lisa/skills/lisa-setup-remote-aws/agents/openai.yaml +4 -0
  17. package/plugins/lisa-agy/commands/lisa/setup-remote-aws.md +6 -0
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-agy/scripts/install-remote-agent-aws.mjs +347 -0
  20. package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +204 -0
  21. package/plugins/lisa-agy/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  22. package/plugins/lisa-agy/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  23. package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  24. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/commands/lisa/setup-remote-aws.md +6 -0
  31. package/plugins/lisa-copilot/scripts/install-remote-agent-aws.mjs +347 -0
  32. package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +204 -0
  33. package/plugins/lisa-copilot/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  34. package/plugins/lisa-copilot/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  35. package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/commands/lisa/setup-remote-aws.md +6 -0
  38. package/plugins/lisa-cursor/scripts/install-remote-agent-aws.mjs +347 -0
  39. package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +204 -0
  40. package/plugins/lisa-cursor/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  41. package/plugins/lisa-cursor/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  42. package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  43. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-agy/plugin.json +1 -1
  46. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  56. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  61. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  66. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-agy/plugin.json +1 -1
  71. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  76. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  81. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/src/base/commands/setup-remote-aws.md +6 -0
  84. package/plugins/src/base/scripts/install-remote-agent-aws.mjs +347 -0
  85. package/plugins/src/base/scripts/remote-agent-aws-setup.sh +204 -0
  86. package/plugins/src/base/skills/lisa-analyze-claude-remote/SKILL.md +21 -18
  87. package/plugins/src/base/skills/lisa-generate-claude-remote-build-script/SKILL.md +15 -27
  88. package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +53 -0
  89. package/scripts/claude-remote-setup.sh +9 -2
  90. package/scripts/remote-agent-aws-setup.sh +17 -0
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install Lisa's vendor-neutral remote AWS bootstrap into a host project.
4
+ *
5
+ * This installer writes only non-secret repository artifacts. The bootstrap
6
+ * SecretString is configured directly in each remote-agent platform.
7
+ * @module install-remote-agent-aws
8
+ */
9
+ import {
10
+ chmodSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const SCRIPT_NAME = "remote-agent-aws-setup.sh";
20
+ const INSTALL_COMMAND = `bash scripts/${SCRIPT_NAME}`;
21
+ const VALID_PLATFORMS = new Set([
22
+ "all",
23
+ "claude",
24
+ "codex",
25
+ "cursor",
26
+ "copilot",
27
+ "agy",
28
+ "opencode",
29
+ ]);
30
+
31
+ function parseArguments(arguments_) {
32
+ let project = process.cwd();
33
+ let platform = "all";
34
+ let secretName = "remote-agent-credentials";
35
+ for (const argument of arguments_) {
36
+ if (argument.startsWith("--project=")) {
37
+ project = path.resolve(argument.slice("--project=".length));
38
+ } else if (argument.startsWith("--platform=")) {
39
+ platform = argument.slice("--platform=".length).toLowerCase();
40
+ } else if (argument.startsWith("--secret-name=")) {
41
+ secretName = argument.slice("--secret-name=".length);
42
+ } else {
43
+ throw new Error(`Unknown argument: ${argument}`);
44
+ }
45
+ }
46
+ if (!VALID_PLATFORMS.has(platform)) {
47
+ throw new Error(
48
+ `Unsupported platform '${platform}'. Expected one of: ${[...VALID_PLATFORMS].join(", ")}`
49
+ );
50
+ }
51
+ if (!/^[A-Za-z0-9/_+=.@-]+$/.test(secretName)) {
52
+ throw new Error("--secret-name must be a valid Secrets Manager name");
53
+ }
54
+ return { project, platform, secretName };
55
+ }
56
+
57
+ function writeSetupScript(project) {
58
+ const source = path.join(
59
+ path.dirname(fileURLToPath(import.meta.url)),
60
+ SCRIPT_NAME
61
+ );
62
+ const destination = path.join(project, "scripts", SCRIPT_NAME);
63
+ mkdirSync(path.dirname(destination), { recursive: true });
64
+ writeFileSync(destination, readFileSync(source));
65
+ chmodSync(destination, 0o700);
66
+ return path.relative(project, destination);
67
+ }
68
+
69
+ function installCursorAdapter(project) {
70
+ const environmentPath = path.join(project, ".cursor", "environment.json");
71
+ mkdirSync(path.dirname(environmentPath), { recursive: true });
72
+
73
+ const environment = existsSync(environmentPath)
74
+ ? JSON.parse(readFileSync(environmentPath, "utf8"))
75
+ : {};
76
+ const existingInstall = environment.install;
77
+ if (existingInstall !== undefined && typeof existingInstall !== "string") {
78
+ throw new Error(
79
+ `.cursor/environment.json install must be a string before Lisa can merge ${INSTALL_COMMAND}`
80
+ );
81
+ }
82
+ const lisaInstall = `LISA_REMOTE_AGENT=cursor ${INSTALL_COMMAND}`;
83
+ if (!existingInstall?.includes(SCRIPT_NAME)) {
84
+ environment.install = existingInstall
85
+ ? `${lisaInstall} && ${existingInstall}`
86
+ : lisaInstall;
87
+ }
88
+ writeFileSync(environmentPath, `${JSON.stringify(environment, null, 2)}\n`);
89
+ return path.relative(project, environmentPath);
90
+ }
91
+
92
+ function installCopilotAdapter(project) {
93
+ const workflowPath = path.join(
94
+ project,
95
+ ".github",
96
+ "workflows",
97
+ "copilot-setup-steps.yml"
98
+ );
99
+ mkdirSync(path.dirname(workflowPath), { recursive: true });
100
+ const marker = "Lisa remote AWS bootstrap";
101
+ const secretEnvironment =
102
+ " LISA_AWS_BOOTSTRAP_JSON: ${{ secrets.LISA_AWS_BOOTSTRAP_JSON }}";
103
+ const checkoutStep = [
104
+ " - uses: actions/checkout@v4",
105
+ " with:",
106
+ " persist-credentials: false",
107
+ ].join("\n");
108
+ const step = [
109
+ ` - name: ${marker}`,
110
+ " env:",
111
+ secretEnvironment,
112
+ " LISA_REMOTE_AGENT: copilot",
113
+ ` run: ${INSTALL_COMMAND}`,
114
+ ].join("\n");
115
+
116
+ if (!existsSync(workflowPath)) {
117
+ writeFileSync(
118
+ workflowPath,
119
+ [
120
+ 'name: "Copilot Setup Steps"',
121
+ "on:",
122
+ " workflow_dispatch:",
123
+ "jobs:",
124
+ " copilot-setup-steps:",
125
+ " runs-on: ubuntu-latest",
126
+ " permissions:",
127
+ " contents: read",
128
+ " steps:",
129
+ checkoutStep,
130
+ step,
131
+ "",
132
+ ].join("\n")
133
+ );
134
+ return path.relative(project, workflowPath);
135
+ }
136
+
137
+ const current = readFileSync(workflowPath, "utf8");
138
+ const lines = current.split("\n");
139
+ const jobIndex = lines.findIndex(line =>
140
+ /^\s{2}copilot-setup-steps:\s*$/.test(line)
141
+ );
142
+ const jobEndIndex = lines.findIndex(
143
+ (line, index) => index > jobIndex && /^\s{2}\S[^:]*:\s*$/.test(line)
144
+ );
145
+ const stepsIndex = lines.findIndex(
146
+ (line, index) =>
147
+ index > jobIndex &&
148
+ (jobEndIndex < 0 || index < jobEndIndex) &&
149
+ /^\s{4}steps:\s*$/.test(line)
150
+ );
151
+ if (jobIndex < 0 || stepsIndex < 0) {
152
+ throw new Error(
153
+ "Existing copilot-setup-steps.yml must contain jobs.copilot-setup-steps.steps before Lisa can merge the AWS bootstrap step"
154
+ );
155
+ }
156
+
157
+ let changed = false;
158
+ const checkoutIndex = lines.findIndex(
159
+ (line, index) =>
160
+ index > stepsIndex &&
161
+ (jobEndIndex < 0 || index < jobEndIndex) &&
162
+ /^\s{6}-\s+uses:\s+actions\/checkout@/.test(line)
163
+ );
164
+ if (checkoutIndex >= 0) {
165
+ const nextStepIndex = lines.findIndex(
166
+ (line, index) => index > checkoutIndex && /^\s{6}-\s/.test(line)
167
+ );
168
+ const checkoutEnd = nextStepIndex < 0 ? lines.length : nextStepIndex;
169
+ const withIndex = lines.findIndex(
170
+ (line, index) =>
171
+ index > checkoutIndex &&
172
+ index < checkoutEnd &&
173
+ /^\s{8}with:\s*$/.test(line)
174
+ );
175
+ const persistenceIndex = lines.findIndex(
176
+ (line, index) =>
177
+ index > checkoutIndex &&
178
+ index < checkoutEnd &&
179
+ /^\s{10}persist-credentials:\s*/.test(line)
180
+ );
181
+ if (persistenceIndex >= 0) {
182
+ if (lines[persistenceIndex] !== " persist-credentials: false") {
183
+ lines[persistenceIndex] = " persist-credentials: false";
184
+ changed = true;
185
+ }
186
+ } else if (withIndex >= 0) {
187
+ lines.splice(withIndex + 1, 0, " persist-credentials: false");
188
+ changed = true;
189
+ } else {
190
+ lines.splice(
191
+ checkoutIndex + 1,
192
+ 0,
193
+ " with:",
194
+ " persist-credentials: false"
195
+ );
196
+ changed = true;
197
+ }
198
+ }
199
+
200
+ const markerIndex = lines.findIndex(line => line.includes(marker));
201
+ if (markerIndex >= 0) {
202
+ const nextStepIndex = lines.findIndex(
203
+ (line, index) => index > markerIndex && /^\s{6}-\s/.test(line)
204
+ );
205
+ const managedStepEnd = nextStepIndex < 0 ? lines.length : nextStepIndex;
206
+ const managedStep = lines.slice(markerIndex, managedStepEnd);
207
+ if (!managedStep.includes(secretEnvironment)) {
208
+ const environmentIndex = lines.findIndex(
209
+ (line, index) =>
210
+ index > markerIndex &&
211
+ index < managedStepEnd &&
212
+ /^\s{8}env:\s*$/.test(line)
213
+ );
214
+ if (environmentIndex < 0) {
215
+ throw new Error(
216
+ "Existing Lisa remote AWS bootstrap step must contain an env block"
217
+ );
218
+ }
219
+ lines.splice(environmentIndex + 1, 0, secretEnvironment);
220
+ changed = true;
221
+ }
222
+ if (checkoutIndex < 0) {
223
+ lines.splice(stepsIndex + 1, 0, ...checkoutStep.split("\n"));
224
+ changed = true;
225
+ }
226
+ if (changed) writeFileSync(workflowPath, lines.join("\n"));
227
+ return path.relative(project, workflowPath);
228
+ }
229
+ const additions = checkoutIndex >= 0 ? [step] : [checkoutStep, step];
230
+ const nextCheckoutStepIndex = lines.findIndex(
231
+ (line, index) => index > checkoutIndex && /^\s{6}-\s/.test(line)
232
+ );
233
+ const updatedJobEndIndex = lines.findIndex(
234
+ (line, index) => index > jobIndex && /^\s{2}\S[^:]*:\s*$/.test(line)
235
+ );
236
+ const additionIndex =
237
+ checkoutIndex < 0
238
+ ? stepsIndex + 1
239
+ : nextCheckoutStepIndex >= 0
240
+ ? nextCheckoutStepIndex
241
+ : updatedJobEndIndex >= 0
242
+ ? updatedJobEndIndex
243
+ : lines.length;
244
+ lines.splice(additionIndex, 0, ...additions);
245
+ writeFileSync(workflowPath, lines.join("\n"));
246
+ return path.relative(project, workflowPath);
247
+ }
248
+
249
+ function writeGuide(project, platform, secretName) {
250
+ const guidePath = path.join(project, "docs", "remote-agent-aws.md");
251
+ mkdirSync(path.dirname(guidePath), { recursive: true });
252
+ writeFileSync(
253
+ guidePath,
254
+ `# Remote coding-agent AWS access
255
+
256
+ ## Context
257
+
258
+ This repository uses Lisa's vendor-neutral AWS bootstrap. Configure the complete
259
+ Secrets Manager SecretString as the secret \`LISA_AWS_BOOTSTRAP_JSON\`; do not
260
+ split it into \`AWS_ACCESS_KEY_ID\` and \`AWS_SECRET_ACCESS_KEY\` environment
261
+ variables. The setup script writes a named source profile whose only permission
262
+ is \`sts:AssumeRole\`, then creates automatically refreshed role profiles.
263
+
264
+ ## Goal
265
+
266
+ Give supported remote coding agents renewable, least-privilege AWS CLI access
267
+ without borrowing a developer identity or granting direct production repair.
268
+
269
+ ## Changes
270
+
271
+ | Runtime | Setup |
272
+ |---|---|
273
+ | Claude | Add \`LISA_AWS_BOOTSTRAP_JSON\` to the cloud environment, set \`LISA_REMOTE_AGENT=claude\`, and run \`${INSTALL_COMMAND}\` in the setup script. |
274
+ | Codex | Add \`LISA_AWS_BOOTSTRAP_JSON\` as a setup secret, set \`LISA_REMOTE_AGENT=codex\`, and run \`${INSTALL_COMMAND}\` in the setup script. Permit required AWS endpoints during the agent phase. |
275
+ | Cursor | Add the secret to the selected cloud environment. \`.cursor/environment.json\` runs the setup script with \`LISA_REMOTE_AGENT=cursor\`. |
276
+ | Copilot | Add the value as an organization-level **Agents** secret. \`copilot-setup-steps.yml\` runs the setup script with \`LISA_REMOTE_AGENT=copilot\`. |
277
+ | Antigravity | Direct AWS CLI is supported only on a user-managed remote host. Google's managed preview currently documents proxy-injected HTTP credentials, not arbitrary AWS credential files. |
278
+ | OpenCode | Run the setup script on the VPS/container that hosts \`opencode serve\`, with \`LISA_REMOTE_AGENT=opencode\`. OpenCode does not supply the host. |
279
+
280
+ Generated for: \`${platform}\`.
281
+
282
+ ## Implementation
283
+
284
+ The bootstrap bundle defines the available profile names. \`dev\` is the
285
+ default when present. Select another account explicitly, for example:
286
+
287
+ \`\`\`bash
288
+ aws --profile staging sts get-caller-identity
289
+ aws --profile production cloudformation describe-stacks
290
+ \`\`\`
291
+
292
+ Production and shared profiles are observer-only. Production repair remains a
293
+ human-driven local-workstation operation.
294
+
295
+ After the infrastructure pipeline deploys the remote-agent IAM stacks, retrieve
296
+ the complete bundle from the shared account. Do not extract or distribute its
297
+ individual access-key fields:
298
+
299
+ \`\`\`bash
300
+ aws --profile shared secretsmanager get-secret-value \\
301
+ --secret-id ${secretName} \\
302
+ --query SecretString \\
303
+ --output text
304
+ \`\`\`
305
+
306
+ Store that exact output as the masked secret \`LISA_AWS_BOOTSTRAP_JSON\` on each
307
+ remote-agent platform. Run the setup command and require its live
308
+ \`sts:GetCallerIdentity\` check to pass before considering the platform ready.
309
+
310
+ ## Notes
311
+
312
+ Rotate by replacing the bootstrap IAM access key through infrastructure and
313
+ then updating this one secret on every configured platform. Disabling or
314
+ deleting the bootstrap user immediately prevents new role sessions.
315
+ `
316
+ );
317
+ return path.relative(project, guidePath);
318
+ }
319
+
320
+ export function installRemoteAgentAws(arguments_ = process.argv.slice(2)) {
321
+ const { project, platform, secretName } = parseArguments(arguments_);
322
+ if (!existsSync(project))
323
+ throw new Error(`Project does not exist: ${project}`);
324
+
325
+ const written = [writeSetupScript(project)];
326
+ if (platform === "all" || platform === "cursor") {
327
+ written.push(installCursorAdapter(project));
328
+ }
329
+ if (platform === "all" || platform === "copilot") {
330
+ written.push(installCopilotAdapter(project));
331
+ }
332
+ written.push(writeGuide(project, platform, secretName));
333
+ return { project, platform, secretName, written };
334
+ }
335
+
336
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
337
+ try {
338
+ process.stdout.write(
339
+ `${JSON.stringify(installRemoteAgentAws(), null, 2)}\n`
340
+ );
341
+ } catch (error) {
342
+ process.stderr.write(
343
+ `install-remote-agent-aws: ${error instanceof Error ? error.message : String(error)}\n`
344
+ );
345
+ process.exitCode = 1;
346
+ }
347
+ }
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bash
2
+ # Vendor-neutral AWS CLI bootstrap for remote coding environments. This is the
3
+ # source copied into every generated Lisa agent plugin.
4
+ #
5
+ # Required secret:
6
+ # LISA_AWS_BOOTSTRAP_JSON Complete SecretString emitted by cdkstarter's
7
+ # remote-agent IAM kit.
8
+ # Optional plain variables:
9
+ # LISA_REMOTE_AGENT claude | codex | cursor | copilot | agy | opencode
10
+ # LISA_AWS_DEFAULT_PROFILE Defaults to dev, then the first available profile.
11
+ # LISA_AWS_SKIP_VERIFY Set to 1 only for an offline image build.
12
+
13
+ set -euo pipefail
14
+ umask 077
15
+
16
+ BOOTSTRAP_PROFILE="lisa-remote-agent-bootstrap"
17
+ AWS_CLI_VERSION="2.36.2"
18
+
19
+ fail() {
20
+ echo "remote-agent-aws-setup: $*" >&2
21
+ exit 1
22
+ }
23
+
24
+ need() {
25
+ command -v "$1" >/dev/null 2>&1
26
+ }
27
+
28
+ as_root() {
29
+ if [ "$(id -u)" = "0" ]; then
30
+ "$@"
31
+ elif need sudo; then
32
+ sudo "$@"
33
+ else
34
+ fail "root access is required to install $1"
35
+ fi
36
+ }
37
+
38
+ install_base_tools() {
39
+ need curl && need unzip && need jq && need gpg && return 0
40
+
41
+ if need apt-get; then
42
+ as_root apt-get update -y
43
+ as_root apt-get install -y curl unzip jq gnupg
44
+ elif need dnf; then
45
+ as_root dnf install -y curl unzip jq gnupg2
46
+ elif need yum; then
47
+ as_root yum install -y curl unzip jq gnupg2
48
+ else
49
+ fail "install curl, unzip, jq, and gpg in the remote image before running this script"
50
+ fi
51
+ }
52
+
53
+ install_aws_cli() {
54
+ need aws && return 0
55
+
56
+ local architecture aws_architecture temporary_directory
57
+ architecture="$(uname -m)"
58
+ case "$architecture" in
59
+ x86_64 | amd64) aws_architecture="x86_64" ;;
60
+ aarch64 | arm64) aws_architecture="aarch64" ;;
61
+ *) fail "unsupported AWS CLI architecture: $architecture" ;;
62
+ esac
63
+
64
+ temporary_directory="$(mktemp -d)"
65
+ trap 'rm -rf "${temporary_directory:-}"' EXIT
66
+ local installer_url
67
+ installer_url="https://awscli.amazonaws.com/awscli-exe-linux-${aws_architecture}-${AWS_CLI_VERSION}.zip"
68
+ curl -fsSL \
69
+ "$installer_url" \
70
+ -o "$temporary_directory/awscliv2.zip"
71
+ curl -fsSL "$installer_url.sig" -o "$temporary_directory/awscliv2.sig"
72
+ cat >"$temporary_directory/aws-cli-public-key.asc" <<'AWS_CLI_PUBLIC_KEY'
73
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
74
+
75
+ mQINBF2Cr7UBEADJZHcgusOJl7ENSyumXh85z0TRV0xJorM2B/JL0kHOyigQluUG
76
+ ZMLhENaG0bYatdrKP+3H91lvK050pXwnO/R7fB/FSTouki4ciIx5OuLlnJZIxSzx
77
+ PqGl0mkxImLNbGWoi6Lto0LYxqHN2iQtzlwTVmq9733zd3XfcXrZ3+LblHAgEt5G
78
+ TfNxEKJ8soPLyWmwDH6HWCnjZ/aIQRBTIQ05uVeEoYxSh6wOai7ss/KveoSNBbYz
79
+ gbdzoqI2Y8cgH2nbfgp3DSasaLZEdCSsIsK1u05CinE7k2qZ7KgKAUIcT/cR/grk
80
+ C6VwsnDU0OUCideXcQ8WeHutqvgZH1JgKDbznoIzeQHJD238GEu+eKhRHcz8/jeG
81
+ 94zkcgJOz3KbZGYMiTh277Fvj9zzvZsbMBCedV1BTg3TqgvdX4bdkhf5cH+7NtWO
82
+ lrFj6UwAsGukBTAOxC0l/dnSmZhJ7Z1KmEWilro/gOrjtOxqRQutlIqG22TaqoPG
83
+ fYVN+en3Zwbt97kcgZDwqbuykNt64oZWc4XKCa3mprEGC3IbJTBFqglXmZ7l9ywG
84
+ EEUJYOlb2XrSuPWml39beWdKM8kzr1OjnlOm6+lpTRCBfo0wa9F8YZRhHPAkwKkX
85
+ XDeOGpWRj4ohOx0d2GWkyV5xyN14p2tQOCdOODmz80yUTgRpPVQUtOEhXQARAQAB
86
+ tCFBV1MgQ0xJIFRlYW0gPGF3cy1jbGlAYW1hem9uLmNvbT6JAlQEEwEIAD4CGwMF
87
+ CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQT7Xbd/1cEYuAURraimMQrMRnJHXAUC
88
+ akV0ygUJDqP4lQAKCRCmMQrMRnJHXFHjD/9eyZLYcKuQOlLvtqSDtUBiEZf6ZZjM
89
+ i3ygYH8rJNtuToUH+HvSpe819urJCquXhDrlK6N+aqW0hCLtNABJG/vsafIgvIYJ
90
+ hSGgpgtNnQyMV1jViRWqPjbouw8OkYKBThUfT1i2Y+wn58ifs6ODBCmTexWtXspA
91
+ Si+Gt49xDOW0APmbOPnI+a4HJW6tVEo6MWS0WjzpiBayR3d1A4pt4YrPfSdDgpLo
92
+ h2SLQqlRqvvVZJaWBjhkErNFpfsBA06sDcPEOb0G8LBUbR4WOcdvhe5LubJbZuxC
93
+ AG9kNPCVeQP1ixwjgjXKysaxeQ6rv0VzIQgRp6tLVLWhy6AKDNvLjFSsmXZ1Wl08
94
+ Y/RlOHXlzLuQMRE6sR1wOdRxc9TsrNWTGiBK65cvSWOy03JeBkQQ8pesqltiyxI9
95
+ U21kkgiXtTSKNGfKK8pO27D81YANhRqPK7iTp6kuFiY2WtOg90KTMNlIT+Ff85Y2
96
+ b1rHj6Z0SrCkJujhWk3IBPic/wJgz01LEc/OAdUPlby90RJZcIBhSlWhT7mXnXIO
97
+ c0HWlNQrns2s3CTyYwZSiSlYe9ApeLwhjDo8NhbFuCAy61l6O5UsR4AfZxx/rGKv
98
+ 2wFb1/RN/P4gNe6vmxZAPjR0AQcwD3tc2McimOLr/22kmPz8IH3I0X7WoSFr0Biz
99
+ E91G7bb0hOb/cA==
100
+ =knv7
101
+ -----END PGP PUBLIC KEY BLOCK-----
102
+ AWS_CLI_PUBLIC_KEY
103
+ mkdir -m 700 "$temporary_directory/gnupg"
104
+ gpg --batch --homedir "$temporary_directory/gnupg" \
105
+ --import "$temporary_directory/aws-cli-public-key.asc" >/dev/null 2>&1
106
+ gpg --batch --homedir "$temporary_directory/gnupg" \
107
+ --verify "$temporary_directory/awscliv2.sig" \
108
+ "$temporary_directory/awscliv2.zip"
109
+ unzip -q "$temporary_directory/awscliv2.zip" -d "$temporary_directory"
110
+ as_root "$temporary_directory/aws/install" \
111
+ --install-dir /usr/local/aws-cli \
112
+ --bin-dir /usr/local/bin
113
+ rm -rf "$temporary_directory"
114
+ trap - EXIT
115
+ }
116
+
117
+ sanitize_session_name() {
118
+ local candidate
119
+ candidate="$(printf '%s' "${LISA_REMOTE_AGENT:-remote-agent}" \
120
+ | sed 's/[^A-Za-z0-9_+=,.@-]/-/g' \
121
+ | cut -c1-64)"
122
+ [ "${#candidate}" -ge 2 ] || candidate="remote-agent"
123
+ printf '%s\n' "$candidate"
124
+ }
125
+
126
+ install_base_tools
127
+ install_aws_cli
128
+
129
+ [ -z "${AWS_ACCESS_KEY_ID:-}" ] || fail \
130
+ "do not set AWS_ACCESS_KEY_ID directly; set only LISA_AWS_BOOTSTRAP_JSON so role profiles cannot be bypassed"
131
+ [ -z "${AWS_SECRET_ACCESS_KEY:-}" ] || fail \
132
+ "do not set AWS_SECRET_ACCESS_KEY directly; set only LISA_AWS_BOOTSTRAP_JSON so role profiles cannot be bypassed"
133
+
134
+ bootstrap_json="${LISA_AWS_BOOTSTRAP_JSON:-}"
135
+ [ -n "$bootstrap_json" ] || fail "LISA_AWS_BOOTSTRAP_JSON is required"
136
+
137
+ printf '%s' "$bootstrap_json" | jq -e '
138
+ type == "object" and
139
+ (.accessKeyId | type == "string" and length > 0) and
140
+ (.secretAccessKey | type == "string" and length > 0) and
141
+ (.externalId | type == "string" and length > 0) and
142
+ (.profiles | (type == "object" or type == "string"))
143
+ ' >/dev/null || fail "LISA_AWS_BOOTSTRAP_JSON is not a valid remote-agent bootstrap bundle"
144
+
145
+ access_key_id="$(printf '%s' "$bootstrap_json" | jq -er '.accessKeyId')"
146
+ secret_access_key="$(printf '%s' "$bootstrap_json" | jq -er '.secretAccessKey')"
147
+ external_id="$(printf '%s' "$bootstrap_json" | jq -er '.externalId')"
148
+ session_token="$(printf '%s' "$bootstrap_json" | jq -r '.sessionToken // empty')"
149
+ profiles_json="$(printf '%s' "$bootstrap_json" | jq -c '
150
+ if (.profiles | type) == "string" then
151
+ .profiles | fromjson
152
+ else
153
+ .profiles
154
+ end
155
+ ')"
156
+
157
+ printf '%s' "$profiles_json" | jq -e '
158
+ type == "object" and length > 0 and
159
+ all(to_entries[];
160
+ (.key | test("^[A-Za-z0-9_-]+$")) and
161
+ (.value.roleArn | type == "string" and startswith("arn:aws:iam::")) and
162
+ (.value.region | type == "string" and length > 0)
163
+ )
164
+ ' >/dev/null || fail "bootstrap profiles must map safe names to roleArn and region"
165
+
166
+ mkdir -p "$HOME/.aws"
167
+ chmod 700 "$HOME/.aws"
168
+
169
+ aws configure set aws_access_key_id "$access_key_id" --profile "$BOOTSTRAP_PROFILE"
170
+ aws configure set aws_secret_access_key "$secret_access_key" --profile "$BOOTSTRAP_PROFILE"
171
+ if [ -n "$session_token" ]; then
172
+ aws configure set aws_session_token "$session_token" --profile "$BOOTSTRAP_PROFILE"
173
+ fi
174
+
175
+ session_name="$(sanitize_session_name)"
176
+ while IFS=$'\t' read -r profile_name role_arn region; do
177
+ aws configure set role_arn "$role_arn" --profile "$profile_name"
178
+ aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile "$profile_name"
179
+ aws configure set external_id "$external_id" --profile "$profile_name"
180
+ aws configure set role_session_name "$session_name" --profile "$profile_name"
181
+ aws configure set region "$region" --profile "$profile_name"
182
+ done < <(printf '%s' "$profiles_json" | jq -r 'to_entries[] | [.key, .value.roleArn, .value.region] | @tsv')
183
+
184
+ default_profile="${LISA_AWS_DEFAULT_PROFILE:-dev}"
185
+ if ! printf '%s' "$profiles_json" | jq -e --arg profile "$default_profile" 'has($profile)' >/dev/null; then
186
+ default_profile="$(printf '%s' "$profiles_json" | jq -r 'keys[0]')"
187
+ fi
188
+
189
+ default_role_arn="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].roleArn')"
190
+ default_region="$(printf '%s' "$profiles_json" | jq -er --arg profile "$default_profile" '.[$profile].region')"
191
+ aws configure set role_arn "$default_role_arn" --profile default
192
+ aws configure set source_profile "$BOOTSTRAP_PROFILE" --profile default
193
+ aws configure set external_id "$external_id" --profile default
194
+ aws configure set role_session_name "$session_name" --profile default
195
+ aws configure set region "$default_region" --profile default
196
+
197
+ chmod 600 "$HOME/.aws/credentials" "$HOME/.aws/config"
198
+
199
+ if [ "${LISA_AWS_SKIP_VERIFY:-0}" != "1" ]; then
200
+ AWS_PAGER="" aws sts get-caller-identity --profile "$default_profile" >/dev/null
201
+ fi
202
+
203
+ profile_names="$(printf '%s' "$profiles_json" | jq -r 'keys | join(", ")')"
204
+ echo "remote-agent-aws-setup: ready (${session_name}; default=${default_profile}; profiles=${profile_names})"
@@ -150,24 +150,27 @@ Group the findings as:
150
150
  Terraform providers).
151
151
  - `aws sso login`, `aws:signin:*`, `sso_start_url`, `sso_account_id`, `sso_role_name`, or
152
152
  `sso_session` in docs or config.
153
- - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_DEFAULT_REGION`,
154
- `AWS_PROFILE`, role ARN, external ID, account, or CloudWatch/STS references.
153
+ - `LISA_AWS_BOOTSTRAP_JSON`, legacy `AWS_ACCESS_KEY_ID` /
154
+ `AWS_SECRET_ACCESS_KEY`, `AWS_PROFILE`, role ARN, external ID, account, or
155
+ CloudWatch/STS references.
155
156
 
156
157
  If AWS is present, emit an AWS credential finding and inventory entry. If the repo's current
157
158
  local path is `aws sso login` or an `aws:signin:*` script, classify that local path as a `GAP`
158
159
  for headless remote routines: it is an interactive browser/device-authorization flow and cannot
159
160
  complete in the cloud. Route to the AWS row in the Credential reference instead of suggesting
160
- SSO auth. The finding must name the headless substrate: a dedicated IAM principal (user or role)
161
- with only permission to `sts:AssumeRole`, environment credentials in the routine UI, and
162
- `~/.aws/config` profiles using `role_arn`, `credential_source = Environment`, optional
163
- `external_id`, and `region`.
161
+ SSO auth. The finding must name Lisa's headless substrate: one dedicated IAM
162
+ user with only `sts:AssumeRole`; one complete Secrets Manager SecretString
163
+ set as `LISA_AWS_BOOTSTRAP_JSON`; and `/lisa:setup-remote-aws`, which writes a
164
+ named source profile plus automatically refreshed role profiles. Explicitly
165
+ reject standard `AWS_ACCESS_KEY_ID` variables because they can bypass the
166
+ intended assume-role profile.
164
167
 
165
168
  When the repository contains concrete non-secret AWS metadata (role ARNs, account aliases,
166
169
  profile names, regions, or ExternalId values), include it in an `awsProfiles` inventory array so
167
170
  `/lisa:generate-claude-remote-build-script` can write matching `~/.aws/config` profiles. Never
168
- invent account IDs, ExternalIds, role names, or regions. If AWS is detected but profile metadata
169
- is absent, emit the required AWS secret names plus an action to add project-specific profile
170
- metadata to the generated artifact or environment notes.
171
+ invent account IDs, ExternalIds, role names, or regions. If AWS is detected
172
+ but profile metadata is absent, require a cdkstarter bootstrap bundle rather
173
+ than asking the repository to reconstruct account metadata by hand.
171
174
 
172
175
  5. **MCP servers** — read every committed `.mcp.json`. For each server report transport and auth.
173
176
  Project-scoped HTTP/SSE servers with no interactive auth are `OK`. Flag stdio servers as
@@ -276,14 +279,14 @@ unsuffixed `…_TOKEN`/`…_KEY` is the simplest to set in a single-account rout
276
279
  - Access: the personal API key inherits the user's workspace permissions — the user must be able to read/create/update Issues in the destination team.
277
280
 
278
281
  ### AWS — host-project operations, logs, deploys, CDK/Serverless/SST, or AWS SDK usage
279
- - Headless substrate: a dedicated IAM principal (user or role) with long-lived bootstrap credentials
280
- stored in the routine environment, used only to call `sts:AssumeRole` into per-account operational
281
- roles. The routine writes `~/.aws/config` profiles with `role_arn`, `credential_source =
282
- Environment`, optional `external_id`, and `region`; agents use `aws --profile <profile> ...`.
283
- - Env: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`; optional `AWS_SESSION_TOKEN` for temporary
284
- bootstrap credentials; `AWS_DEFAULT_REGION` when no profile region is provided. Role ARNs,
285
- ExternalIds, profile names, and regions are non-secret project metadata and belong in generated
286
- artifacts or project docs, not in the skill's global guidance.
282
+ - Headless substrate: cdkstarter's vendor-neutral remote-agent kit. Its single
283
+ Secrets Manager SecretString contains the assume-only access key, ExternalId,
284
+ and per-account role metadata. `/lisa:setup-remote-aws` writes a private named
285
+ source profile and renewable role profiles; agents use
286
+ `aws --profile <profile> ...`.
287
+ - Env: `LISA_AWS_BOOTSTRAP_JSON` (secret, required) and
288
+ `LISA_REMOTE_AGENT` (plain platform label). Do not set `AWS_ACCESS_KEY_ID` or
289
+ `AWS_SECRET_ACCESS_KEY` in the remote environment.
287
290
  - Allowlist: `*.amazonaws.com`, or narrower service hosts such as `sts.amazonaws.com`,
288
291
  `logs.<region>.amazonaws.com`, `cloudwatch.<region>.amazonaws.com`,
289
292
  `xray.<region>.amazonaws.com`, `ssm.<region>.amazonaws.com`, and service-specific endpoints the
@@ -366,7 +369,7 @@ so the generator can render acquisition comments into its template:
366
369
  {
367
370
  "name": "dev",
368
371
  "roleArn": "arn:aws:iam::<account-id>:role/<role-name>",
369
- "credentialSource": "Environment",
372
+ "credentialSource": "lisa-remote-agent-bootstrap",
370
373
  "externalId": "<project-external-id>",
371
374
  "region": "us-east-1"
372
375
  }