@raindancers/raindancers-crew 0.0.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.
@@ -0,0 +1,339 @@
1
+ #!/bin/bash
2
+ # KiroCrew EC2 bootstrap (cloud-init, first boot, root).
3
+ #
4
+ # Ported faithfully from the upstream kirocrew-ec2.yaml UserData
5
+ # (github.com/kirodotdev/KiroCrew, src/kiro_crew/cloud/templates). The upstream
6
+ # CloudFormation !Sub placeholders are replaced by a small header that the
7
+ # RemoteCrewInstance construct prepends at synth time, so this file carries NO
8
+ # CloudFormation-specific syntax and can be linted/diffed as a plain script.
9
+ #
10
+ # The header the construct prepends defines, before this body runs:
11
+ # WAIT_HANDLE signed WaitCondition URL (cfn-signal target)
12
+ # DASHBOARD_PORT gateway port
13
+ # SOURCE_BUCKET S3 source bucket (empty => git clone)
14
+ # SOURCE_KEY S3 source key
15
+ # KIROCREW_REPO git repo to clone
16
+ # KIROCREW_REF git ref to install
17
+ # WEBHOOK_TOKEN_SECRET_ARN (optional) Secrets Manager ARN of the native-
18
+ # webhook Bearer token; when non-empty it is fetched at boot
19
+ # and written to config.json as hooks.webhook_token. Empty =>
20
+ # webhook auth unconfigured (loopback/SSM only), unchanged.
21
+ # CREW_AUTOPILOT (optional) "1" => set agent.approval_mode="auto" in
22
+ # config.json. Empty => gateway default (interactive).
23
+ # CREW_DISABLE_IDLE_CLOSE (optional) "1" => set session.timeout_secs=0
24
+ # (disables the idle session sweep). Empty => default 3600s.
25
+ #
26
+ # Security-load-bearing choices preserved from upstream: IMDSv2 is enforced at
27
+ # the instance (construct side), the Node tarball SHA-256 is verified before
28
+ # root extraction, the dashboard SPA build is a FATAL gate, and the bootstrap
29
+ # is resumable across a first-boot reboot.
30
+ set -u
31
+
32
+ BOOTSTRAP_SCRIPT=/usr/local/sbin/kirocrew-bootstrap
33
+ BOOTSTRAP_STATE=/var/lib/kirocrew-bootstrap
34
+ INSTALL_DONE=$BOOTSTRAP_STATE/install-complete
35
+ SIGNAL_DONE=$BOOTSTRAP_STATE/signal-complete
36
+
37
+ # cloud-init runs UserData once. Persist the rendered script and hand off to a
38
+ # systemd oneshot so a managed patch reboot can resume it on boot.
39
+ if [ "$#" -eq 0 ]; then
40
+ install -m 0700 "$0" "$BOOTSTRAP_SCRIPT"
41
+ cat > /etc/systemd/system/kirocrew-bootstrap.service <<'UNIT'
42
+ [Unit]
43
+ Description=Kiro Crew resumable bootstrap
44
+ After=network-online.target
45
+ Wants=network-online.target
46
+
47
+ [Service]
48
+ Type=oneshot
49
+ ExecStart=/usr/local/sbin/kirocrew-bootstrap --resume
50
+ RemainAfterExit=yes
51
+
52
+ [Install]
53
+ WantedBy=multi-user.target
54
+ UNIT
55
+ systemctl daemon-reload
56
+ systemctl enable kirocrew-bootstrap.service
57
+ systemctl start --no-block kirocrew-bootstrap.service
58
+ exit 0
59
+ fi
60
+
61
+ mkdir -p "$BOOTSTRAP_STATE"
62
+ [ -f "$SIGNAL_DONE" ] && exit 0
63
+ exec >>/var/log/kirocrew-setup.log 2>&1
64
+ echo "=== KiroCrew bootstrap starting $(date -u) ==="
65
+
66
+ HANDLE="$WAIT_HANDLE"
67
+ LOG=/var/log/kirocrew-setup.log
68
+
69
+ # Fold the log tail into a FAILURE reason so a rollback carries the real cause.
70
+ fail() {
71
+ err1=$(grep -aiE 'error|fail|cannot|denied|refused|curl: \(' "$LOG" 2>/dev/null \
72
+ | grep -av 'BOOTSTRAP FAILED' | tail -1 | tr -d '\r"\\' | tr -cd '\40-\176' | tail -c 200)
73
+ echo "BOOTSTRAP FAILED: $1"
74
+ tail_ctx=$(tail -n 25 "$LOG" 2>/dev/null | tr -d '\r"\\' \
75
+ | grep -aviE 'Installing (npm|kirocrew and) depend|building React app' \
76
+ | tr '\n' '|' | tr -cd '\40-\176' | tail -c 900)
77
+ reason="$(printf '%s' "$1" | tr -d '"\\' | tr '\n' '|' | tr -cd '\40-\176') :: $err1 :: ...$tail_ctx"
78
+ /opt/aws/bin/cfn-signal -e 1 -r "$(echo "$reason" | head -c 1000)" "$HANDLE" || \
79
+ curl -s -X PUT -H 'Content-Type:' --data-binary \
80
+ "{\"Status\":\"FAILURE\",\"Reason\":\"$(echo "$reason" | head -c 1000)\",\"UniqueId\":\"kirocrew\",\"Data\":\"fail\"}" "$HANDLE" || true
81
+ exit 1
82
+ }
83
+
84
+ if [ ! -f "$INSTALL_DONE" ]; then
85
+ systemctl stop kirocrew.service 2>/dev/null || true
86
+
87
+ echo "--- suppressing AL2023 first-boot SELinux reboot ---"
88
+ rm -f /run/cloud-init-selinux-reboot 2>/dev/null || true
89
+ rm -f /etc/cloud/cloud.cfg.d/40_selinux-reboot.cfg 2>/dev/null || true
90
+ shutdown -c 2>/dev/null || true
91
+
92
+ RUN_USER=ec2-user
93
+ RUN_HOME=/home/$RUN_USER
94
+
95
+ echo "--- provisioning swap (headroom for the vite build) ---"
96
+ if [ ! -e /swapfile ] && [ "$(free -m | awk '/^Swap:/ {print $2}')" = "0" ]; then
97
+ ( fallocate -l 4G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096 ) 2>/dev/null \
98
+ && chmod 600 /swapfile && mkswap /swapfile >/dev/null 2>&1 && swapon /swapfile \
99
+ && echo '/swapfile none swap sw 0 0' >> /etc/fstab \
100
+ && echo "swap on: $(free -m | awk '/^Swap:/ {print $2}') MB" \
101
+ || echo "swap provisioning skipped (non-fatal)"
102
+ else
103
+ echo "swap already present or /swapfile exists - skipping"
104
+ fi
105
+
106
+ echo "--- installing system packages ---"
107
+ dnf install -y git tmux python3 python3-pip unzip tar gzip which \
108
+ || fail "essential package install failed (git/python3/unzip)"
109
+ dnf install -y python3.12 python3.12-pip \
110
+ || echo "python3.12 unavailable from dnf; install.sh will provision one"
111
+ dnf install -y ripgrep || echo "ripgrep unavailable (optional), continuing"
112
+
113
+ echo "--- installing Node.js ---"
114
+ NODE_MAJOR_MIN=22
115
+ command -v node >/dev/null 2>&1 || dnf install -y nodejs npm || true
116
+ NODE_MAJOR=$(node --version 2>/dev/null | sed -n 's/^v\([0-9][0-9]*\).*/\1/p' | head -1)
117
+ [ -n "$NODE_MAJOR" ] || NODE_MAJOR=0
118
+ if [ "$NODE_MAJOR" -lt "$NODE_MAJOR_MIN" ]; then
119
+ NODE_V=v22.12.0
120
+ case "$(uname -m)" in
121
+ aarch64) NODE_ARCH=linux-arm64; NODE_SHA=9e7905fdee722f9650a03ae644b51c4c6effd3b98ac93c588700072ab35c9ddb ;;
122
+ x86_64) NODE_ARCH=linux-x64; NODE_SHA=e05a4d65232ae2b27b3d77da2e368522fb46b923335b8e0d5f77624c32484044 ;;
123
+ *) fail "unsupported arch for Node install: $(uname -m)" ;;
124
+ esac
125
+ NODE_TARB=node-$NODE_V-$NODE_ARCH
126
+ echo "installing Node $NODE_V ($NODE_ARCH) from the official nodejs.org tarball"
127
+ curl --proto '=https' --tlsv1.2 -fsSL "https://nodejs.org/dist/$NODE_V/$NODE_TARB.tar.gz" -o /tmp/node.tar.gz || fail "Node download failed"
128
+ # TLS protects transport, not the artifact: verify the PINNED SHA-256
129
+ # before extracting as root. Fails closed on any mismatch.
130
+ echo "$NODE_SHA /tmp/node.tar.gz" | sha256sum -c - || fail "Node tarball checksum mismatch (possible tampering)"
131
+ tar -xzf /tmp/node.tar.gz -C /usr/local || fail "Node extract failed"
132
+ ln -sf /usr/local/$NODE_TARB/bin/node /usr/local/bin/node
133
+ ln -sf /usr/local/$NODE_TARB/bin/npm /usr/local/bin/npm
134
+ ln -sf /usr/local/$NODE_TARB/bin/npx /usr/local/bin/npx
135
+ hash -r 2>/dev/null || true
136
+ NODE_MAJOR=$(node --version 2>/dev/null | sed -n 's/^v\([0-9][0-9]*\).*/\1/p' | head -1)
137
+ [ -n "$NODE_MAJOR" ] || NODE_MAJOR=0
138
+ fi
139
+ [ "$NODE_MAJOR" -ge "$NODE_MAJOR_MIN" ] || fail "Node.js too old (major $NODE_MAJOR, need >=$NODE_MAJOR_MIN)"
140
+ echo "node: $(command -v node) $(node --version 2>/dev/null || echo missing)"
141
+ echo "npm: $(command -v npm) $(npm --version 2>/dev/null || echo missing)"
142
+
143
+ echo "--- installing kiro-cli ---"
144
+ # AL2023 glibc 2.34 is too old for the standard build: musl required.
145
+ ARCH=$(uname -m)
146
+ if [ "$ARCH" = "aarch64" ]; then
147
+ KIRO_URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-aarch64-linux-musl.zip"
148
+ else
149
+ KIRO_URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-x86_64-linux-musl.zip"
150
+ fi
151
+ sudo -u $RUN_USER bash -lc "cd \$HOME; curl --proto '=https' --tlsv1.2 -sSf '$KIRO_URL' -o kirocli.zip" \
152
+ || fail "could not DOWNLOAD kiro-cli from $KIRO_URL (host unresolvable/unreachable here)"
153
+ sudo -u $RUN_USER bash -lc "
154
+ set -e
155
+ cd \$HOME
156
+ unzip -o -q kirocli.zip
157
+ ./kirocli/install.sh --no-confirm || yes | ./kirocli/install.sh || true
158
+ " || echo 'kiro-cli install returned nonzero (verifying the binary below)'
159
+ sudo -u $RUN_USER bash -lc 'command -v kiro-cli >/dev/null 2>&1 || [ -x "$HOME/.local/bin/kiro-cli" ] || [ -x /usr/local/bin/kiro-cli ]' \
160
+ || fail "kiro-cli did not install (the chat backend would not work)"
161
+
162
+ echo "--- fetching + installing KiroCrew ---"
163
+ cat > /tmp/kcfetch.sh <<KCFETCH
164
+ #!/bin/bash
165
+ set -e
166
+ # sudo resets PATH to secure_path; prepend /usr/local/bin so the build sees Node 22.
167
+ export PATH="/usr/local/bin:\$PATH"
168
+ cd "$RUN_HOME"
169
+ rm -rf kirocrew && mkdir kirocrew
170
+ if [ -n "$SOURCE_BUCKET" ]; then
171
+ echo "downloading source from s3://$SOURCE_BUCKET/$SOURCE_KEY"
172
+ aws s3 cp "s3://$SOURCE_BUCKET/$SOURCE_KEY" /tmp/kirocrew-src.tar.gz
173
+ tar -xzf /tmp/kirocrew-src.tar.gz -C kirocrew
174
+ else
175
+ echo "cloning $KIROCREW_REPO@$KIROCREW_REF"
176
+ rm -rf kirocrew
177
+ # '--' ends option parsing so the URL can't read as a git flag.
178
+ git clone --depth 1 --branch "$KIROCREW_REF" -- "$KIROCREW_REPO" kirocrew
179
+ fi
180
+ cd kirocrew
181
+ export KIROCREW_REQUIRE_FRONTEND=1
182
+ if ! bash install.sh --voice; then
183
+ echo "install.sh failed on first attempt - settling then retrying once"
184
+ sleep 20
185
+ bash install.sh --voice
186
+ fi
187
+ KCFETCH
188
+ chmod +x /tmp/kcfetch.sh
189
+ chown $RUN_USER /tmp/kcfetch.sh
190
+ sudo -u $RUN_USER bash /tmp/kcfetch.sh || fail "kirocrew source fetch or install failed"
191
+
192
+ echo "--- verifying the dashboard SPA was actually built ---"
193
+ DIST_INDEX="$RUN_HOME/kirocrew/src/kiro_crew/static/dist/index.html"
194
+ if [ ! -f "$DIST_INDEX" ]; then
195
+ fe_err="$(grep -aiE 'npm error|npm ERR|error TS[0-9]|vite|Killed|Cannot find module|ENOSPC|no space left|out of memory|FATAL ERROR|Skipping frontend build' "$LOG" 2>/dev/null | tr -d '\r"\\' | tail -n 12 | tr '\n' '|' | tail -c 500)"
196
+ [ -n "$fe_err" ] || fe_err="<none captured; check /var/log/kirocrew-setup.log on the instance>"
197
+ fail "dashboard frontend build missing (no static/dist). Build errors: $fe_err"
198
+ fi
199
+
200
+ echo "--- installing the systemd service ---"
201
+ sudo -u $RUN_USER bash -lc "
202
+ export PATH=\$HOME/.local/bin:\$PATH
203
+ kirocrew setup --agent-only || true
204
+ " || true
205
+
206
+ # --- Native-webhook Bearer token (RC3, guarded). When a Secrets Manager ARN
207
+ # was passed, fetch the token and write it into the crew config.json as
208
+ # hooks.webhook_token so POST /api/hooks/agent authenticates. The token is
209
+ # fetched at boot, never baked into userData/code. The gateway still binds
210
+ # loopback only; routable exposure is a consumer reverse-proxy/tunnel concern.
211
+ if [ -n "${WEBHOOK_TOKEN_SECRET_ARN:-}" ]; then
212
+ echo "--- configuring native-webhook token from Secrets Manager ---"
213
+ WEBHOOK_TOKEN=$(aws secretsmanager get-secret-value \
214
+ --secret-id "$WEBHOOK_TOKEN_SECRET_ARN" \
215
+ --query SecretString --output text 2>/dev/null) \
216
+ || fail "could not fetch the webhook token secret ($WEBHOOK_TOKEN_SECRET_ARN)"
217
+ if [ -z "$WEBHOOK_TOKEN" ]; then
218
+ fail "webhook token secret resolved empty ($WEBHOOK_TOKEN_SECRET_ARN)"
219
+ fi
220
+ CONFIG_DIR="$RUN_HOME/.kiro/crew"
221
+ CONFIG_JSON="$CONFIG_DIR/config.json"
222
+ sudo -u $RUN_USER mkdir -p "$CONFIG_DIR"
223
+ # Merge hooks.webhook_token into config.json without clobbering other keys.
224
+ # python3 is installed above; write the token via env, never on argv.
225
+ WEBHOOK_TOKEN="$WEBHOOK_TOKEN" CONFIG_JSON="$CONFIG_JSON" \
226
+ sudo -u $RUN_USER -E python3 - <<'PYEOF' \
227
+ || fail "could not write hooks.webhook_token into config.json"
228
+ import json, os
229
+ path = os.environ["CONFIG_JSON"]
230
+ token = os.environ["WEBHOOK_TOKEN"]
231
+ try:
232
+ with open(path) as f:
233
+ cfg = json.load(f)
234
+ if not isinstance(cfg, dict):
235
+ cfg = {}
236
+ except (FileNotFoundError, ValueError):
237
+ cfg = {}
238
+ hooks = cfg.get("hooks")
239
+ if not isinstance(hooks, dict):
240
+ hooks = {}
241
+ hooks["webhook_token"] = token
242
+ cfg["hooks"] = hooks
243
+ with open(path, "w") as f:
244
+ json.dump(cfg, f, indent=2)
245
+ PYEOF
246
+ chown "$RUN_USER":"$RUN_USER" "$CONFIG_JSON" 2>/dev/null || true
247
+ chmod 600 "$CONFIG_JSON" 2>/dev/null || true
248
+ unset WEBHOOK_TOKEN
249
+ echo "webhook token written to config.json (hooks.webhook_token)"
250
+ fi
251
+
252
+ # --- Always-on crew runtime (RC4, guarded). Merge autopilot / no-idle-close
253
+ # into config.json. Empty flags => keys untouched => current gateway defaults.
254
+ if [ -n "${CREW_AUTOPILOT:-}" ] || [ -n "${CREW_DISABLE_IDLE_CLOSE:-}" ]; then
255
+ echo "--- configuring always-on crew runtime (autopilot/idle-close) ---"
256
+ CONFIG_DIR="$RUN_HOME/.kiro/crew"
257
+ CONFIG_JSON="$CONFIG_DIR/config.json"
258
+ sudo -u $RUN_USER mkdir -p "$CONFIG_DIR"
259
+ CREW_AUTOPILOT="${CREW_AUTOPILOT:-}" \
260
+ CREW_DISABLE_IDLE_CLOSE="${CREW_DISABLE_IDLE_CLOSE:-}" \
261
+ CONFIG_JSON="$CONFIG_JSON" \
262
+ sudo -u $RUN_USER -E python3 - <<'PYEOF' \
263
+ || fail "could not write crew runtime settings into config.json"
264
+ import json, os
265
+ path = os.environ["CONFIG_JSON"]
266
+ try:
267
+ with open(path) as f:
268
+ cfg = json.load(f)
269
+ if not isinstance(cfg, dict):
270
+ cfg = {}
271
+ except (FileNotFoundError, ValueError):
272
+ cfg = {}
273
+ if os.environ.get("CREW_AUTOPILOT"):
274
+ agent = cfg.get("agent")
275
+ if not isinstance(agent, dict):
276
+ agent = {}
277
+ agent["approval_mode"] = "auto"
278
+ cfg["agent"] = agent
279
+ if os.environ.get("CREW_DISABLE_IDLE_CLOSE"):
280
+ session = cfg.get("session")
281
+ if not isinstance(session, dict):
282
+ session = {}
283
+ session["timeout_secs"] = 0
284
+ cfg["session"] = session
285
+ with open(path, "w") as f:
286
+ json.dump(cfg, f, indent=2)
287
+ PYEOF
288
+ chown "$RUN_USER":"$RUN_USER" "$CONFIG_JSON" 2>/dev/null || true
289
+ echo "crew runtime settings written to config.json"
290
+ fi
291
+
292
+ KIROCREW_BIN=$(sudo -u $RUN_USER bash -lc 'command -v kirocrew || echo $HOME/.local/bin/kirocrew')
293
+ cat > /etc/systemd/system/kirocrew.service <<UNIT
294
+ [Unit]
295
+ Description=KiroCrew AI Agent Gateway
296
+ After=network-online.target
297
+ Wants=network-online.target
298
+
299
+ [Service]
300
+ Type=simple
301
+ User=$RUN_USER
302
+ ExecStart=$KIROCREW_BIN gateway
303
+ Restart=on-failure
304
+ RestartSec=10
305
+ WorkingDirectory=$RUN_HOME
306
+ Environment=HOME=$RUN_HOME
307
+ Environment=PATH=$RUN_HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
308
+ Environment=KIROCREW_PORT=$DASHBOARD_PORT
309
+
310
+ [Install]
311
+ WantedBy=multi-user.target
312
+ UNIT
313
+ systemctl daemon-reload
314
+ touch "$INSTALL_DONE"
315
+ fi
316
+
317
+ # Safe to repeat after install completed but before the WaitCondition ack was recorded.
318
+ systemctl enable kirocrew.service || fail "could not enable kirocrew.service"
319
+ systemctl start kirocrew.service || fail "could not start kirocrew.service"
320
+
321
+ echo "--- waiting for the gateway to answer on 127.0.0.1:$DASHBOARD_PORT ---"
322
+ ok=0
323
+ for i in $(seq 1 60); do
324
+ if curl -fsS -o /dev/null http://127.0.0.1:$DASHBOARD_PORT/ 2>/dev/null; then ok=1; break; fi
325
+ sleep 5
326
+ done
327
+ [ "$ok" = "1" ] || fail "gateway did not become healthy within 5 minutes"
328
+
329
+ echo "=== KiroCrew bootstrap complete $(date -u) ==="
330
+ signal_ok=0
331
+ for attempt in 1 2 3; do
332
+ if /opt/aws/bin/cfn-signal -e 0 -r "kirocrew healthy" "$HANDLE"; then signal_ok=1; break; fi
333
+ if curl -fsS -X PUT -H 'Content-Type:' --data-binary \
334
+ "{\"Status\":\"SUCCESS\",\"Reason\":\"kirocrew healthy\",\"UniqueId\":\"kirocrew\",\"Data\":\"ok\"}" "$HANDLE"; then signal_ok=1; break; fi
335
+ echo "WaitCondition success signal attempt $attempt failed; retrying"
336
+ sleep 5
337
+ done
338
+ [ "$signal_ok" = "1" ] || { echo "could not deliver WaitCondition success"; exit 1; }
339
+ touch "$SIGNAL_DONE"
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # Restore a KiroCrew crew on a REPLACEMENT instance from an S3 snapshot backup.
3
+ # Installed at /usr/local/sbin/kirocrew-restore-from-s3 by RemoteCrewInstance
4
+ # when a backup bucket is configured. Injected header (prepended at synth):
5
+ # BACKUP_BUCKET source S3 bucket
6
+ # BACKUP_PREFIX key prefix
7
+ # AWS_REGION_ARG --region <region> or empty
8
+ #
9
+ # Usage: kirocrew-restore-from-s3 # restore the latest snapshot
10
+ # kirocrew-restore-from-s3 <s3-key> # restore a specific key
11
+ #
12
+ # Uses --mode replace: this is meant for a FRESH crew whose empty stores are
13
+ # cleared and rebuilt as the snapshot. It is NOT for merging into a crew whose
14
+ # current state you want to keep (use `kirocrew restore <file> --mode merge`
15
+ # by hand for that). Restore refuses while the gateway runs, so it is stopped
16
+ # first and restarted after.
17
+ set -eu
18
+
19
+ # Load the backup destination the construct baked in at install time.
20
+ if [ -f /etc/kirocrew/backup.env ]; then
21
+ # shellcheck disable=SC1091
22
+ . /etc/kirocrew/backup.env
23
+ fi
24
+ : "${BACKUP_BUCKET:?BACKUP_BUCKET not set (missing /etc/kirocrew/backup.env)}"
25
+ : "${BACKUP_PREFIX:=crew-snapshots/}"
26
+ : "${AWS_REGION_ARG:=}"
27
+
28
+ RUN_USER=ec2-user
29
+ RUN_HOME=/home/$RUN_USER
30
+ export HOME=$RUN_HOME
31
+ export PATH=$RUN_HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
32
+
33
+ KEY="${1:-${BACKUP_PREFIX}latest.tar}"
34
+ DEST=/tmp/kirocrew-restore.tar
35
+
36
+ echo "pulling s3://$BACKUP_BUCKET/$KEY"
37
+ # shellcheck disable=SC2086
38
+ aws s3 cp "s3://$BACKUP_BUCKET/$KEY" "$DEST" $AWS_REGION_ARG
39
+
40
+ echo "stopping the gateway before restore"
41
+ systemctl stop kirocrew.service 2>/dev/null || true
42
+
43
+ echo "restoring (mode=replace) as $RUN_USER"
44
+ sudo -u "$RUN_USER" env HOME="$RUN_HOME" PATH="$PATH" \
45
+ kirocrew restore "$DEST" --mode replace --force
46
+
47
+ echo "restarting the gateway"
48
+ systemctl start kirocrew.service
49
+
50
+ rm -f "$DEST"
51
+ echo "restore complete from s3://$BACKUP_BUCKET/$KEY"
@@ -0,0 +1,107 @@
1
+ import { Duration, RemovalPolicy } from 'aws-cdk-lib';
2
+ import * as iam from 'aws-cdk-lib/aws-iam';
3
+ import * as kms from 'aws-cdk-lib/aws-kms';
4
+ import * as s3 from 'aws-cdk-lib/aws-s3';
5
+ import { Construct } from 'constructs';
6
+ import { ICrewBackupBucket } from './remote-crew-instance-props';
7
+
8
+ /**
9
+ * Properties for {@link CrewBackupBucket}.
10
+ */
11
+ export interface CrewBackupBucketProps {
12
+ /**
13
+ * Explicit bucket name. Omit to let CloudFormation generate one.
14
+ *
15
+ * @default - CloudFormation-generated
16
+ */
17
+ readonly bucketName?: string;
18
+
19
+ /**
20
+ * Days after which a NONCURRENT snapshot version is expired. Current
21
+ * versions are always kept. Set 0 to keep all versions forever.
22
+ *
23
+ * @default 90
24
+ */
25
+ readonly noncurrentVersionExpirationDays?: number;
26
+
27
+ /**
28
+ * What happens to the bucket when the stack is destroyed. Defaults to
29
+ * RETAIN — the whole point is that the crew's learnings outlive the
30
+ * instance, so the backup must outlive a stack teardown too.
31
+ *
32
+ * @default RemovalPolicy.RETAIN
33
+ */
34
+ readonly removalPolicy?: RemovalPolicy;
35
+ }
36
+
37
+ /**
38
+ * A hardened S3 bucket for KiroCrew snapshot backups.
39
+ *
40
+ * The snapshot bundle produced by `kirocrew snapshot --purpose backup` is
41
+ * already redaction-scrubbed (the signing key, `.env`, and execution logs
42
+ * never ship), so this stores portable crew state, not raw secrets. The bucket
43
+ * is nonetheless locked down as if it did: SSE-KMS at rest, all public access
44
+ * blocked, TLS-only access, versioned so an overwrite cannot destroy history,
45
+ * and a lifecycle rule that expires stale noncurrent versions.
46
+ *
47
+ * Use {@link grantWrite} to let an instance/task role push snapshots, and
48
+ * {@link grantRead} to let a replacement instance pull them for restore.
49
+ */
50
+ export class CrewBackupBucket extends Construct implements ICrewBackupBucket {
51
+ /** The backup bucket. */
52
+ public readonly bucket: s3.IBucket;
53
+ /** The KMS key encrypting the bucket. */
54
+ public readonly key: kms.Key;
55
+
56
+ constructor(scope: Construct, id: string, props: CrewBackupBucketProps = {}) {
57
+ super(scope, id);
58
+
59
+ this.key = new kms.Key(this, 'Key', {
60
+ description: 'KiroCrew snapshot backup bucket encryption key',
61
+ enableKeyRotation: true,
62
+ removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,
63
+ });
64
+
65
+ const noncurrentDays = props.noncurrentVersionExpirationDays ?? 90;
66
+
67
+ this.bucket = new s3.Bucket(this, 'Bucket', {
68
+ bucketName: props.bucketName,
69
+ encryption: s3.BucketEncryption.KMS,
70
+ encryptionKey: this.key,
71
+ bucketKeyEnabled: true,
72
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
73
+ enforceSSL: true,
74
+ versioned: true,
75
+ removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,
76
+ lifecycleRules:
77
+ noncurrentDays > 0
78
+ ? [
79
+ {
80
+ // Keep current snapshots forever; expire superseded versions so
81
+ // the bucket does not grow without bound.
82
+ noncurrentVersionExpiration: Duration.days(noncurrentDays),
83
+ abortIncompleteMultipartUploadAfter: Duration.days(7),
84
+ },
85
+ ]
86
+ : undefined,
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Grant a principal permission to WRITE snapshots (and the KMS encrypt it
92
+ * needs). Used by the crew instance/task role that pushes backups.
93
+ */
94
+ public grantWrite(grantee: iam.IGrantable): void {
95
+ this.bucket.grantWrite(grantee);
96
+ this.key.grantEncryptDecrypt(grantee);
97
+ }
98
+
99
+ /**
100
+ * Grant a principal permission to READ snapshots (and the KMS decrypt it
101
+ * needs). Used by a replacement instance restoring from backup.
102
+ */
103
+ public grantRead(grantee: iam.IGrantable): void {
104
+ this.bucket.grantRead(grantee);
105
+ this.key.grantDecrypt(grantee);
106
+ }
107
+ }
@@ -0,0 +1,109 @@
1
+ import { Tags } from 'aws-cdk-lib';
2
+ import * as ec2 from 'aws-cdk-lib/aws-ec2';
3
+ import * as ecs from 'aws-cdk-lib/aws-ecs';
4
+ import { Construct } from 'constructs';
5
+
6
+ const STACK_TAG_RE = /^[a-zA-Z0-9-]{1,51}$/;
7
+
8
+ /**
9
+ * CPU architecture a crew container image was built for. Must match the image:
10
+ * a task whose runtime platform disagrees with its image fails at start.
11
+ */
12
+ export enum FargateCpuArchitecture {
13
+ X86_64 = 'X86_64',
14
+ ARM64 = 'ARM64',
15
+ }
16
+
17
+ /**
18
+ * Properties for {@link FargateCrewBase}.
19
+ */
20
+ export interface FargateCrewBaseProps {
21
+ /**
22
+ * VPC the crew tasks are placed in.
23
+ */
24
+ readonly vpc: ec2.IVpc;
25
+
26
+ /**
27
+ * Subnets for the tasks' awsvpc network interfaces. Each must be able to
28
+ * reach the container registry — a NAT-routed private subnet, or a public
29
+ * subnet with `assignPublicIp` at RunTask.
30
+ *
31
+ * Networking is taken, never invented: whether egress is via NAT or a public
32
+ * subnet is a property of the operator's VPC this construct cannot discover,
33
+ * so it is passed in and echoed as an output for the launch spec.
34
+ *
35
+ * @default - the VPC's private-with-egress subnets
36
+ */
37
+ readonly vpcSubnets?: ec2.SubnetSelection;
38
+
39
+ /**
40
+ * Architecture the crew image was built for. Surfaced as an output for the
41
+ * launch spec so every placement field is read from a stack, not the
42
+ * operator's memory.
43
+ *
44
+ * @default FargateCpuArchitecture.X86_64
45
+ */
46
+ readonly cpuArchitecture?: FargateCpuArchitecture;
47
+
48
+ /**
49
+ * Discovery tag value written as `kirocrew:fargate`. Must match
50
+ * `[a-zA-Z0-9-]{1,51}`. The cluster is named `kirocrew-crew-<stackTag>`.
51
+ *
52
+ * @default 'kirocrew'
53
+ */
54
+ readonly stackTag?: string;
55
+ }
56
+
57
+ /**
58
+ * Shared Fargate scaffolding for KiroCrew remote crews: the ECS cluster every
59
+ * crew task runs on and the egress-only security group they are placed in.
60
+ *
61
+ * ONE per account and region. The per-crew roles and log group live in
62
+ * {@link FargateCrew} (one per crew), so deleting a crew cannot delete the
63
+ * cluster its siblings run on.
64
+ *
65
+ * Port of the upstream `kirocrew-fargate-base` CloudFormation template.
66
+ */
67
+ export class FargateCrewBase extends Construct {
68
+ /** The ECS cluster crew tasks run on. */
69
+ public readonly cluster: ecs.Cluster;
70
+ /** The egress-only task security group (no inbound). */
71
+ public readonly securityGroup: ec2.SecurityGroup;
72
+ /** The `kirocrew:fargate` discovery tag value. */
73
+ public readonly stackTag: string;
74
+ /** The architecture crew images must be built for. */
75
+ public readonly cpuArchitecture: FargateCpuArchitecture;
76
+
77
+ constructor(scope: Construct, id: string, props: FargateCrewBaseProps) {
78
+ super(scope, id);
79
+
80
+ const stackTag = props.stackTag ?? 'kirocrew';
81
+ if (!STACK_TAG_RE.test(stackTag)) {
82
+ throw new Error(`stackTag must match ${STACK_TAG_RE} (got '${stackTag}')`);
83
+ }
84
+ this.stackTag = stackTag;
85
+ this.cpuArchitecture = props.cpuArchitecture ?? FargateCpuArchitecture.X86_64;
86
+
87
+ // Named from the tag (not discovered) so a launcher can derive it, and to
88
+ // keep tasks out of the account's implicit `default` cluster.
89
+ this.cluster = new ecs.Cluster(this, 'Cluster', {
90
+ vpc: props.vpc,
91
+ clusterName: `kirocrew-crew-${stackTag}`,
92
+ containerInsightsV2: ecs.ContainerInsights.DISABLED,
93
+ });
94
+
95
+ // No ingress at all. The crew's control surface is reached
96
+ // outbound-authenticated; nothing needs to dial in. Egress open so the
97
+ // task can pull its image and reach the model endpoint.
98
+ this.securityGroup = new ec2.SecurityGroup(this, 'TaskSecurityGroup', {
99
+ vpc: props.vpc,
100
+ description: `Kiro Crew Fargate crews ${stackTag} - egress only, no inbound`,
101
+ allowAllOutbound: true,
102
+ });
103
+
104
+ for (const taggable of [this.cluster, this.securityGroup]) {
105
+ Tags.of(taggable).add('kirocrew:managed', 'true');
106
+ Tags.of(taggable).add('kirocrew:fargate', stackTag);
107
+ }
108
+ }
109
+ }