From 497dd0180251fd74dc9c4ba82f546d93f685b548 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:32:45 +0000 Subject: [PATCH 1/5] feat(kirocrew): opt-in Telegram channel wiring during install Roy: 'add telegram auto-connection. ask user if they want to connect telegram. if yes, show them short guide. Get both info from them (allow retry if user pastes wrong formatted stuff). then during pack install, put both sets of info in [user home]/.kiro/crew/config.json: "telegram": { "enabled": true, "allowed_user_ids": [123456789] }'. Three-part change: 1. install.sh (wizard, pre-deploy): - New confirm block gated on PACK_NAME=kirocrew && !AUTO_YES, default_no (opt-in), before the existing Kiro API key prompt. - Short on-screen guide: @BotFather /newbot for token, @userinfobot for numeric user ID. - Bot token retry loop (5 attempts, regex ^[0-9]+:[A-Za-z0-9_-]+$). - User ID retry loop (5 attempts, regex ^[0-9]{5,15}$). - Enter or 'skip' at either prompt abandons both fields cleanly. - Bash 3.2-safe lowercasing via tr (Codex P1 pattern from #91). - Values written to shell vars KIROCREW_TG_BOT_TOKEN and KIROCREW_TG_USER_ID. 2. deploy/bootstrap.sh: - Plumb the two vars into PACK_CONFIG as new fields: 'telegram-bot-token' and 'telegram-user-id' (hyphenated keys to match the existing 'from-secret' / 'codex-model' style). 3. packs/kirocrew/install.sh (pack, on the instance): - Read the two values via pack_config_get (new PACK_ARG_TG_*). - New Step 15c writes both when present: * TELEGRAM_BOT_TOKEN= appended (idempotently) to the existing ~/.kiro/crew/.env managed by the pack. Env var is KiroCrew's preferred way (per their docs/telegram-integration.md) since it keeps the token out of config.json. * telegram.enabled=true + allowed_user_ids=[] jq-merged into ~/.kiro/crew/config.json (upgrade-safe: preserves other keys). * Fallback to a telegram-only overwrite if jq merge fails (invalid existing JSON), with warn. * Done BEFORE the gateway starts \u2014 no restart required. - Half-configured input (only one of the two) warns and skips both, so we never leave the channel in a broken state. Config path and key names verified against KiroCrew v0.3.0 docs: https://raw.githubusercontent.com/kirodotdev/KiroCrew/main/src/kiro_crew/docs/telegram-integration.md Verified: - bash -n install.sh: OK - bash -n deploy/bootstrap.sh: OK - bash -n packs/kirocrew/install.sh: OK - Diff: install.sh +87, deploy/bootstrap.sh +4, packs/kirocrew/install.sh +54 (net +145) --- deploy/bootstrap.sh | 4 ++ install.sh | 87 +++++++++++++++++++++++++++++++++++++++ packs/kirocrew/install.sh | 54 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 3f30641..6549f4b 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -274,6 +274,8 @@ jq -n \ --arg from_secret "$KIRO_FROM_SECRET" \ --arg telegram_bot_token_secret "$TELEGRAM_BOT_TOKEN_SECRET" \ --arg telegram_user "$TELEGRAM_USER" \ + --arg telegram_bot_token "${KIROCREW_TG_BOT_TOKEN:-}" \ + --arg telegram_user_id "${KIROCREW_TG_USER_ID:-}" \ --arg skip_telemetron "$SKIP_TELEMETRON" \ --arg primary "$PRIMARY" \ --arg daily_driver "$DAILY_DRIVER" \ @@ -286,6 +288,8 @@ jq -n \ "from-secret":$from_secret, telegram_bot_token_secret:$telegram_bot_token_secret, telegram_user:$telegram_user, + "telegram-bot-token":$telegram_bot_token, + "telegram-user-id":$telegram_user_id, "skip-telemetron":$skip_telemetron, primary:$primary, "daily-driver":$daily_driver, diff --git a/install.sh b/install.sh index 1316f7c..d6c4c63 100755 --- a/install.sh +++ b/install.sh @@ -3364,6 +3364,93 @@ run_config_and_review() { build_deploy_params fi + # Pack-specific: kirocrew Telegram channel setup + # Prompts the operator for a bot token + numeric user ID, both with retry + # loops that accept 'skip' or empty input to bail. Values flow to the pack + # via PACK_CONFIG (bootstrap.sh -> pack_config_get), which then writes: + # - bot token -> ~/.kiro/crew/.env (as TELEGRAM_BOT_TOKEN=...) + # - user ID -> ~/.kiro/crew/config.json ('telegram.allowed_user_ids') + # - enabled -> ~/.kiro/crew/config.json ('telegram.enabled' = true) + # Both writes are gated on both values being present. + KIROCREW_TG_BOT_TOKEN="" + KIROCREW_TG_USER_ID="" + if [[ "${PACK_NAME:-}" == "kirocrew" && "$AUTO_YES" != true ]]; then + if confirm "Connect KiroCrew to Telegram? (chat with your agent from your phone)" "default_no"; then + echo "" + echo -e " ${BOLD}Two things to get from Telegram before continuing:${NC}" + echo "" + echo -e " 1. ${BOLD}Create a bot${NC} — message ${CYAN}@BotFather${NC}, send ${BOLD}/newbot${NC}," + echo -e " and follow the prompts. You'll get a token like ${DIM}123456789:AA…${NC}" + echo "" + echo -e " 2. ${BOLD}Find your user ID${NC} — message ${CYAN}@userinfobot${NC}; it replies with" + echo -e " your number (e.g. ${DIM}123456789${NC}). That's the only account your" + echo -e " bot will answer." + echo "" + echo -e " ${DIM}Press Enter (empty) or type 'skip' at either prompt to skip Telegram setup.${NC}" + echo "" + + # --- Bot token: retry loop, format ^[0-9]+:[A-Za-z0-9_-]+$ --- + _KC_TG_ATTEMPTS=0 + _KC_TG_MAX=5 + while (( _KC_TG_ATTEMPTS < _KC_TG_MAX )); do + _KC_TG_ATTEMPTS=$((_KC_TG_ATTEMPTS + 1)) + _KC_TG_INPUT="" + prompt_secret "Telegram bot token" _KC_TG_INPUT "" + _KC_TG_INPUT_LC="$(printf '%s' "$_KC_TG_INPUT" | tr '[:upper:]' '[:lower:]')" + if [[ -z "$_KC_TG_INPUT" ]] || [[ "$_KC_TG_INPUT_LC" == "skip" ]]; then + KIROCREW_TG_BOT_TOKEN="" + break + fi + if [[ "$_KC_TG_INPUT" =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then + KIROCREW_TG_BOT_TOKEN="$_KC_TG_INPUT" + break + fi + _KC_TG_REMAINING=$((_KC_TG_MAX - _KC_TG_ATTEMPTS)) + if (( _KC_TG_REMAINING > 0 )); then + warn "Bot token doesn't match expected format (digits:alphanumerics, e.g. 123456789:AA-...). ${_KC_TG_REMAINING} attempt(s) left. Press Enter or type 'skip' to skip." + else + warn "Bot token invalid after ${_KC_TG_MAX} attempts. Skipping Telegram setup." + KIROCREW_TG_BOT_TOKEN="" + fi + done + + # --- User ID: retry loop, all-digit (Telegram user IDs are 32-bit+ ints) --- + if [[ -n "$KIROCREW_TG_BOT_TOKEN" ]]; then + _KC_TG_ATTEMPTS=0 + while (( _KC_TG_ATTEMPTS < _KC_TG_MAX )); do + _KC_TG_ATTEMPTS=$((_KC_TG_ATTEMPTS + 1)) + _KC_TG_INPUT="" + prompt "Your Telegram user ID (numeric)" _KC_TG_INPUT "" + _KC_TG_INPUT_LC="$(printf '%s' "$_KC_TG_INPUT" | tr '[:upper:]' '[:lower:]')" + if [[ -z "$_KC_TG_INPUT" ]] || [[ "$_KC_TG_INPUT_LC" == "skip" ]]; then + KIROCREW_TG_USER_ID="" + KIROCREW_TG_BOT_TOKEN="" # neither goes without both + break + fi + if [[ "$_KC_TG_INPUT" =~ ^[0-9]{5,15}$ ]]; then + KIROCREW_TG_USER_ID="$_KC_TG_INPUT" + break + fi + _KC_TG_REMAINING=$((_KC_TG_MAX - _KC_TG_ATTEMPTS)) + if (( _KC_TG_REMAINING > 0 )); then + warn "User ID must be all digits (5-15 chars). ${_KC_TG_REMAINING} attempt(s) left. Press Enter or type 'skip' to skip." + else + warn "User ID invalid after ${_KC_TG_MAX} attempts. Skipping Telegram setup." + KIROCREW_TG_USER_ID="" + KIROCREW_TG_BOT_TOKEN="" + fi + done + fi + unset _KC_TG_ATTEMPTS _KC_TG_MAX _KC_TG_INPUT _KC_TG_INPUT_LC _KC_TG_REMAINING + + if [[ -n "$KIROCREW_TG_BOT_TOKEN" && -n "$KIROCREW_TG_USER_ID" ]]; then + ok "Telegram setup captured (token + user ID ${KIROCREW_TG_USER_ID}); will be written to the instance during pack install." + else + info "Skipping Telegram setup — you can enable it later by editing ~/.kiro/crew/config.json on the instance." + fi + fi + fi + # Pack-specific: kiro-cli/kirocrew interactive API key for headless mode if [[ "${PACK_NAME:-}" == "kiro-cli" || "${PACK_NAME:-}" == "kirocrew" ]]; then if [[ -z "${KIRO_FROM_SECRET:-}" && "$AUTO_YES" != true ]]; then diff --git a/packs/kirocrew/install.sh b/packs/kirocrew/install.sh index 12f0d69..c2ea048 100755 --- a/packs/kirocrew/install.sh +++ b/packs/kirocrew/install.sh @@ -35,6 +35,8 @@ PACK_ARG_GATEWAY_PORT="$(pack_config_get gateway-port "5476")" PACK_ARG_START_GATEWAY="$(pack_config_get start-gateway "true")" PACK_ARG_KIROCREW_HOME="$(pack_config_get kirocrew-home "")" PACK_ARG_PROFILE="$(pack_config_get profile "")" +PACK_ARG_TG_BOT_TOKEN="$(pack_config_get telegram-bot-token "")" +PACK_ARG_TG_USER_ID="$(pack_config_get telegram-user-id "")" # ── Help ────────────────────────────────────────────────────────────────────── usage() { @@ -712,6 +714,58 @@ if [[ "${PACK_ARG_PROFILE:-}" == "builder" ]]; then chmod 600 "${KIROCREW_DENIED_FILE}" ok "denied_commands.json written to ${KIROCREW_DENIED_FILE} (builder profile, disable_all=true, merge-preserving)" fi + +# Step 15c: Telegram channel wiring (opt-in via wizard) +# When the operator opted in during the pre-deploy wizard, PACK_CONFIG carries +# telegram-bot-token and telegram-user-id. Write both: +# - TELEGRAM_BOT_TOKEN= to ~/.kiro/crew/.env (env var; kirocrew +# prefers this over the config.bot_token fallback so the secret stays out +# of config.json — see docs/telegram-integration.md) +# - telegram.enabled = true, telegram.allowed_user_ids = [] merged +# into ~/.kiro/crew/config.json via jq (upgrade-safe: preserves other keys) +# Done BEFORE the gateway starts so it picks up the channel on first boot. +if [[ -n "${PACK_ARG_TG_BOT_TOKEN:-}" && -n "${PACK_ARG_TG_USER_ID:-}" ]]; then + step "Wiring KiroCrew Telegram channel (bot token + allowed user ID)" + KIRO_USER="${KIRO_USER:-ec2-user}" + KIRO_USER_HOME="$(getent passwd "${KIRO_USER}" | cut -d: -f6 2>/dev/null || echo "/home/${KIRO_USER}")" + KIROCREW_ENV_DIR="${KIROCREW_HOME:-${KIRO_USER_HOME}/.kiro/crew}" + KIROCREW_ENV_FILE="${KIROCREW_ENV_DIR}/.env" + KIROCREW_MAIN_CFG="${KIROCREW_ENV_DIR}/config.json" + + # 1) TELEGRAM_BOT_TOKEN in the .env file (idempotent: strip any prior line first) + ( umask 077 + mkdir -p "${KIROCREW_ENV_DIR}" + if [[ -f "${KIROCREW_ENV_FILE}" ]]; then + grep -v '^TELEGRAM_BOT_TOKEN=' "${KIROCREW_ENV_FILE}" > "${KIROCREW_ENV_FILE}.tmp" 2>/dev/null || true + mv "${KIROCREW_ENV_FILE}.tmp" "${KIROCREW_ENV_FILE}" + fi + printf 'TELEGRAM_BOT_TOKEN=%s\n' "${PACK_ARG_TG_BOT_TOKEN}" >> "${KIROCREW_ENV_FILE}" + ) + chmod 600 "${KIROCREW_ENV_FILE}" + + # 2) Merge telegram.enabled + allowed_user_ids into config.json + # User IDs are numeric per Telegram / KiroCrew (allowed_user_ids: [123456789]). + if [[ -f "${KIROCREW_MAIN_CFG}" ]]; then + tmp_cfg="$(mktemp)" + if jq --argjson uid "${PACK_ARG_TG_USER_ID}" \ + '.telegram = ((.telegram // {}) + {enabled: true, allowed_user_ids: [$uid]})' \ + "${KIROCREW_MAIN_CFG}" > "${tmp_cfg}" 2>/dev/null; then + mv "${tmp_cfg}" "${KIROCREW_MAIN_CFG}" + else + rm -f "${tmp_cfg}" + warn "jq merge failed on ${KIROCREW_MAIN_CFG}; writing telegram-only config as fallback (existing keys will be lost)" + printf '{\n "telegram": { "enabled": true, "allowed_user_ids": [%s] }\n}\n' "${PACK_ARG_TG_USER_ID}" > "${KIROCREW_MAIN_CFG}" + fi + else + # First-boot: no config.json yet. Kirocrew will merge this with its defaults. + printf '{\n "telegram": { "enabled": true, "allowed_user_ids": [%s] }\n}\n' "${PACK_ARG_TG_USER_ID}" > "${KIROCREW_MAIN_CFG}" + fi + chown -R "${KIRO_USER}:${KIRO_USER}" "${KIROCREW_ENV_DIR}" 2>/dev/null || true + chmod 600 "${KIROCREW_MAIN_CFG}" + ok "Telegram wired: bot token in ${KIROCREW_ENV_FILE}, allowed_user_ids=[${PACK_ARG_TG_USER_ID}] in ${KIROCREW_MAIN_CFG}" +else + [[ -n "${PACK_ARG_TG_BOT_TOKEN:-}${PACK_ARG_TG_USER_ID:-}" ]] && warn "Telegram: got one of (bot token, user ID) but not both; skipping wiring." +fi # ── Step 16: Install systemd service ───────────────────────────────────────── if [[ "${START_GATEWAY}" == "true" ]]; then step "Installing kirocrew-gateway systemd service" From 4c9d1fdc94a09233455dbe550f0394c87ab335d7 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:54:55 +0000 Subject: [PATCH 2/5] fix(kirocrew): plumb Telegram creds through CFN into bootstrap (Codex P1) Codex P1 on PR #95: the two KIROCREW_TG_* vars set by the install wizard never reached the remote bootstrap, so packs/kirocrew/install.sh always read them as empty and silently skipped Telegram wiring even when the wizard reported the credentials were captured. Full chain now: 1. install.sh - PARAM_CFN_NAMES: append KirocrewTgBotToken, KirocrewTgUserId. - PARAM_VALUES: append matching ${KIROCREW_TG_BOT_TOKEN:-} and ${KIROCREW_TG_USER_ID:-} so build_deploy_params surfaces them to both CFN CLI and CFN deploy paths. 2. deploy/cloudformation/template.yaml - New parameters KirocrewTgBotToken and KirocrewTgUserId (NoEcho: true, Default: ''). - Added to the Model Access parameter group after TelegramUser. - UserData exports KIROCREW_TG_BOT_TOKEN / KIROCREW_TG_USER_ID from the CFN params. - UserData forwards them to bootstrap.sh as --kirocrew-tg-bot-token / --kirocrew-tg-user-id. 3. deploy/bootstrap.sh - Init both vars from environment (defense in depth). - New CLI flags --kirocrew-tg-bot-token and --kirocrew-tg-user-id with the same 'requires a value' guard used by the rest of the flags. - Existing PACK_CONFIG jq block already reads them, so pack-side resolution needs no further change on this commit. Ties directly to Codex review comment https://github.com/inceptionstack/lowkey/pull/95#discussion_r3837274293 --- deploy/bootstrap.sh | 12 ++++++++++++ deploy/cloudformation/template.yaml | 19 +++++++++++++++++++ install.sh | 4 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 6549f4b..e4b675d 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -134,6 +134,8 @@ CODEX_MODEL="" KIRO_FROM_SECRET="" TELEGRAM_BOT_TOKEN_SECRET="${TELEGRAM_BOT_TOKEN_SECRET:-}" TELEGRAM_USER="${TELEGRAM_USER:-}" +KIROCREW_TG_BOT_TOKEN="${KIROCREW_TG_BOT_TOKEN:-}" +KIROCREW_TG_USER_ID="${KIROCREW_TG_USER_ID:-}" while [[ $# -gt 0 ]]; do case "$1" in @@ -220,6 +222,16 @@ while [[ $# -gt 0 ]]; do TELEGRAM_USER="$2" shift 2 ;; + --kirocrew-tg-bot-token) + [[ $# -gt 1 ]] || { echo "ERROR: --kirocrew-tg-bot-token requires a value" >&2; exit 1; } + KIROCREW_TG_BOT_TOKEN="$2" + shift 2 + ;; + --kirocrew-tg-user-id) + [[ $# -gt 1 ]] || { echo "ERROR: --kirocrew-tg-user-id requires a value" >&2; exit 1; } + KIROCREW_TG_USER_ID="$2" + shift 2 + ;; --primary) [[ $# -gt 1 ]] || { echo "ERROR: --primary requires a value" >&2; exit 1; } PRIMARY="$2" diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 0ff8566..18f76c9 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -26,6 +26,8 @@ Metadata: - KiroFromSecret - TelegramBotTokenSecret - TelegramUser + - KirocrewTgBotToken + - KirocrewTgUserId - Primary - DailyDriver - CodexModel @@ -272,6 +274,19 @@ Parameters: Default: '' Description: "Telegram username for bot pairing (roundhouse pack only, without @ prefix)." + KirocrewTgBotToken: + Type: String + Default: '' + NoEcho: true + Description: "KiroCrew Telegram bot token (kirocrew pack only). Optional opt-in wiring. Format: :." + + KirocrewTgUserId: + Type: String + Default: '' + NoEcho: true + Description: "KiroCrew Telegram numeric user ID (kirocrew pack only). Optional opt-in wiring. Digits only." + + Primary: Type: String Default: openclaw @@ -1861,6 +1876,8 @@ Resources: export KIRO_FROM_SECRET="${KiroFromSecret}" export TELEGRAM_BOT_TOKEN_SECRET="${TelegramBotTokenSecret}" export TELEGRAM_USER="${TelegramUser}" + export KIROCREW_TG_BOT_TOKEN="${KirocrewTgBotToken}" + export KIROCREW_TG_USER_ID="${KirocrewTgUserId}" export PACK_NAME="${PackName}" export PROFILE_NAME="${ProfileName}" # Publish failure to SSM and signal CFN on any error @@ -1905,6 +1922,8 @@ Resources: --kiro-from-secret "$KIRO_FROM_SECRET" \ --telegram-bot-token-secret "$TELEGRAM_BOT_TOKEN_SECRET" \ --telegram-user "$TELEGRAM_USER" \ + --kirocrew-tg-bot-token "$KIROCREW_TG_BOT_TOKEN" \ + --kirocrew-tg-user-id "$KIROCREW_TG_USER_ID" \ --primary "${Primary}" \ --daily-driver "${DailyDriver}" \ --codex-model "${CodexModel}" diff --git a/install.sh b/install.sh index d6c4c63..54d4bba 100755 --- a/install.sh +++ b/install.sh @@ -2390,7 +2390,7 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console and CFN CLI # ============================================================================ # ⚠ KEEP THESE TWO ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaVersionArn EdgeConfigSecretName EdgeConfigSecretArn SigningKeySecretName SigningKeySecretArn) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaVersionArn EdgeConfigSecretName EdgeConfigSecretArn SigningKeySecretName SigningKeySecretArn KirocrewTgBotToken KirocrewTgUserId) PARAM_VALUES=() # populated by build_deploy_params() # Per-pack default model (passed to CFN DefaultModel / bootstrap.sh --model). @@ -2457,6 +2457,8 @@ build_deploy_params() { "${EDGE_CONFIG_SECRET_ARN:-}" "${SIGNING_KEY_SECRET_NAME:-}" "${SIGNING_KEY_SECRET_ARN:-}" + "${KIROCREW_TG_BOT_TOKEN:-}" + "${KIROCREW_TG_USER_ID:-}" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ From 818c0f858594f58dfd9503e008fceaea15b01ed1 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 23 Aug 2026 05:18:36 +0000 Subject: [PATCH 3/5] fix(kirocrew): store TG bot token in Secrets Manager + rebuild params after wizard (Codex P1 x2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two P1s posted by Codex on commit 4c9d1fd: P1 #A (install.sh:3378) — Rebuild parameters after collecting Telegram credentials build_deploy_params ran at line 3335 BEFORE the KiroCrew Telegram wizard block. When the operator opted in, KIROCREW_TG_* got populated in shell vars but PARAM_VALUES was already frozen with empty strings, so on the default CFN CLI path the pack still received empty values while the wizard reported success. Only the unrelated WebUI-auth CLI path happened to rebuild later. Fix: append a build_deploy_params call inside the success branch of the KiroCrew wizard (after the secret is created and KIROCREW_TG_BOT_TOKEN_SECRET is set), so PARAM_VALUES reflects the fresh state before the deploy step consumes it. P1 #B (template.yaml:1879) — Keep the bot token out of EC2 user data Passing the raw bot token as a CloudFormation parameter and !Sub-ing it into UserData embeds it in the instance's resolved user data attribute. NoEcho only masks the parameter DISPLAY; principals with ec2:DescribeInstanceAttribute --attribute userData can still recover the token in plaintext. Fix: store the token in Secrets Manager BEFORE stack deploy, pass only the secret arn/id through CFN + UserData, resolve on the instance via the instance role. Same pattern as the existing TelegramBotTokenSecret flow for roundhouse. Concrete changes: 1) install.sh - PARAM_CFN_NAMES: rename KirocrewTgBotToken -> KirocrewTgBotTokenSecret. - PARAM_VALUES: emit ${KIROCREW_TG_BOT_TOKEN_SECRET:-} instead of the plaintext token. - KiroCrew wizard success branch: create/update Secrets Manager secret /lowkey/${ENV_NAME}/kirocrew-telegram-bot-token with the captured token (tags: loki:managed, loki:pack=kirocrew, loki:env=${ENV_NAME}), set KIROCREW_TG_BOT_TOKEN_SECRET to the secret id, CLEAR the plaintext var so it can't leak downstream, then call build_deploy_params to refresh PARAM_VALUES. 2) deploy/cloudformation/template.yaml - Parameter KirocrewTgBotToken (NoEcho plaintext) REMOVED. - Parameter KirocrewTgBotTokenSecret (plaintext arn/id) ADDED. - UserData: export KIROCREW_TG_BOT_TOKEN_SECRET only; the token itself never appears in UserData. - New Condition HasKirocrewTgBotTokenSecret. - New Resource KirocrewTgSecretReadPolicy (AWS::IAM::Policy) granting the instance role secretsmanager:GetSecretValue + DescribeSecret, scoped by !Sub to arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}* — created only when the parameter is non-empty. - CFN flag propagation to bootstrap.sh updated to --kirocrew-tg-bot-token-secret. 3) deploy/bootstrap.sh - New CLI flag --kirocrew-tg-bot-token-secret (kept --kirocrew-tg-bot-token flag for backward-compat / local invocation). - New resolver block before the PACK_CONFIG jq build: when KIROCREW_TG_BOT_TOKEN is empty but KIROCREW_TG_BOT_TOKEN_SECRET is set, resolve via aws secretsmanager get-secret-value (region from REGION or IMDS) and populate KIROCREW_TG_BOT_TOKEN. Non-fatal on failure — pack side already tolerates missing token. - Rest of the pipeline (jq --arg telegram_bot_token → PACK_CONFIG → packs/kirocrew/install.sh) unchanged, so the pack keeps a single plaintext code path regardless of how the token was supplied. Verification: bash -n clean on install.sh, deploy/bootstrap.sh, and packs/kirocrew/install.sh. --- deploy/bootstrap.sh | 27 ++++++++++++++++++++ deploy/cloudformation/template.yaml | 39 +++++++++++++++++++++++------ install.sh | 34 ++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index e4b675d..6d5f28e 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -135,6 +135,7 @@ KIRO_FROM_SECRET="" TELEGRAM_BOT_TOKEN_SECRET="${TELEGRAM_BOT_TOKEN_SECRET:-}" TELEGRAM_USER="${TELEGRAM_USER:-}" KIROCREW_TG_BOT_TOKEN="${KIROCREW_TG_BOT_TOKEN:-}" +KIROCREW_TG_BOT_TOKEN_SECRET="${KIROCREW_TG_BOT_TOKEN_SECRET:-}" KIROCREW_TG_USER_ID="${KIROCREW_TG_USER_ID:-}" while [[ $# -gt 0 ]]; do @@ -227,6 +228,11 @@ while [[ $# -gt 0 ]]; do KIROCREW_TG_BOT_TOKEN="$2" shift 2 ;; + --kirocrew-tg-bot-token-secret) + [[ $# -gt 1 ]] || { echo "ERROR: --kirocrew-tg-bot-token-secret requires a value" >&2; exit 1; } + KIROCREW_TG_BOT_TOKEN_SECRET="$2" + shift 2 + ;; --kirocrew-tg-user-id) [[ $# -gt 1 ]] || { echo "ERROR: --kirocrew-tg-user-id requires a value" >&2; exit 1; } KIROCREW_TG_USER_ID="$2" @@ -268,6 +274,27 @@ if [[ -z "$PACK_NAME" ]]; then exit 1 fi +# ── Resolve KiroCrew Telegram bot-token secret (P1 security: token never in UserData) ── +# The installer stores the bot token in Secrets Manager and only passes the +# arn/id through the CFN parameter + UserData. Resolve it here before the +# pack-config JSON is built so the pack side keeps a single plaintext code +# path (telegram-bot-token) regardless of how the token was supplied. +if [[ -z "${KIROCREW_TG_BOT_TOKEN:-}" && -n "${KIROCREW_TG_BOT_TOKEN_SECRET:-}" ]]; then + # Instance role has secretsmanager:GetSecretValue on this specific secret; + # region auto-detected from IMDS if AWS_DEFAULT_REGION isn't already set. + _kc_tg_region="${REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" + _kc_tg_resolved="$(aws secretsmanager get-secret-value \ + --secret-id "${KIROCREW_TG_BOT_TOKEN_SECRET}" \ + --query SecretString --output text \ + --region "${_kc_tg_region}" 2>/dev/null || true)" + if [[ -n "${_kc_tg_resolved}" ]]; then + KIROCREW_TG_BOT_TOKEN="${_kc_tg_resolved}" + else + echo "WARN: could not resolve KirocrewTgBotTokenSecret (${KIROCREW_TG_BOT_TOKEN_SECRET}) — pack will skip Telegram wiring" >&2 + fi + unset _kc_tg_resolved _kc_tg_region +fi + # ── Write pack config JSON ──────────────────────────────────────────────────── PACK_CONFIG="/tmp/loki-pack-config.json" jq -n \ diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 18f76c9..62973d2 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -26,7 +26,7 @@ Metadata: - KiroFromSecret - TelegramBotTokenSecret - TelegramUser - - KirocrewTgBotToken + - KirocrewTgBotTokenSecret - KirocrewTgUserId - Primary - DailyDriver @@ -274,17 +274,15 @@ Parameters: Default: '' Description: "Telegram username for bot pairing (roundhouse pack only, without @ prefix)." - KirocrewTgBotToken: + KirocrewTgBotTokenSecret: Type: String Default: '' - NoEcho: true - Description: "KiroCrew Telegram bot token (kirocrew pack only). Optional opt-in wiring. Format: :." + Description: "AWS Secrets Manager secret id/arn holding the KiroCrew Telegram bot token (kirocrew pack only). The instance resolves the secret at bootstrap time via its IAM role — the token itself is never embedded in UserData." KirocrewTgUserId: Type: String Default: '' - NoEcho: true - Description: "KiroCrew Telegram numeric user ID (kirocrew pack only). Optional opt-in wiring. Digits only." + Description: "KiroCrew Telegram numeric user ID (kirocrew pack only). Opt-in wiring. Digits only. Not sensitive (same class as TelegramUser)." Primary: @@ -467,6 +465,7 @@ Conditions: CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] IsBuilder: !Equals [!Ref ProfileName, 'builder'] IsNotBuilder: !Not [!Condition IsBuilder] + HasKirocrewTgBotTokenSecret: !Not [!Equals [!Ref KirocrewTgBotTokenSecret, '']] IsAccountAssistant: !Equals [!Ref ProfileName, 'account_assistant'] IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] RunSecurityServices: !Not [!Condition IsPersonalAssistant] @@ -858,6 +857,30 @@ Resources: - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/loki/*' - !Sub 'arn:aws:cloudformation:*:${AWS::AccountId}:stack/*' + # KiroCrew Telegram bot-token secret read access. + # Only granted when a secret arn/id was supplied by the installer (P1 fix: + # the token itself is stored in Secrets Manager and never embedded in UserData). + # Uses !Sub with a wildcard suffix so both plain-name and full-arn inputs + # resolve correctly, scoped to the account + region of this stack. + KirocrewTgSecretReadPolicy: + Type: AWS::IAM::Policy + Condition: HasKirocrewTgBotTokenSecret + Properties: + PolicyName: !Sub '${EnvironmentName}-kirocrew-tg-secret-read' + Roles: + - !Ref InstanceRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: ReadKirocrewTelegramBotToken + Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:DescribeSecret + Resource: + - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}*' + - !Ref KirocrewTgBotTokenSecret + # Bedrock inference for account_assistant (ReadOnlyAccess does not include invoke) AccountAssistantBedrockPolicy: Type: AWS::IAM::Policy @@ -1876,7 +1899,7 @@ Resources: export KIRO_FROM_SECRET="${KiroFromSecret}" export TELEGRAM_BOT_TOKEN_SECRET="${TelegramBotTokenSecret}" export TELEGRAM_USER="${TelegramUser}" - export KIROCREW_TG_BOT_TOKEN="${KirocrewTgBotToken}" + export KIROCREW_TG_BOT_TOKEN_SECRET="${KirocrewTgBotTokenSecret}" export KIROCREW_TG_USER_ID="${KirocrewTgUserId}" export PACK_NAME="${PackName}" export PROFILE_NAME="${ProfileName}" @@ -1922,7 +1945,7 @@ Resources: --kiro-from-secret "$KIRO_FROM_SECRET" \ --telegram-bot-token-secret "$TELEGRAM_BOT_TOKEN_SECRET" \ --telegram-user "$TELEGRAM_USER" \ - --kirocrew-tg-bot-token "$KIROCREW_TG_BOT_TOKEN" \ + --kirocrew-tg-bot-token-secret "$KIROCREW_TG_BOT_TOKEN_SECRET" \ --kirocrew-tg-user-id "$KIROCREW_TG_USER_ID" \ --primary "${Primary}" \ --daily-driver "${DailyDriver}" \ diff --git a/install.sh b/install.sh index 54d4bba..b4e0701 100755 --- a/install.sh +++ b/install.sh @@ -2390,7 +2390,7 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console and CFN CLI # ============================================================================ # ⚠ KEEP THESE TWO ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaVersionArn EdgeConfigSecretName EdgeConfigSecretArn SigningKeySecretName SigningKeySecretArn KirocrewTgBotToken KirocrewTgUserId) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaVersionArn EdgeConfigSecretName EdgeConfigSecretArn SigningKeySecretName SigningKeySecretArn KirocrewTgBotTokenSecret KirocrewTgUserId) PARAM_VALUES=() # populated by build_deploy_params() # Per-pack default model (passed to CFN DefaultModel / bootstrap.sh --model). @@ -2457,7 +2457,7 @@ build_deploy_params() { "${EDGE_CONFIG_SECRET_ARN:-}" "${SIGNING_KEY_SECRET_NAME:-}" "${SIGNING_KEY_SECRET_ARN:-}" - "${KIROCREW_TG_BOT_TOKEN:-}" + "${KIROCREW_TG_BOT_TOKEN_SECRET:-}" "${KIROCREW_TG_USER_ID:-}" ) # Validate parallel arrays are in sync @@ -3375,6 +3375,7 @@ run_config_and_review() { # - enabled -> ~/.kiro/crew/config.json ('telegram.enabled' = true) # Both writes are gated on both values being present. KIROCREW_TG_BOT_TOKEN="" + KIROCREW_TG_BOT_TOKEN_SECRET="" KIROCREW_TG_USER_ID="" if [[ "${PACK_NAME:-}" == "kirocrew" && "$AUTO_YES" != true ]]; then if confirm "Connect KiroCrew to Telegram? (chat with your agent from your phone)" "default_no"; then @@ -3446,7 +3447,34 @@ run_config_and_review() { unset _KC_TG_ATTEMPTS _KC_TG_MAX _KC_TG_INPUT _KC_TG_INPUT_LC _KC_TG_REMAINING if [[ -n "$KIROCREW_TG_BOT_TOKEN" && -n "$KIROCREW_TG_USER_ID" ]]; then - ok "Telegram setup captured (token + user ID ${KIROCREW_TG_USER_ID}); will be written to the instance during pack install." + # Persist the bot token to Secrets Manager *before* the stack sees it, + # so the CloudFormation parameter can be a plaintext arn/id and the + # token itself never lands in the resolved EC2 UserData attribute + # (Codex P1 on 4c9d1fd: NoEcho only masks the parameter display, not + # ec2:DescribeInstanceAttribute --attribute userData). + _KC_TG_SECRET_NAME="/lowkey/${ENV_NAME}/kirocrew-telegram-bot-token" + log "Writing Telegram bot token to Secrets Manager: ${_KC_TG_SECRET_NAME}" + if aws secretsmanager describe-secret --secret-id "$_KC_TG_SECRET_NAME" --region "$DEPLOY_REGION" >/dev/null 2>&1; then + aws secretsmanager put-secret-value \ + --secret-id "$_KC_TG_SECRET_NAME" \ + --secret-string "$KIROCREW_TG_BOT_TOKEN" \ + --region "$DEPLOY_REGION" >/dev/null || fail "Failed to update Telegram bot token in Secrets Manager" + else + aws secretsmanager create-secret \ + --name "$_KC_TG_SECRET_NAME" \ + --description "KiroCrew Telegram bot token for ${ENV_NAME} (managed by lowkey install.sh)" \ + --secret-string "$KIROCREW_TG_BOT_TOKEN" \ + --tags "Key=loki:managed,Value=true" "Key=loki:pack,Value=kirocrew" "Key=loki:env,Value=${ENV_NAME}" \ + --region "$DEPLOY_REGION" >/dev/null || fail "Failed to create Telegram bot token secret in Secrets Manager" + fi + KIROCREW_TG_BOT_TOKEN_SECRET="$_KC_TG_SECRET_NAME" + # Drop the plaintext token from installer state — the arn is all CFN needs. + KIROCREW_TG_BOT_TOKEN="" + unset _KC_TG_SECRET_NAME + ok "Telegram setup captured (secret ${KIROCREW_TG_BOT_TOKEN_SECRET} + user ID ${KIROCREW_TG_USER_ID}); will be resolved on the instance during pack install." + # Rebuild PARAM_VALUES so the KirocrewTg* fields are no longer stale + # (Codex P1 on 4c9d1fd: build_deploy_params ran before this wizard). + build_deploy_params else info "Skipping Telegram setup — you can enable it later by editing ~/.kiro/crew/config.json on the instance." fi From 79c93b6e97b3d9c7e205babbb1b42974b6baf74a Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 23 Aug 2026 05:46:52 +0000 Subject: [PATCH 4/5] fix(kirocrew): inline TG secret policy, exempt account_assistant deny, defer secret write (Codex P1 x3 + P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P1s and one P2 posted by Codex on commit 818c0f8: P1 #C (template.yaml:882) — Emit only a valid secret ARN in the IAM policy KirocrewTgSecretReadPolicy Resource listed BOTH !Sub arn:aws:secretsmanager:...:secret:${KirocrewTgBotTokenSecret}* !Ref KirocrewTgBotTokenSecret Plain name: the !Ref is not an ARN, IAM rejects the policy. Full ARN: the !Sub produces arn:...:secret:arn:aws:... (nested), also invalid. P1 #D (template.yaml:879) — Exempt the Telegram secret from the account_assistant deny AccountAssistantDenyPolicy explicitly denies secretsmanager:GetSecretValue on Resource '*'; explicit Deny always wins over the sibling Allow, so the bootstrap resolver silently fails under the account_assistant profile and Telegram wiring is skipped. P1 #E (template.yaml:867) — Race between Instance and the sibling IAM Policy Instance depended only on InstanceProfile, not on the new AWS::IAM::Policy. UserData could start before IAM propagated the permission; the resolver runs once and swallows failure, permanently skipping Telegram wiring. P2 (install.sh:3461) — Defer creating the secret until deployment confirmation The wizard used to write to Secrets Manager BEFORE show_summary. Canceling at the summary or choosing 'Change settings' left an orphaned secret. Roundhouse and kiro-cli already defer. Concrete changes: 1) deploy/cloudformation/template.yaml - Removed sibling resource KirocrewTgSecretReadPolicy and its Condition HasKirocrewTgBotTokenSecret entirely. - Added the equivalent statement INLINE on InstanceRole.Policies: PolicyName: ${EnvironmentName}-kirocrew-tg-secret-read Sid: ReadKirocrewTelegramBotToken Action: secretsmanager:GetSecretValue, secretsmanager:DescribeSecret Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}* Attached at role creation - no race with UserData (P1 #E). Exactly ONE Resource form, well-formed for the installer's plain-name input - the wildcard suffix covers the Secrets Manager 6-char random suffix (P1 #C). - AccountAssistantDenyPolicy DenySecretValues statement: replaced Resource '*' with NotResource: [the same !Sub'd ARN pattern] so the deny still covers everything else but exempts this one bootstrap secret (P1 #D). 2) install.sh - Wizard success branch: no longer calls aws secretsmanager create-secret / put-secret-value. Only computes _KC_TG_SECRET_NAME, sets KIROCREW_TG_BOT_TOKEN_SECRET, keeps the plaintext token in-memory in KIROCREW_TG_BOT_TOKEN, and calls build_deploy_params to refresh PARAM_VALUES. - New deferred block placed immediately before the existing Roundhouse deferred secret write (post show_summary confirmation): materializes the secret via create-secret with put-secret-value fallback for updates, restore-secret guard, tags (loki:managed, loki:pack=kirocrew, loki:env=${ENV_NAME}), mktemp+chmod 600+file:// pattern to keep the token out of process args, then rm -f + unset to scrub state. Verification: bash -n clean on install.sh, deploy/bootstrap.sh, and packs/kirocrew/install.sh. Removed stale KirocrewTgSecretReadPolicy resource and HasKirocrewTgBotTokenSecret condition; verified inline policy + NotResource exemption + deferred write are all in place. --- deploy/cloudformation/template.yaml | 59 +++++++++++++----------- install.sh | 69 +++++++++++++++++++---------- 2 files changed, 79 insertions(+), 49 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 62973d2..14427c2 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -465,7 +465,6 @@ Conditions: CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] IsBuilder: !Equals [!Ref ProfileName, 'builder'] IsNotBuilder: !Not [!Condition IsBuilder] - HasKirocrewTgBotTokenSecret: !Not [!Equals [!Ref KirocrewTgBotTokenSecret, '']] IsAccountAssistant: !Equals [!Ref ProfileName, 'account_assistant'] IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] RunSecurityServices: !Not [!Condition IsPersonalAssistant] @@ -825,6 +824,32 @@ Resources: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - !If [IsBuilder, 'arn:aws:iam::aws:policy/AdministratorAccess', !Ref 'AWS::NoValue'] - !If [IsAccountAssistant, 'arn:aws:iam::aws:policy/ReadOnlyAccess', !Ref 'AWS::NoValue'] + Policies: + # KiroCrew Telegram bot-token secret read access. + # Inline on the role (not a sibling AWS::IAM::Policy) so it is + # attached at role creation — the Instance's DependsOn InstanceProfile + # is then sufficient and there is no race between UserData starting + # and the permission propagating (Codex P1 on 818c0f8). + # + # Installer always passes a plain secret name in the form + # /lowkey//kirocrew-telegram-bot-token, so the Resource is a + # single well-formed ARN built via !Sub — never mix a plain name + # with a pre-built ARN (Codex P1 on 818c0f8: doing both produced + # arn:...:secret:arn:aws:... which is malformed). + # + # Empty parameter → empty policy statement action; harmless because + # the resource pattern won't match anything, but the statement is + # present on the role so no follow-up IAM propagation is required. + - PolicyName: !Sub '${EnvironmentName}-kirocrew-tg-secret-read' + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: ReadKirocrewTelegramBotToken + Effect: Allow + Action: + - secretsmanager:GetSecretValue + - secretsmanager:DescribeSecret + Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}*' Tags: - Key: Name Value: !Sub '${EnvironmentName}-role' @@ -857,30 +882,6 @@ Resources: - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/loki/*' - !Sub 'arn:aws:cloudformation:*:${AWS::AccountId}:stack/*' - # KiroCrew Telegram bot-token secret read access. - # Only granted when a secret arn/id was supplied by the installer (P1 fix: - # the token itself is stored in Secrets Manager and never embedded in UserData). - # Uses !Sub with a wildcard suffix so both plain-name and full-arn inputs - # resolve correctly, scoped to the account + region of this stack. - KirocrewTgSecretReadPolicy: - Type: AWS::IAM::Policy - Condition: HasKirocrewTgBotTokenSecret - Properties: - PolicyName: !Sub '${EnvironmentName}-kirocrew-tg-secret-read' - Roles: - - !Ref InstanceRole - PolicyDocument: - Version: '2012-10-17' - Statement: - - Sid: ReadKirocrewTelegramBotToken - Effect: Allow - Action: - - secretsmanager:GetSecretValue - - secretsmanager:DescribeSecret - Resource: - - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}*' - - !Ref KirocrewTgBotTokenSecret - # Bedrock inference for account_assistant (ReadOnlyAccess does not include invoke) AccountAssistantBedrockPolicy: Type: AWS::IAM::Policy @@ -919,7 +920,13 @@ Resources: Action: - secretsmanager:GetSecretValue - secretsmanager:GetResourcePolicy - Resource: '*' + NotResource: + # Bootstrap needs to read the KiroCrew Telegram bot-token secret + # (installer-managed, plain name /lowkey//kirocrew-telegram-bot-token) + # even under account_assistant. Explicit Deny always wins over the + # matching Allow on InstanceRole, so exempt this one ARN pattern + # via NotResource (Codex P1 on 818c0f8). + - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${KirocrewTgBotTokenSecret}*' - Sid: DenyS3ObjectAccess Effect: Deny Action: diff --git a/install.sh b/install.sh index b4e0701..f1d62d8 100755 --- a/install.sh +++ b/install.sh @@ -3447,31 +3447,19 @@ run_config_and_review() { unset _KC_TG_ATTEMPTS _KC_TG_MAX _KC_TG_INPUT _KC_TG_INPUT_LC _KC_TG_REMAINING if [[ -n "$KIROCREW_TG_BOT_TOKEN" && -n "$KIROCREW_TG_USER_ID" ]]; then - # Persist the bot token to Secrets Manager *before* the stack sees it, - # so the CloudFormation parameter can be a plaintext arn/id and the - # token itself never lands in the resolved EC2 UserData attribute - # (Codex P1 on 4c9d1fd: NoEcho only masks the parameter display, not - # ec2:DescribeInstanceAttribute --attribute userData). + # Codex P2 (818c0f8): defer the Secrets Manager write until AFTER + # show_summary confirms the deploy — same pattern Roundhouse and + # kiro-cli already use. Here we only stash the token in a shell var + # and pre-compute the secret name so build_deploy_params can emit + # the correct CFN parameter value; the actual create-secret / + # put-secret-value call happens in the post-confirmation block + # (search for _KC_TG_SECRET_NAME below). _KC_TG_SECRET_NAME="/lowkey/${ENV_NAME}/kirocrew-telegram-bot-token" - log "Writing Telegram bot token to Secrets Manager: ${_KC_TG_SECRET_NAME}" - if aws secretsmanager describe-secret --secret-id "$_KC_TG_SECRET_NAME" --region "$DEPLOY_REGION" >/dev/null 2>&1; then - aws secretsmanager put-secret-value \ - --secret-id "$_KC_TG_SECRET_NAME" \ - --secret-string "$KIROCREW_TG_BOT_TOKEN" \ - --region "$DEPLOY_REGION" >/dev/null || fail "Failed to update Telegram bot token in Secrets Manager" - else - aws secretsmanager create-secret \ - --name "$_KC_TG_SECRET_NAME" \ - --description "KiroCrew Telegram bot token for ${ENV_NAME} (managed by lowkey install.sh)" \ - --secret-string "$KIROCREW_TG_BOT_TOKEN" \ - --tags "Key=loki:managed,Value=true" "Key=loki:pack,Value=kirocrew" "Key=loki:env,Value=${ENV_NAME}" \ - --region "$DEPLOY_REGION" >/dev/null || fail "Failed to create Telegram bot token secret in Secrets Manager" - fi KIROCREW_TG_BOT_TOKEN_SECRET="$_KC_TG_SECRET_NAME" - # Drop the plaintext token from installer state — the arn is all CFN needs. - KIROCREW_TG_BOT_TOKEN="" - unset _KC_TG_SECRET_NAME - ok "Telegram setup captured (secret ${KIROCREW_TG_BOT_TOKEN_SECRET} + user ID ${KIROCREW_TG_USER_ID}); will be resolved on the instance during pack install." + # NOTE: KIROCREW_TG_BOT_TOKEN stays populated in-memory; it is + # written to Secrets Manager and cleared only after the operator + # confirms deployment. + ok "Telegram setup captured (secret ${KIROCREW_TG_BOT_TOKEN_SECRET} + user ID ${KIROCREW_TG_USER_ID}); token will be stored after you confirm deployment." # Rebuild PARAM_VALUES so the KirocrewTg* fields are no longer stale # (Codex P1 on 4c9d1fd: build_deploy_params ran before this wizard). build_deploy_params @@ -3622,6 +3610,41 @@ main() { _telem_pack_selected 2>/dev/null || true _telem_method_selected 2>/dev/null || true + # KiroCrew Telegram: save bot token to Secrets Manager (deferred until after user confirmation) + # Codex P2 on 818c0f8: the wizard used to write this before show_summary, + # which orphaned a secret if the operator cancelled at the summary or + # chose "Change settings". Matches the Roundhouse + Kiro API-key pattern. + if [[ -n "${KIROCREW_TG_BOT_TOKEN:-}" && -n "${_KC_TG_SECRET_NAME:-}" ]]; then + info "Storing KiroCrew Telegram bot token in Secrets Manager: ${_KC_TG_SECRET_NAME}" + local kc_tg_token_file + kc_tg_token_file=$(mktemp /tmp/lowkey-kc-tg-token.XXXXXX) + chmod 600 "$kc_tg_token_file" + printf '%s' "$KIROCREW_TG_BOT_TOKEN" > "$kc_tg_token_file" + # Restore if in pending-deletion state (same guard as Roundhouse). + aws secretsmanager restore-secret --secret-id "$_KC_TG_SECRET_NAME" --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + local kc_tg_sm_err="" + if kc_tg_sm_err=$(aws secretsmanager create-secret \ + --name "$_KC_TG_SECRET_NAME" \ + --secret-string "file://${kc_tg_token_file}" \ + --description "KiroCrew Telegram bot token for ${ENV_NAME} (managed by lowkey install.sh)" \ + --tags "Key=loki:managed,Value=true" "Key=loki:pack,Value=kirocrew" "Key=loki:env,Value=${ENV_NAME}" \ + --region "$DEPLOY_REGION" 2>&1); then + ok "KiroCrew Telegram token saved to Secrets Manager" + elif kc_tg_sm_err=$(aws secretsmanager put-secret-value \ + --secret-id "$_KC_TG_SECRET_NAME" \ + --secret-string "file://${kc_tg_token_file}" \ + --region "$DEPLOY_REGION" 2>&1); then + ok "KiroCrew Telegram token updated in Secrets Manager" + else + rm -f "$kc_tg_token_file" + fail "Failed to save KiroCrew Telegram bot token to Secrets Manager: ${kc_tg_sm_err}" + fi + rm -f "$kc_tg_token_file" + # Scrub the plaintext token from installer state — CFN only needs the arn/id. + KIROCREW_TG_BOT_TOKEN="" + unset KIROCREW_TG_BOT_TOKEN _KC_TG_SECRET_NAME + fi + # Roundhouse: save bot token to Secrets Manager (deferred until after user confirmation) if [[ -n "${_RH_BOT_TOKEN:-}" && -n "${_RH_SECRET_NAME:-}" ]]; then info "Storing bot token in Secrets Manager: ${_RH_SECRET_NAME}" From 9bce967786c42a7da25e0a5045ba45e2edc72046 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 23 Aug 2026 05:59:54 +0000 Subject: [PATCH 5/5] fix(kirocrew): resolve TG secret in stack region, not Bedrock region (Codex P1) Codex P1 on 79c93b6 (deploy/bootstrap.sh region lookup): Chain of failure for deploys outside the Bedrock allowlist (us-east-1 us-west-2 eu-west-1 eu-central-1 eu-north-1 ap-northeast-1 ap-southeast-1) e.g. ap-south-1: install.sh:2421-2425 BEDROCK_REGION = DEPLOY_REGION if allowlisted, else us-east-1 install.sh (deferred) creates the KiroCrew TG secret in DEPLOY_REGION template.yaml UserData bootstrap.sh --region "$BEDROCK_REGION" bootstrap.sh sets REGION from --region Old resolver read ${REGION:-...} -> lookup in Bedrock region, not stack region 2>/dev/null || true swallowed the failure Telegram wiring silently skipped despite operator opt-in. Fix: thread the CFN stack region into bootstrap independently of the Bedrock region. 1) deploy/cloudformation/template.yaml UserData now exports STACK_REGION=${AWS::Region} in addition to the existing REGION and BEDROCK_REGION. STACK_REGION is documented as the authoritative CFN stack / deployment region for downstream resolvers that must hit AWS APIs where the installer created resources (Secrets Manager here; more callers can follow the same pattern). REGION and BEDROCK_REGION semantics unchanged for back-compat. 2) deploy/bootstrap.sh KiroCrew TG resolver region source-of-truth, in order: 1. STACK_REGION exported by UserData (authoritative). 2. IMDSv2 placement/region (works on any EC2, exact stack region). 3. AWS_DEFAULT_REGION. 4. REGION (Bedrock region) only as a last resort, so single-region deployments in the Bedrock allowlist still work when UserData is skipped (dev/manual invocation). WARN message now includes the region actually queried so a future silent-skip is easier to diagnose from the bootstrap log. Verification: bash -n clean on install.sh and deploy/bootstrap.sh; UserData exports STACK_REGION; resolver picks STACK_REGION first with IMDSv2 fallback. --- deploy/bootstrap.sh | 29 +++++++++++++++++++++++++---- deploy/cloudformation/template.yaml | 7 +++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 6d5f28e..630b045 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -279,10 +279,31 @@ fi # arn/id through the CFN parameter + UserData. Resolve it here before the # pack-config JSON is built so the pack side keeps a single plaintext code # path (telegram-bot-token) regardless of how the token was supplied. +# +# Region source-of-truth (Codex P1 on 79c93b6): +# The installer creates the secret in the CFN STACK region (DEPLOY_REGION), +# but bootstrap.sh's --region flag carries the BEDROCK region — which is +# pinned to us-east-1 whenever the deploy region is outside the Bedrock +# allowlist (e.g. ap-south-1). So we MUST NOT use $REGION here. +# Order of preference: +# 1) STACK_REGION exported by UserData (authoritative). +# 2) IMDSv2 placement/region (works on any EC2, exact stack region). +# 3) AWS_DEFAULT_REGION as a last resort. +# 4) REGION (Bedrock region) only if nothing else is available — same +# behavior as before, kept so single-region deployments in the +# Bedrock allowlist still work when UserData is skipped (dev/manual). if [[ -z "${KIROCREW_TG_BOT_TOKEN:-}" && -n "${KIROCREW_TG_BOT_TOKEN_SECRET:-}" ]]; then - # Instance role has secretsmanager:GetSecretValue on this specific secret; - # region auto-detected from IMDS if AWS_DEFAULT_REGION isn't already set. - _kc_tg_region="${REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" + _kc_tg_region="${STACK_REGION:-}" + if [[ -z "$_kc_tg_region" ]]; then + _kc_imds_token="$(curl -sf -X PUT http://169.254.169.254/latest/api/token \ + -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' 2>/dev/null || true)" + if [[ -n "$_kc_imds_token" ]]; then + _kc_tg_region="$(curl -sf -H "X-aws-ec2-metadata-token: ${_kc_imds_token}" \ + http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null || true)" + fi + unset _kc_imds_token + fi + _kc_tg_region="${_kc_tg_region:-${AWS_DEFAULT_REGION:-${REGION:-us-east-1}}}" _kc_tg_resolved="$(aws secretsmanager get-secret-value \ --secret-id "${KIROCREW_TG_BOT_TOKEN_SECRET}" \ --query SecretString --output text \ @@ -290,7 +311,7 @@ if [[ -z "${KIROCREW_TG_BOT_TOKEN:-}" && -n "${KIROCREW_TG_BOT_TOKEN_SECRET:-}" if [[ -n "${_kc_tg_resolved}" ]]; then KIROCREW_TG_BOT_TOKEN="${_kc_tg_resolved}" else - echo "WARN: could not resolve KirocrewTgBotTokenSecret (${KIROCREW_TG_BOT_TOKEN_SECRET}) — pack will skip Telegram wiring" >&2 + echo "WARN: could not resolve KirocrewTgBotTokenSecret (${KIROCREW_TG_BOT_TOKEN_SECRET}) in region ${_kc_tg_region} — pack will skip Telegram wiring" >&2 fi unset _kc_tg_resolved _kc_tg_region fi diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 14427c2..3179f3a 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1894,6 +1894,13 @@ Resources: # Export all params as env vars for the bootstrap script export ACCT_ID="${AWS::AccountId}" export REGION="${AWS::Region}" + # STACK_REGION is the CFN stack / deployment region — preserved + # separately from BEDROCK_REGION and from bootstrap.sh's --region + # (which is repurposed for the Bedrock region on Bedrock-model deploys). + # Used by resolvers that must hit AWS APIs in the same region where + # the installer created resources (e.g. Secrets Manager for the + # KiroCrew Telegram bot token). Codex P1 on 79c93b6. + export STACK_REGION="${AWS::Region}" export STACK_NAME="${AWS::StackName}" export DEFAULT_MODEL="${DefaultModel}" export BEDROCK_REGION="${BedrockRegion}"