@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.
- package/.jsii +10552 -0
- package/.kiro/specs/private-brain-construct-changes/design.md +147 -0
- package/.kiro/specs/private-brain-construct-changes/requirements.md +123 -0
- package/.kiro/specs/private-brain-construct-changes/tasks.md +119 -0
- package/API.md +1821 -0
- package/LICENSE +202 -0
- package/README.md +253 -0
- package/lib/crew-backup-bucket.d.ts +62 -0
- package/lib/crew-backup-bucket.js +108 -0
- package/lib/fargate-crew-base.d.ts +68 -0
- package/lib/fargate-crew-base.js +102 -0
- package/lib/fargate-crew.d.ts +89 -0
- package/lib/fargate-crew.js +200 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +22 -0
- package/lib/remote-crew-instance-props.d.ts +287 -0
- package/lib/remote-crew-instance-props.js +15 -0
- package/lib/remote-crew-instance.d.ts +35 -0
- package/lib/remote-crew-instance.js +309 -0
- package/package.json +138 -0
- package/src/assets/backup.sh +57 -0
- package/src/assets/bootstrap.sh +339 -0
- package/src/assets/restore-from-s3.sh +51 -0
- package/src/crew-backup-bucket.ts +107 -0
- package/src/fargate-crew-base.ts +109 -0
- package/src/fargate-crew.ts +248 -0
- package/src/index.ts +5 -0
- package/src/remote-crew-instance-props.ts +316 -0
- package/src/remote-crew-instance.ts +395 -0
|
@@ -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
|
+
}
|