From b9fcd6765e29108c393b979473c6156b241bdfd9 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Wed, 1 Apr 2026 00:29:17 +0300 Subject: [PATCH 001/172] Update project title in README.md for clarity --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eda549..bd71879 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Loki: A Stateful Prototyping/Dev/Research/Ops Agent based on openclaw in your AWS account +# Loki: A Harness for Safely Deploying Stateful Full-Stack Builder Agents based on OpenClaw, Hermes and more into your AWS Account. [![Loki Agent Demo](https://img.youtube.com/vi/dJSk8DYlHvI/maxresdefault.jpg)](https://www.youtube.com/watch?v=dJSk8DYlHvI) From d77abe7217d3a3c1822321bd5122f04995c7554b Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 21:37:40 +0000 Subject: [PATCH 002/172] fix: multi-agent support for optional + telegram bootstrap files Updated all 6 files in bootstraps/optional/ and bootstraps/telegram/: - BOOTSTRAP-GITHUBACTION-CODE-REVIEW: marked agent-agnostic - BOOTSTRAP-PIPELINE-NOTIFICATIONS: added Hermes event/webhook injection - BOOTSTRAP-WEB-UI: added Hermes Open WebUI + API server alternative - OPTIMIZE-TOO-LARGE-CONTEXT: added Hermes compression/limits config - BOOTSTRAP-TELEGRAM: added Hermes gateway setup, .env config, pairing - BOOTSTRAP-TELEGRAM-GROUP: added Hermes group config via config.yaml All files now have 'Applies to: All agents' header and agent-specific sections where config differs. --- .../BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md | 2 + .../BOOTSTRAP-PIPELINE-NOTIFICATIONS.md | 59 ++++++++++++- bootstraps/optional/BOOTSTRAP-WEB-UI.md | 66 +++++++++++++- .../optional/OPTIMIZE-TOO-LARGE-CONTEXT.md | 48 +++++++++++ .../telegram/BOOTSTRAP-TELEGRAM-GROUP.md | 76 ++++++++++++++-- bootstraps/telegram/BOOTSTRAP-TELEGRAM.md | 86 ++++++++++++++++++- 6 files changed, 320 insertions(+), 17 deletions(-) diff --git a/bootstraps/optional/BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md b/bootstraps/optional/BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md index a86b237..259568f 100644 --- a/bootstraps/optional/BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md +++ b/bootstraps/optional/BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md @@ -1,5 +1,7 @@ # BOOTSTRAP-GITHUBACTION-CODE-REVIEW.md — Automatic Code Review with Claude Code +> **Applies to:** All agents (agent-agnostic — uses GitHub Actions + Bedrock directly) + > Add this to any repo in the inceptionstack org to get automatic AI code review on PRs and commits. ## Overview diff --git a/bootstraps/optional/BOOTSTRAP-PIPELINE-NOTIFICATIONS.md b/bootstraps/optional/BOOTSTRAP-PIPELINE-NOTIFICATIONS.md index 6e93f5e..14c0361 100644 --- a/bootstraps/optional/BOOTSTRAP-PIPELINE-NOTIFICATIONS.md +++ b/bootstraps/optional/BOOTSTRAP-PIPELINE-NOTIFICATIONS.md @@ -1,4 +1,6 @@ -# BOOTSTRAP-PIPELINE-NOTIFICATIONS.md — Pipeline Notifications to Telegram + OpenClaw +# BOOTSTRAP-PIPELINE-NOTIFICATIONS.md — Pipeline Notifications to Telegram + Agent + +> **Applies to:** All agents (with agent-specific sections below) > **Run this once to wire up build notifications.** > If `memory/.bootstrapped-pipeline-notifications` exists, skip. @@ -387,7 +389,7 @@ When a system event arrives (CodePipeline path), Loki receives it in the main se - **On SUCCEEDED:** Log it. If a task was in-progress waiting for pipeline green, move it to `done` and notify the operator. - **On STARTED:** Log it silently. -GitHub webhook notifications go only to Telegram (no OpenClaw system event injection currently). +GitHub webhook notifications go only to Telegram (no agent system event injection currently). --- @@ -396,3 +398,56 @@ GitHub webhook notifications go only to Telegram (no OpenClaw system event injec ```bash mkdir -p memory && echo "Pipeline notifications bootstrapped $(date -u +%Y-%m-%dT%H:%M:%SZ)" > memory/.bootstrapped-pipeline-notifications ``` + +--- + +## OpenClaw-Specific Configuration + +The Lambda notifier injects system events into OpenClaw via SSM RunCommand: + +```bash +runuser -u ec2-user -- bash -c 'openclaw system event --text "PIPELINE_MSG" --mode now' +``` + +This delivers the event to OpenClaw's main session, where the agent can auto-react to failures. The `notifyOpenClaw` function in the Lambda code (Part 1) handles this path. + +## Hermes-Specific Configuration + +Hermes receives notifications differently. Instead of SSM RunCommand to inject system events, use the Hermes gateway API or `hermes event` CLI: + +**Option A — `hermes event` CLI via SSM:** + +Replace the `notifyOpenClaw` function in the Lambda handler with: + +```javascript +async function notifyHermes(message) { + try { + await ssm.send(new SendCommandCommand({ + InstanceIds: [INSTANCE_ID], + DocumentName: "AWS-RunShellScript", + Parameters: { + commands: [ + `runuser -u ec2-user -- bash -c 'export PATH="/home/ec2-user/.local/bin:$PATH" && hermes event "${message.replace(/"/g, '\\"')}"'` + ] + }, + TimeoutSeconds: 15 + })); + } catch (e) { + console.warn("Hermes notify failed:", e.message); + } +} +``` + +**Option B — Hermes gateway webhook:** + +If the Hermes gateway exposes a webhook endpoint, POST events directly from the Lambda without SSM: + +```javascript +await fetch('https://your-hermes-gateway/webhook', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: message }) +}); +``` + +Check Hermes docs for the webhook format: diff --git a/bootstraps/optional/BOOTSTRAP-WEB-UI.md b/bootstraps/optional/BOOTSTRAP-WEB-UI.md index adb549a..99bd5d4 100644 --- a/bootstraps/optional/BOOTSTRAP-WEB-UI.md +++ b/bootstraps/optional/BOOTSTRAP-WEB-UI.md @@ -1,10 +1,14 @@ -# BOOTSTRAP-WEB-UI.md — Expose OpenClaw Control UI via CloudFront + Cognito +# BOOTSTRAP-WEB-UI.md — Expose Agent Control UI via CloudFront + Cognito -> **Target audience:** You are an OpenClaw agent implementing this for your human. Follow every step. At the end, you MUST guide your human through manual browser steps (device pairing + gateway token). Present the full plan to the user and get approval before making any infrastructure changes. +> **Applies to:** All agents (with agent-specific sections below) + +> **Target audience:** You are an agent implementing this for your human. Follow every step. At the end, you MUST guide your human through manual browser steps. Present the full plan to the user and get approval before making any infrastructure changes. ## What This Does -Exposes the OpenClaw Gateway's built-in Control UI (Vite + Lit SPA) through a CloudFront distribution, secured by Cognito login. The gateway stays on loopback — a Node.js proxy on the same EC2 handles Cognito JWT validation and proxies HTTP + WebSocket to the gateway. +Exposes the agent's web UI through a CloudFront distribution, secured by Cognito login. The gateway stays on loopback — a Node.js proxy on the same EC2 handles Cognito JWT validation and proxies HTTP + WebSocket to the gateway. + +> **Note:** The detailed architecture below is for OpenClaw's built-in Control UI (Vite + Lit SPA). For Hermes, see the Hermes-specific section at the bottom — Hermes uses Open WebUI or its own API server endpoint instead. ## Architecture @@ -643,3 +647,59 @@ These are AWS-managed CloudFront policies (same in all accounts): - Device pairing is a third layer — each new browser must be explicitly approved by the operator. - The gateway remains on loopback (`127.0.0.1`) — never exposed to the network. - The proxy itself has no secrets hardcoded — Cognito config comes from environment variables. + +--- + +## OpenClaw-Specific Configuration + +The entire Step 1–8 walkthrough above is specific to OpenClaw's built-in Control UI (Vite + Lit SPA on port 18789). Follow the steps as documented — they cover proxy setup, ALB, CloudFront, Cognito, gateway config patches, and device pairing. + +Key OpenClaw-specific details: +- Gateway target: `http://127.0.0.1:18789` +- Config: `openclaw config patch` for `gateway.controlUi.allowedOrigins` +- Device pairing: `openclaw devices list` / `openclaw devices approve` +- Token: gateway token from `~/.openclaw/openclaw.json` + +## Hermes-Specific Configuration + +Hermes does **not** have OpenClaw's built-in Vite SPA Control UI. Instead, Hermes provides two web access options: + +### Option A: Open WebUI Integration (Recommended) + +Hermes has built-in support for [Open WebUI](https://github.com/open-webui/open-webui) as a frontend. It exposes an OpenAI-compatible API server that Open WebUI can connect to directly. + +1. Start the Hermes API server: + ```bash + hermes api # Foreground + hermes api --port 11434 # Custom port + ``` + +2. Deploy Open WebUI (Docker): + ```bash + docker run -d -p 3000:8080 \ + -e OPENAI_API_BASE_URL=http://host.docker.internal:11434/v1 \ + -e OPENAI_API_KEY=not-needed \ + --name open-webui \ + ghcr.io/open-webui/open-webui:main + ``` + +3. To expose Open WebUI via CloudFront + Cognito, use the same ALB + CloudFront + proxy pattern from Steps 2-8 above, but target port `3000` (Open WebUI) instead of port `18789`. + +See Hermes docs: + +### Option B: Direct API Server + Custom Frontend + +Hermes exposes an OpenAI-compatible `/v1/chat/completions` endpoint. You can build or use any OpenAI-compatible web UI: + +```bash +hermes api --host 0.0.0.0 --port 11434 +``` + +To secure this behind CloudFront + Cognito, use the same proxy pattern from Steps 2-8 above, targeting port `11434` as the backend. The Cognito JWT proxy code works identically — just change `GATEWAY_TARGET` from `http://127.0.0.1:18789` to `http://127.0.0.1:11434`. + +### Shared Infrastructure + +The ALB, CloudFront, Cognito, and security group setup (Steps 3-5) is identical regardless of which agent or backend you're proxying. The only differences are: +- **Target port** in the proxy's `GATEWAY_TARGET` environment variable +- **Health check path** in the ALB target group (may differ per backend) +- **No device pairing** — Hermes doesn't use OpenClaw's device pairing; Cognito JWT is the sole auth layer diff --git a/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md b/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md index 1821ca0..8b4d7e1 100644 --- a/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md +++ b/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md @@ -1,5 +1,7 @@ # OPTIMIZE-TOO-LARGE-CONTEXT.md — Context Window Optimization +> **Applies to:** All agents (with agent-specific sections below) + > **Run this if your system prompt exceeds ~5,000 tokens for workspace files, or if you're hitting context limits too quickly.** ## ⚠️ HARD RULE — NEVER DELETE CORE FILES @@ -46,3 +48,49 @@ If `memory/.bootstrapped-security` or `memory/.bootstrapped-skills` exists, you' ## 4. Verify After making changes, check your total system prompt token count. Target: under 5,000 tokens for workspace files (down from ~9,000). Report before/after counts. + +--- + +## OpenClaw-Specific Configuration + +OpenClaw loads workspace files (`SOUL.md`, `USER.md`, `AGENTS.md`, `IDENTITY.md`, `MEMORY.md`, `TOOLS.md`, `HEARTBEAT.md`) into the system prompt as "Project Context" on every message. To check current token usage: + +```bash +openclaw status +``` + +The status output shows system prompt token count. Skills are loaded on-demand (not in the system prompt) — moving content from workspace files to skills reduces per-message cost. + +OpenClaw config options for context management: +- `agents.defaults.memorySearch.enabled: true` — offloads knowledge to searchable memory instead of the system prompt +- Skills in `~/.openclaw/workspace/skills/` — loaded only when relevant, not every turn + +## Hermes-Specific Configuration + +Hermes manages context differently: +- **MEMORY.md** is capped at ~2,200 chars and **USER.md** at ~1,375 chars (configurable in `config.yaml`) +- **Context files** (`.hermes.md`, `AGENTS.md`, etc.) are loaded per-directory based on the CWD +- Hermes has built-in **context compression** that activates automatically when context exceeds a threshold + +To adjust Hermes context limits: + +```yaml +# In ~/.hermes/config.yaml +memory: + memory_char_limit: 2200 # MEMORY.md max chars + user_char_limit: 1375 # USER.md max chars + +compression: + enabled: true + threshold: 0.50 # Compress when context exceeds 50% of model window + summary_model: "your-model" +``` + +To check current context usage in a Hermes session: +``` +/usage # Shows token counts +/compress # Manually compress context +/insights # Usage analytics +``` + +Hermes skills are also loaded on-demand. Moving reference material from workspace files into skills (at `~/.hermes/skills/`) has the same benefit — reduces per-message context overhead. diff --git a/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md b/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md index 6f50c0e..149932f 100644 --- a/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md +++ b/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md @@ -1,5 +1,7 @@ # BOOTSTRAP-TELEGRAM-GROUP.md — Telegram Group Hub Setup +> **Applies to:** All agents (with agent-specific sections below) + > **Purpose:** Set up a private Telegram group where the human owner can broadcast messages to multiple Loki instances across separate AWS accounts and receive replies from all of them in one place. > > **Prerequisites:** Complete `BOOTSTRAP-TELEGRAM.md` first. Your bot must be working in DMs (sending and receiving messages) before setting up a group. Do not attempt this bootstrap until regular Telegram integration is confirmed working. @@ -111,10 +113,14 @@ The negative number (e.g. `-1001234567890`) is your **group chat ID**. Save it --- -## Part 3: Configure OpenClaw +## Part 3: Configure the Agent for Group Chat > **Required for both new and existing groups.** +Configuration differs by agent type. Follow the section for your agent below. + +### OpenClaw Configuration + Replace `GROUP_CHAT_ID` with the group chat ID (negative number), and `OWNER_USER_ID` with the owner's Telegram numeric user ID (already in `channels.telegram.allowFrom`). ```bash @@ -158,6 +164,55 @@ EOF OpenClaw restarts automatically after the config change. +### Hermes Configuration + +Replace `GROUP_CHAT_ID` with the group chat ID (negative number), and `OWNER_USER_ID` with the owner's Telegram numeric user ID. + +**Step 1 — Set allowed users in `.env`:** + +```bash +# In ~/.hermes/.env — ensure the owner's ID is in the allowed users +TELEGRAM_ALLOWED_USERS=OWNER_USER_ID +``` + +**Step 2 — Configure group behavior in `config.yaml`:** + +```yaml +# In ~/.hermes/config.yaml +telegram: + require_mention: true # Only respond when @mentioned or replied to + mention_patterns: + - "@fleet" # Broadcast trigger — all bots respond + - "@all" # Broadcast trigger — all bots respond +``` + +**Step 3 — Set home channel (optional):** + +Send `/sethome` in the group chat, or set it manually: + +```bash +# In ~/.hermes/.env +TELEGRAM_HOME_CHANNEL=GROUP_CHAT_ID +TELEGRAM_HOME_CHANNEL_NAME="LokiFleet" +``` + +**Step 4 — Restart the gateway:** + +```bash +hermes gateway stop && hermes gateway start +``` + +**How to talk in the group (same as OpenClaw):** +- **`@fleet check GuardDuty`** — all bots respond (broadcast via mention_patterns) +- **Reply to a specific bot's message** — only that bot responds +- **`@bot_username do X`** — only that specific bot responds + +**Hermes group security:** +- `TELEGRAM_ALLOWED_USERS` controls who can trigger the bot (same as DMs) +- `require_mention: true` prevents the bot from responding to every message +- Bot-to-bot messages are ignored because bot user IDs aren't in `TELEGRAM_ALLOWED_USERS` +- Alternatively, promote the bot to group admin instead of disabling privacy mode — admin bots see all messages regardless of privacy setting + --- ## Part 4: Verify @@ -175,23 +230,28 @@ If the bot doesn't respond: ## Why This Is Safe With Multiple Bots -- Each bot only processes messages from `allowFrom` user IDs (the owner) -- Bot-to-bot messages are ignored because bot user IDs are not in `allowFrom` +- Each bot only processes messages from authorized user IDs (the owner) + - **OpenClaw:** `allowFrom` in the group config + - **Hermes:** `TELEGRAM_ALLOWED_USERS` in `.env` +- Bot-to-bot messages are ignored because bot user IDs are not in the allowlist - No infinite reply loops possible - No cross-account networking, IAM, or VPC peering required - The "mesh" is Telegram's infrastructure — encrypted, private group, invite-only +- Mixed fleets work fine — OpenClaw and Hermes bots can coexist in the same group --- ## Security Checklist - [ ] Group is private (no public username/link) -- [ ] `groupPolicy: "allowlist"` (not `"open"`) -- [ ] `allowFrom` contains only the owner's numeric user ID -- [ ] `groupAllowFrom` contains only the owner's numeric user ID -- [ ] Each bot has privacy mode disabled in BotFather (required for function) +- [ ] **OpenClaw:** `groupPolicy: "allowlist"` (not `"open"`) +- [ ] **OpenClaw:** `allowFrom` contains only the owner's numeric user ID +- [ ] **OpenClaw:** `groupAllowFrom` contains only the owner's numeric user ID +- [ ] **Hermes:** `TELEGRAM_ALLOWED_USERS` contains only the owner's numeric user ID +- [ ] **Hermes:** `require_mention: true` in `config.yaml` +- [ ] Each bot has privacy mode disabled in BotFather (required for function) — or promoted to admin - [ ] Optional: `/setjoingroups` disabled in BotFather after setup (prevents unauthorized group adds) -- [ ] No `"*"` wildcards in any `allowFrom` or `groupAllowFrom` +- [ ] No `"*"` wildcards in any allowlist - [ ] Group invite link not shared publicly (revoke after adding bots if needed) --- diff --git a/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md b/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md index 916bedf..01a19a5 100644 --- a/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md +++ b/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md @@ -1,5 +1,7 @@ # BOOTSTRAP-TELEGRAM.md — Telegram Setup + Communication Rules +> **Applies to:** All agents (with agent-specific sections below) + > **Part 1** (setup) runs once. **Part 2** (formatting rules) applies permanently to every message. > If `memory/.bootstrapped-telegram` exists, Part 1 is done — skip to Part 2 to refresh the rules. @@ -40,7 +42,11 @@ curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates" \ Note your **numeric chat ID** (e.g. `123456789`). -### Step 3: Configure OpenClaw +### Step 3: Configure the Agent + +Configuration differs by agent type. Follow the section for your agent below. + +#### OpenClaw Configuration Add the Telegram channel to OpenClaw config. Ask Loki to run: @@ -84,12 +90,63 @@ EOF OpenClaw restarts automatically after the config change. +#### Hermes Configuration + +Hermes configures Telegram via environment variables and `config.yaml`: + +**Option A — Interactive setup (recommended):** + +```bash +hermes gateway setup +# Select "Telegram" when prompted +# Enter your bot token and allowed user IDs +``` + +**Option B — Manual configuration:** + +Add to `~/.hermes/.env`: + +```bash +TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE +TELEGRAM_ALLOWED_USERS=YOUR_CHAT_ID +``` + +To fetch the token from Secrets Manager: + +```bash +BOT_TOKEN=$(aws secretsmanager get-secret-value \ + --secret-id /faststart/telegram-bot-token \ + --query SecretString --output text --region us-east-1) + +echo "TELEGRAM_BOT_TOKEN=${BOT_TOKEN}" >> ~/.hermes/.env +echo "TELEGRAM_ALLOWED_USERS=YOUR_CHAT_ID" >> ~/.hermes/.env +``` + +Then start the gateway: + +```bash +hermes gateway # Foreground (testing) +hermes gateway install && hermes gateway start # Systemd service (production) +``` + +**Optional Hermes Telegram config** in `~/.hermes/config.yaml`: + +```yaml +telegram: + require_mention: false # true = only respond when @mentioned in groups + mention_patterns: + - "^\\s*loki\\b" # Custom wake word +``` + +Hermes supports Telegram voice messages (auto-transcription), images, file attachments, and streaming responses out of the box. + ### Step 4: Verify -Send your bot a message. You should get a response from Loki within a few seconds. +Send your bot a message. You should get a response from the agent within a few seconds. + +**OpenClaw test:** ```bash -# Test the bot directly BOT_TOKEN=$(aws secretsmanager get-secret-value \ --secret-id /faststart/telegram-bot-token \ --query SecretString --output text --region us-east-1) @@ -100,15 +157,34 @@ curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \ -d parse_mode="HTML" ``` +**Hermes test:** + +```bash +hermes gateway status # Should show "running" +# Then send a message to your bot in Telegram +``` + ### Step 5: Security — allowlist only -The config above uses `dmPolicy: allowlist` — Loki only responds to chat IDs in `allowFrom`. Never set `dmPolicy: all` on a production instance. Anyone who finds your bot could interact with your agent. +Both agents use allowlists by default — the agent only responds to explicitly authorized user IDs. + +**OpenClaw:** `dmPolicy: allowlist` + `allowFrom` in `openclaw.json`. Never set `dmPolicy: all` on a production instance. To add more users: ```bash openclaw config patch '{"channels":{"telegram":{"allowFrom":["CHAT_ID_1","CHAT_ID_2"]}}}' ``` +**Hermes:** `TELEGRAM_ALLOWED_USERS` in `~/.hermes/.env` (comma-separated). Alternatively, Hermes supports DM pairing — unknown users get a one-time pairing code: + +```bash +hermes pairing approve telegram PAIRING_CODE # Approve a user +hermes pairing list # View pending + approved +hermes pairing revoke telegram USER_ID # Remove access +``` + +Never set `GATEWAY_ALLOW_ALL_USERS=true` on a production instance. + --- ## Part 2: Formatting Rules (Permanent) @@ -190,6 +266,8 @@ Reactions are available but use them sparingly — at most 1 per 5–10 exchange - Use inline buttons for destructive operation confirmations. ``` +> **Note:** These formatting rules apply regardless of agent type — Telegram renders markdown the same way for both OpenClaw and Hermes. + --- ## Finish From 1d3e33b9ee4112b9731ba27721dd9a243160e81c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 21:38:00 +0000 Subject: [PATCH 003/172] ci: bump version to 0.5.6 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 18feb9c..4c4089d 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.5" +INSTALLER_VERSION="0.5.6" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 12151fc3a2f45247fcbd38bb15644edf4d3ee2a3 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 21:39:03 +0000 Subject: [PATCH 004/172] =?UTF-8?q?fix:=20context=20optimization=20is=20Op?= =?UTF-8?q?enClaw-only=20=E2=80=94=20Hermes=20has=20built-in=20compression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../optional/OPTIMIZE-TOO-LARGE-CONTEXT.md | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md b/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md index 8b4d7e1..03b44c1 100644 --- a/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md +++ b/bootstraps/optional/OPTIMIZE-TOO-LARGE-CONTEXT.md @@ -1,6 +1,6 @@ # OPTIMIZE-TOO-LARGE-CONTEXT.md — Context Window Optimization -> **Applies to:** All agents (with agent-specific sections below) +> **Applies to:** OpenClaw only — Hermes has built-in context compression and does not need manual optimization. > **Run this if your system prompt exceeds ~5,000 tokens for workspace files, or if you're hitting context limits too quickly.** @@ -51,7 +51,7 @@ After making changes, check your total system prompt token count. Target: under --- -## OpenClaw-Specific Configuration +## OpenClaw-Specific Notes OpenClaw loads workspace files (`SOUL.md`, `USER.md`, `AGENTS.md`, `IDENTITY.md`, `MEMORY.md`, `TOOLS.md`, `HEARTBEAT.md`) into the system prompt as "Project Context" on every message. To check current token usage: @@ -65,32 +65,12 @@ OpenClaw config options for context management: - `agents.defaults.memorySearch.enabled: true` — offloads knowledge to searchable memory instead of the system prompt - Skills in `~/.openclaw/workspace/skills/` — loaded only when relevant, not every turn -## Hermes-Specific Configuration +## Why This Doesn't Apply to Hermes -Hermes manages context differently: -- **MEMORY.md** is capped at ~2,200 chars and **USER.md** at ~1,375 chars (configurable in `config.yaml`) -- **Context files** (`.hermes.md`, `AGENTS.md`, etc.) are loaded per-directory based on the CWD -- Hermes has built-in **context compression** that activates automatically when context exceeds a threshold +Hermes has built-in automatic context management: +- **MEMORY.md** is capped at ~2,200 chars and **USER.md** at ~1,375 chars +- **Context compression** activates automatically when context exceeds a configurable threshold +- `/compress` manually triggers compression; `/usage` shows token counts +- Skills are loaded progressively on-demand -To adjust Hermes context limits: - -```yaml -# In ~/.hermes/config.yaml -memory: - memory_char_limit: 2200 # MEMORY.md max chars - user_char_limit: 1375 # USER.md max chars - -compression: - enabled: true - threshold: 0.50 # Compress when context exceeds 50% of model window - summary_model: "your-model" -``` - -To check current context usage in a Hermes session: -``` -/usage # Shows token counts -/compress # Manually compress context -/insights # Usage analytics -``` - -Hermes skills are also loaded on-demand. Moving reference material from workspace files into skills (at `~/.hermes/skills/`) has the same benefit — reduces per-message context overhead. +No manual optimization is needed — skip this bootstrap entirely for Hermes agents. From 3ad72a18ce88b55e327f1281034dc7c8566947f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 21:39:26 +0000 Subject: [PATCH 005/172] ci: bump version to 0.5.7 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 4c4089d..829265c 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.6" +INSTALLER_VERSION="0.5.7" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 5f4b447ec7bbbaf4929a7d396973775968292624 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 21:45:43 +0000 Subject: [PATCH 006/172] =?UTF-8?q?docs:=20update=20README=20=E2=80=94=20a?= =?UTF-8?q?dd=20repo=20table,=20Hermes=20link,=20replace=20embedrock=20wit?= =?UTF-8?q?h=20bedrockify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd71879..19ecafb 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,15 @@ Loki is: Loki is fully open source. The deployment templates, brain files, skills, and bootstrap scripts are all available at [github.com/inceptionstack/loki-agent](https://github.com/inceptionstack/loki-agent). -Built on [OpenClaw](https://github.com/openclaw/openclaw) — the engine that powers the agent runtime, tool execution, and memory system. +Built on [OpenClaw](https://github.com/openclaw/openclaw) and [Hermes](https://github.com/NousResearch/hermes-agent) — choose your agent runtime at deploy time. + +### InceptionStack Repositories + +| Repo | Description | +|------|-------------| +| **[loki-agent](https://github.com/inceptionstack/loki-agent)** | Deploy templates (CloudFormation, SAM, Terraform), pack system, bootstrap scripts, brain files | +| **[loki-skills](https://github.com/inceptionstack/loki-skills)** | Agent skills library — AWS infrastructure, observability, payments, and more (OpenClaw + Hermes) | +| **[bedrockify](https://github.com/inceptionstack/bedrockify)** | OpenAI-compatible proxy for Amazon Bedrock — chat completions + embeddings in one binary | +| **[ai-patterns](https://github.com/inceptionstack/ai-patterns)** | AI Agent Architecture Patterns — definitions, naming, and design considerations | Contributions, issues, and feedback welcome. From 2b25946451862c3ab82bb13196ed92a955c4e3f1 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 21:50:08 +0000 Subject: [PATCH 007/172] docs: add backfill step + memory quality guidance to semantic search bootstrap Incorporates the improvements from PR #3 by @gilinachum, applied to the current multi-agent file structure (bootstraps/essential/, bedrockify on port 8090, OpenClaw/Hermes sections). - Step 5: backfill existing memory with 'openclaw memory index --force' - Memory quality section: heartbeat noise degrades vector search - Rule: only write what changed (decisions, bugs, alerts) - Exclude pattern config for heartbeat files Co-authored-by: Gili Nachum --- .../essential/BOOTSTRAP-MEMORY-SEARCH.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md b/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md index f54e56d..f5def39 100644 --- a/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md +++ b/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md @@ -95,6 +95,73 @@ Then restart the OpenClaw gateway. Ask the agent to run `memory_search` with any query. It should return ranked results from workspace memory files using hybrid search (70% vector, 30% text). +### Step 5: Backfill Existing Memory + +After enabling semantic search for the first time, existing memory files are **not** automatically indexed. Run: + +```bash +openclaw memory index --force +``` + +This vectorizes all current memory files. Without this step, `memory_search` will only find content written *after* the setup — silently missing everything prior. + +### Memory Quality Matters + +Vector search ranks results by cosine similarity. **Low-signal, repetitive content tanks the scores of everything nearby** — making recall unreliable even for genuinely useful memories. + +#### What hurts search quality + +High-frequency repetitive content compresses into a dense cluster in vector space. This raises the effective similarity floor and pushes useful content below the retrieval threshold. + +The biggest offender is **heartbeat logs**: + +```markdown +## Heartbeat 02:19 UTC +### Apps: ✅ frontend + api healthy +### Security Hub: no change — 1 CRITICAL, 94 HIGH +## Heartbeat 02:49 UTC +### Apps: ✅ frontend + api healthy +### Security Hub: no change — 1 CRITICAL, 94 HIGH +``` + +A single daily memory file can contain 40–50 of these. They're semantically near-identical, contribute nothing to recall, and dilute chunk quality across the entire index. + +#### Rule: only write what changed + +**Don't write:** +- "no change", "all healthy", "nothing to report" +- Repeated status confirmations +- Routine cron completions with no notable outcome + +**Do write:** +- App went down or returned unexpected status +- Security finding count changed (new CVE, severity shift) +- A decision was made +- A bug was found or fixed +- A TODO was started or completed autonomously + +#### Keep heartbeat files out of the index + +If your agent writes verbose heartbeat logs that are useful for audit but not for recall, route them to a separate file pattern and exclude from indexing: + +```bash +# Write heartbeat noise here (not indexed) +memory/heartbeat-YYYY-MM-DD.md + +# Keep daily notes clean (indexed) +memory/YYYY-MM-DD.md +``` + +To exclude a pattern from indexing, configure the memory sources in `openclaw.json`: + +```json +"memorySearch": { + "sources": { + "exclude": ["memory/heartbeat-*.md"] + } +} +``` + ## Hermes-Specific Configuration Hermes has its own built-in memory system: From 791696ba7ed9a55700a021c1b378bf55cea6994e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 21:50:33 +0000 Subject: [PATCH 008/172] ci: bump version to 0.5.8 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 829265c..383f7a9 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.7" +INSTALLER_VERSION="0.5.8" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From aa3a78e020812397195d8fce057183fc488c4d48 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:10:23 +0000 Subject: [PATCH 009/172] feat: add Pi Coding Agent pack (experimental) --- packs/pi/install.sh | 138 ++++++++++++++++++++++++++++ packs/pi/manifest.yaml | 36 ++++++++ packs/pi/resources/shell-profile.sh | 3 + 3 files changed, 177 insertions(+) create mode 100755 packs/pi/install.sh create mode 100644 packs/pi/manifest.yaml create mode 100644 packs/pi/resources/shell-profile.sh diff --git a/packs/pi/install.sh b/packs/pi/install.sh new file mode 100755 index 0000000..4405a84 --- /dev/null +++ b/packs/pi/install.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# packs/pi/install.sh — Install Pi Coding Agent and configure it to use bedrockify +# +# Usage: +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] +# +# Assumes: +# - bedrockify is already installed and running (see packs/bedrockify/) +# - npm/node available +# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) +# +# Idempotent: safe to re-run. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +# Defaults from config file (written by bootstrap dispatcher), then CLI overrides +# Note: reads "model" key directly — Pi accepts any OpenAI-style model ID string. +# The generic --model from the dispatcher carries Bedrock model IDs; we pass them +# through to bedrockify which handles the translation. +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" +PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat <&1)" || true +if ! printf '%s' "${HEALTH}" | grep -q '"status":"ok"'; then + fail "bedrockify is not running on port ${BEDROCKIFY_PORT}. Install bedrockify pack first." +fi +ok "bedrockify is healthy on port ${BEDROCKIFY_PORT}" + +# ── Install Pi ──────────────────────────────────────────────────────────────── +step "Installing Pi Coding Agent" + +if command -v pi &>/dev/null; then + PI_EXISTING="$(pi --version 2>/dev/null || echo unknown)" + log "pi already installed (${PI_EXISTING}) — reinstalling" +fi + +npm install -g @mariozechner/pi-coding-agent + +# Add npm global bin to PATH for current session +NPM_GLOBAL_BIN="$(npm root -g 2>/dev/null | sed 's|/lib/node_modules||')"/bin +export PATH="${NPM_GLOBAL_BIN}:${HOME}/.local/bin:$PATH" + +if ! command -v pi &>/dev/null; then + fail "pi command not found after install. Check PATH or install output." +fi + +PI_VERSION="$(pi --version 2>/dev/null || echo unknown)" +ok "Pi installed: ${PI_VERSION}" + +# ── Configure Pi ───────────────────────────────────────────────────────────── +step "Configuring Pi" + +mkdir -p "${HOME}/.pi/agent" + +# Write models.json with bedrockify provider config +cat > "${HOME}/.pi/agent/models.json" < Date: Tue, 31 Mar 2026 22:10:58 +0000 Subject: [PATCH 010/172] feat: dynamic pack discovery in installer + registry entries for pi and ironclaw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Installer reads packs/registry.yaml to build the selection menu dynamically - No more hardcoded pack list — new packs auto-appear by adding to registry - Experimental packs show (experimental) tag in yellow - Instance size defaults read from registry per pack - Deploy templates (CFN, SAM, Terraform) updated to accept pi and ironclaw - Registry entries added for pi (experimental) and ironclaw (experimental) --- deploy/cloudformation/template.yaml | 4 +- deploy/sam/template.yaml | 4 +- deploy/terraform/variables.tf | 6 +-- install.sh | 65 +++++++++++++++++++++++------ packs/registry.yaml | 30 +++++++++++++ 5 files changed, 91 insertions(+), 18 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 5960d35..2c7b636 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -108,7 +108,9 @@ Parameters: AllowedValues: - openclaw - hermes - Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter, t4g.medium sufficient)." + - pi + - ironclaw + Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." EnvironmentName: Type: String diff --git a/deploy/sam/template.yaml b/deploy/sam/template.yaml index cf43893..3a6bd0c 100644 --- a/deploy/sam/template.yaml +++ b/deploy/sam/template.yaml @@ -109,7 +109,9 @@ Parameters: AllowedValues: - openclaw - hermes - Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter, t4g.medium sufficient)." + - pi + - ironclaw + Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." EnvironmentName: Type: String diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 7dc93f9..6b6f2e2 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -5,12 +5,12 @@ variable "aws_region" { } variable "pack_name" { - description = "Agent pack to deploy (openclaw or hermes)" + description = "Agent pack to deploy (openclaw, hermes, pi, or ironclaw)" type = string default = "openclaw" validation { - condition = contains(["openclaw", "hermes"], var.pack_name) - error_message = "pack_name must be openclaw or hermes." + condition = contains(["openclaw", "hermes", "pi", "ironclaw"], var.pack_name) + error_message = "pack_name must be openclaw, hermes, pi, or ironclaw." } } diff --git a/install.sh b/install.sh index 383f7a9..e0aa1ac 100755 --- a/install.sh +++ b/install.sh @@ -379,17 +379,48 @@ collect_config() { info "Configuration" echo "" - # ---- Pack selection (first question) -------------------------------------- + # ---- Pack selection (dynamically discovered from registry.yaml) ----------- + local registry="${CLONE_DIR}/packs/registry.yaml" + local -a pack_names=() + local -a pack_descs=() + local -a pack_experimental=() + + # Parse agent packs from registry.yaml (requires python3, already validated) + while IFS='|' read -r pname pdesc pexp; do + pack_names+=("$pname") + pack_descs+=("$pdesc") + pack_experimental+=("$pexp") + done < <(python3 -c " +import yaml, sys +with open('$registry') as f: + reg = yaml.safe_load(f) +for name, cfg in reg.get('packs', {}).items(): + if cfg.get('type') == 'agent': + exp = 'true' if cfg.get('experimental', False) else 'false' + print(f\"{name}|{cfg.get('description', name)}|{exp}\") +" 2>/dev/null || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") + echo " Agent to deploy:" - echo " 1) OpenClaw -- stateful AI agent with 24/7 gateway (recommended)" - echo " 2) Hermes -- NousResearch CLI agent (lighter)" + local i + for i in "${!pack_names[@]}"; do + local num=$((i + 1)) + local tag="" + [[ "${pack_experimental[$i]}" == "true" ]] && tag=" ${YELLOW}(experimental)${NC}" + local rec="" + [[ "${pack_names[$i]}" == "openclaw" ]] && rec=" ${GREEN}(recommended)${NC}" + echo -e " ${num}) ${BOLD}${pack_names[$i]}${NC} -- ${pack_descs[$i]}${rec}${tag}" + done echo "" local pack_choice prompt "Deploy which agent" pack_choice "1" - case "$pack_choice" in - 2) PACK_NAME="hermes" ;; - *) PACK_NAME="openclaw" ;; - esac + local idx=$(( pack_choice - 1 )) + if [[ $idx -lt 0 || $idx -ge ${#pack_names[@]} ]]; then + idx=0 # default to first (openclaw) + fi + PACK_NAME="${pack_names[$idx]}" + if [[ "${pack_experimental[$idx]}" == "true" ]]; then + warn "${PACK_NAME} is experimental — expect rough edges" + fi ok "Selected pack: ${PACK_NAME}" # Count existing deployments to generate a smart default env name @@ -405,12 +436,20 @@ collect_config() { ENV_NAME=$(echo "$ENV_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-') prompt "Loki watermark (tag to identify this deployment)" LOKI_WATERMARK "$ENV_NAME" - # Adjust instance size default based on pack - local default_size_choice="3" # openclaw → t4g.xlarge - if [[ "$PACK_NAME" == "hermes" ]]; then - default_size_choice="1" # hermes → t4g.medium - info "Hermes is lightweight — defaulting to t4g.medium" - fi + # Adjust instance size default based on pack registry + local default_size_choice="3" # default → t4g.xlarge + local pack_instance_type + pack_instance_type=$(python3 -c " +import yaml +with open('$registry') as f: + reg = yaml.safe_load(f) +print(reg.get('packs', {}).get('$PACK_NAME', {}).get('instance_type', 't4g.xlarge')) +" 2>/dev/null || echo "t4g.xlarge") + case "$pack_instance_type" in + t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; + t4g.large) default_size_choice="2" ;; + *) default_size_choice="3" ;; + esac echo "" echo " Instance sizes:" echo " 1) t4g.medium -- 2 vCPU, 4GB (~\$25/mo) light use" diff --git a/packs/registry.yaml b/packs/registry.yaml index 4bdad86..968658d 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -28,6 +28,7 @@ packs: gateway: 3001 brain: true claude_code: true + experimental: false hermes: type: agent @@ -41,3 +42,32 @@ packs: ports: {} brain: false claude_code: false + experimental: false + + pi: + type: agent + description: "Pi — minimal terminal coding harness (experimental)" + deps: + - bedrockify + instance_type: t4g.medium + root_volume_gb: 40 + data_volume_gb: 0 + default_model: "us.anthropic.claude-sonnet-4-6-v1" + ports: {} + brain: false + claude_code: false + experimental: true + + ironclaw: + type: agent + description: "IronClaw — Rust-based AI agent by NEAR AI (experimental)" + deps: + - bedrockify + instance_type: t4g.medium + root_volume_gb: 40 + data_volume_gb: 0 + default_model: "us.anthropic.claude-sonnet-4-6-v1" + ports: {} + brain: false + claude_code: false + experimental: true From b91b3cc93494b9989febf08ecbfb090de0765055 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:11:12 +0000 Subject: [PATCH 011/172] feat: add IronClaw pack (experimental) --- packs/ironclaw/install.sh | 176 ++++++++++++++++++++++ packs/ironclaw/manifest.yaml | 36 +++++ packs/ironclaw/resources/shell-profile.sh | 3 + 3 files changed, 215 insertions(+) create mode 100755 packs/ironclaw/install.sh create mode 100644 packs/ironclaw/manifest.yaml create mode 100644 packs/ironclaw/resources/shell-profile.sh diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh new file mode 100755 index 0000000..7aee789 --- /dev/null +++ b/packs/ironclaw/install.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# packs/ironclaw/install.sh — Install IronClaw (NEAR AI Rust agent) and configure it to use bedrockify +# +# Usage: +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] +# +# Assumes: +# - bedrockify is already installed and running (see packs/bedrockify/) +# - curl available +# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) +# +# Idempotent: safe to re-run. +# +# Notes: +# - IronClaw is a single static Rust binary (musl build) — no Rust/Cargo needed. +# - We bypass the `ironclaw onboard` OAuth wizard by writing .env directly with +# LLM_BACKEND=openai_compatible pointing at bedrockify. +# - Known issue: IronClaw may attempt to use the Linux secret-service (dbus) for +# keychain operations on some code paths. On headless EC2 this can error. +# If IronClaw adds an IRONCLAW_NO_KEYCHAIN or similar env var in a future release, +# add it to ~/.ironclaw/.env to suppress the keychain lookup. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +# Defaults from config file (written by bootstrap dispatcher), then CLI overrides +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" +PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat <&1)" || true +if ! printf '%s' "${HEALTH}" | grep -q '"status":"ok"'; then + fail "bedrockify is not running on port ${BEDROCKIFY_PORT}. Install bedrockify pack first." +fi +ok "bedrockify is healthy on port ${BEDROCKIFY_PORT}" + +# ── Install IronClaw binary ─────────────────────────────────────────────────── +step "Installing IronClaw binary" + +# Detect architecture +ARCH=$(uname -m) +case "$ARCH" in + aarch64|arm64) RELEASE_ARCH="aarch64-unknown-linux-musl" ;; + x86_64) RELEASE_ARCH="x86_64-unknown-linux-musl" ;; + *) fail "Unsupported architecture: $ARCH" ;; +esac +log "Architecture: ${ARCH} → ${RELEASE_ARCH}" + +if command -v ironclaw &>/dev/null; then + IRONCLAW_EXISTING="$(ironclaw --version 2>/dev/null || echo unknown)" + log "ironclaw already installed (${IRONCLAW_EXISTING}) — reinstalling" +fi + +# Ensure local bin dir exists +mkdir -p "${HOME}/.local/bin" + +# Download and extract to /tmp, then find and install the binary +RELEASE_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-${RELEASE_ARCH}.tar.gz" +log "Downloading from: ${RELEASE_URL}" + +EXTRACT_DIR="$(mktemp -d /tmp/ironclaw-extract-XXXXXX)" +trap 'rm -rf "${EXTRACT_DIR}"' EXIT + +curl -fsSL "${RELEASE_URL}" | tar xz -C "${EXTRACT_DIR}" + +# Find the ironclaw binary (may be at root or in a subdirectory) +IRONCLAW_BIN="$(find "${EXTRACT_DIR}" -type f -name "ironclaw" | head -1)" +if [[ -z "${IRONCLAW_BIN}" ]]; then + # Some releases use the full target triple as the binary name + IRONCLAW_BIN="$(find "${EXTRACT_DIR}" -type f \( -name "ironclaw*" \) | grep -v '\.tar\|\.gz\|\.md\|\.txt' | head -1)" +fi +if [[ -z "${IRONCLAW_BIN}" ]]; then + fail "Could not locate ironclaw binary in extracted archive. Contents: $(find "${EXTRACT_DIR}" | head -20)" +fi +log "Found binary: ${IRONCLAW_BIN}" + +install -m 755 "${IRONCLAW_BIN}" "${HOME}/.local/bin/ironclaw" + +# Add local bin to PATH for current session +export PATH="${HOME}/.local/bin:$PATH" + +if ! command -v ironclaw &>/dev/null; then + fail "ironclaw command not found after install. Check PATH or install output." +fi + +IRONCLAW_VERSION="$(ironclaw --version 2>/dev/null || echo unknown)" +ok "IronClaw installed: ${IRONCLAW_VERSION}" + +# ── Configure IronClaw ──────────────────────────────────────────────────────── +step "Configuring IronClaw" + +mkdir -p "${HOME}/.ironclaw" + +# Write .env to configure bedrockify as the OpenAI-compatible backend. +# This bypasses the `ironclaw onboard` OAuth wizard entirely. +cat > "${HOME}/.ironclaw/.env" < Date: Tue, 31 Mar 2026 22:13:28 +0000 Subject: [PATCH 012/172] feat: add IronClaw pack (experimental) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single static Rust binary from NEAR AI. Uses openai_compatible backend pointed at bedrockify — bypasses NEAR AI OAuth entirely. Pre-built arm64 (musl) binary from GitHub releases. Known issue: dbus/keyring on headless EC2 — mitigated with IRONCLAW_DISABLE_KEYRING=1 in .env. --- packs/ironclaw/install.sh | 168 ++++++++++++++++++++++ packs/ironclaw/manifest.yaml | 36 +++++ packs/ironclaw/resources/shell-profile.sh | 3 + 3 files changed, 207 insertions(+) create mode 100755 packs/ironclaw/install.sh create mode 100644 packs/ironclaw/manifest.yaml create mode 100644 packs/ironclaw/resources/shell-profile.sh diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh new file mode 100755 index 0000000..a1da26c --- /dev/null +++ b/packs/ironclaw/install.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# packs/ironclaw/install.sh — Install IronClaw and configure it to use bedrockify +# +# Usage: +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] +# +# Assumes: +# - bedrockify is already installed and running (see packs/bedrockify/) +# - curl available +# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) +# +# IronClaw is a single static Rust binary — no Rust/Cargo needed at runtime. +# We download the pre-built musl binary from GitHub releases. +# +# Note: IronClaw has an `ironclaw onboard` wizard that tries browser-based +# NEAR AI OAuth. We bypass this entirely by writing .env directly with +# LLM_BACKEND=openai_compatible, pointing at bedrockify. +# +# Known issue: IronClaw may attempt dbus/secret-service for keychain access +# on Linux. On headless EC2, this may fail silently. We set +# IRONCLAW_DISABLE_KEYRING=1 as a workaround. +# +# Idempotent: safe to re-run. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" +PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat <&1)" || true +if ! printf '%s' "${HEALTH}" | grep -q '"status":"ok"'; then + fail "bedrockify is not running on port ${BEDROCKIFY_PORT}. Install bedrockify pack first." +fi +ok "bedrockify is healthy on port ${BEDROCKIFY_PORT}" + +# ── Install IronClaw ────────────────────────────────────────────────────────── +step "Installing IronClaw" + +if command -v ironclaw &>/dev/null; then + IC_EXISTING="$(ironclaw --version 2>/dev/null || echo unknown)" + log "ironclaw already installed (${IC_EXISTING}) — reinstalling" +fi + +# Detect architecture and pick the right release binary +ARCH="$(uname -m)" +case "${ARCH}" in + aarch64|arm64) RELEASE_ARCH="aarch64-unknown-linux-musl" ;; + x86_64) RELEASE_ARCH="x86_64-unknown-linux-musl" ;; + *) fail "Unsupported architecture: ${ARCH}" ;; +esac + +DOWNLOAD_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-${RELEASE_ARCH}.tar.gz" +TEMP_DIR="$(mktemp -d)" + +log "Downloading ironclaw-${RELEASE_ARCH} from GitHub releases..." +if ! curl -fsSL "${DOWNLOAD_URL}" -o "${TEMP_DIR}/ironclaw.tar.gz"; then + rm -rf "${TEMP_DIR}" + fail "Failed to download IronClaw from ${DOWNLOAD_URL}" +fi + +# Extract and find the binary (tar layout may vary across releases) +tar xzf "${TEMP_DIR}/ironclaw.tar.gz" -C "${TEMP_DIR}" +IRONCLAW_BIN="$(find "${TEMP_DIR}" -name 'ironclaw' -type f -executable 2>/dev/null | head -1)" +if [[ -z "${IRONCLAW_BIN}" ]]; then + # Fallback: binary might not have +x in the archive + IRONCLAW_BIN="$(find "${TEMP_DIR}" -name 'ironclaw' -type f 2>/dev/null | head -1)" +fi +if [[ -z "${IRONCLAW_BIN}" ]]; then + rm -rf "${TEMP_DIR}" + fail "Could not find ironclaw binary in downloaded archive" +fi + +mkdir -p "${HOME}/.local/bin" +install -m 755 "${IRONCLAW_BIN}" "${HOME}/.local/bin/ironclaw" +rm -rf "${TEMP_DIR}" + +export PATH="${HOME}/.local/bin:$PATH" + +if ! command -v ironclaw &>/dev/null; then + fail "ironclaw command not found after install. Check PATH." +fi + +IC_VERSION="$(ironclaw --version 2>/dev/null || echo unknown)" +ok "IronClaw installed: ${IC_VERSION}" + +# ── Configure IronClaw ──────────────────────────────────────────────────────── +step "Configuring IronClaw" + +mkdir -p "${HOME}/.ironclaw" + +# Write .env with bedrockify config — bypasses NEAR AI OAuth entirely +cat > "${HOME}/.ironclaw/.env" < Date: Tue, 31 Mar 2026 22:34:04 +0000 Subject: [PATCH 013/172] fix: use stdlib-only YAML parser (no PyYAML), remove leaked ironclaw files Code review findings: - CRITICAL: import yaml requires PyYAML which isn't in Python stdlib. The installer runs on user machines (laptops, CloudShell) where PyYAML may not be installed. Replaced with a simple regex-based parser using only stdlib (re module). - Removed ironclaw pack files that leaked from parallel sub-agent work. Those belong on feature/pack-ironclaw only. --- install.sh | 46 ++++-- packs/ironclaw/install.sh | 176 ---------------------- packs/ironclaw/manifest.yaml | 36 ----- packs/ironclaw/resources/shell-profile.sh | 3 - 4 files changed, 35 insertions(+), 226 deletions(-) delete mode 100755 packs/ironclaw/install.sh delete mode 100644 packs/ironclaw/manifest.yaml delete mode 100644 packs/ironclaw/resources/shell-profile.sh diff --git a/install.sh b/install.sh index e0aa1ac..fbc2c05 100755 --- a/install.sh +++ b/install.sh @@ -385,19 +385,34 @@ collect_config() { local -a pack_descs=() local -a pack_experimental=() - # Parse agent packs from registry.yaml (requires python3, already validated) + # Parse agent packs from registry.yaml (stdlib only — no PyYAML dependency) while IFS='|' read -r pname pdesc pexp; do pack_names+=("$pname") pack_descs+=("$pdesc") pack_experimental+=("$pexp") done < <(python3 -c " -import yaml, sys -with open('$registry') as f: - reg = yaml.safe_load(f) -for name, cfg in reg.get('packs', {}).items(): +import re, sys +text = open('$registry').read() +# Simple state-machine parser for the flat registry YAML structure +current_pack = None +packs = {} +for line in text.split('\n'): + # Top-level pack name (2-space indent under packs:) + m = re.match(r'^ (\w[\w-]*):\s*$', line) + if m: + current_pack = m.group(1) + packs[current_pack] = {} + continue + if current_pack: + # Key-value pairs (4-space indent) + kv = re.match(r'^ (\w[\w-]*):\s+(.+)$', line) + if kv: + packs[current_pack][kv.group(1)] = kv.group(2).strip().strip('\"').strip(\"'\") +for name, cfg in packs.items(): if cfg.get('type') == 'agent': - exp = 'true' if cfg.get('experimental', False) else 'false' - print(f\"{name}|{cfg.get('description', name)}|{exp}\") + desc = cfg.get('description', name) + exp = 'true' if cfg.get('experimental', '').lower() == 'true' else 'false' + print(f'{name}|{desc}|{exp}') " 2>/dev/null || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") echo " Agent to deploy:" @@ -440,10 +455,19 @@ for name, cfg in reg.get('packs', {}).items(): local default_size_choice="3" # default → t4g.xlarge local pack_instance_type pack_instance_type=$(python3 -c " -import yaml -with open('$registry') as f: - reg = yaml.safe_load(f) -print(reg.get('packs', {}).get('$PACK_NAME', {}).get('instance_type', 't4g.xlarge')) +import re +text = open('$registry').read() +current_pack = None +for line in text.split('\n'): + m = re.match(r'^ (\w[\w-]*):\s*$', line) + if m: + current_pack = m.group(1) + continue + if current_pack == '$PACK_NAME': + kv = re.match(r'^ instance_type:\s+(.+)$', line) + if kv: + print(kv.group(1).strip().strip('\"')) + break " 2>/dev/null || echo "t4g.xlarge") case "$pack_instance_type" in t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh deleted file mode 100755 index 7aee789..0000000 --- a/packs/ironclaw/install.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -# packs/ironclaw/install.sh — Install IronClaw (NEAR AI Rust agent) and configure it to use bedrockify -# -# Usage: -# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] -# -# Assumes: -# - bedrockify is already installed and running (see packs/bedrockify/) -# - curl available -# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) -# -# Idempotent: safe to re-run. -# -# Notes: -# - IronClaw is a single static Rust binary (musl build) — no Rust/Cargo needed. -# - We bypass the `ironclaw onboard` OAuth wizard by writing .env directly with -# LLM_BACKEND=openai_compatible pointing at bedrockify. -# - Known issue: IronClaw may attempt to use the Linux secret-service (dbus) for -# keychain operations on some code paths. On headless EC2 this can error. -# If IronClaw adds an IRONCLAW_NO_KEYCHAIN or similar env var in a future release, -# add it to ~/.ironclaw/.env to suppress the keychain lookup. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# shellcheck source=../common.sh -source "${SCRIPT_DIR}/../common.sh" - -# ── Defaults ────────────────────────────────────────────────────────────────── -# Defaults from config file (written by bootstrap dispatcher), then CLI overrides -PACK_ARG_REGION="$(pack_config_get region "us-east-1")" -PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" -PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" - -# ── Help ────────────────────────────────────────────────────────────────────── -usage() { - cat <&1)" || true -if ! printf '%s' "${HEALTH}" | grep -q '"status":"ok"'; then - fail "bedrockify is not running on port ${BEDROCKIFY_PORT}. Install bedrockify pack first." -fi -ok "bedrockify is healthy on port ${BEDROCKIFY_PORT}" - -# ── Install IronClaw binary ─────────────────────────────────────────────────── -step "Installing IronClaw binary" - -# Detect architecture -ARCH=$(uname -m) -case "$ARCH" in - aarch64|arm64) RELEASE_ARCH="aarch64-unknown-linux-musl" ;; - x86_64) RELEASE_ARCH="x86_64-unknown-linux-musl" ;; - *) fail "Unsupported architecture: $ARCH" ;; -esac -log "Architecture: ${ARCH} → ${RELEASE_ARCH}" - -if command -v ironclaw &>/dev/null; then - IRONCLAW_EXISTING="$(ironclaw --version 2>/dev/null || echo unknown)" - log "ironclaw already installed (${IRONCLAW_EXISTING}) — reinstalling" -fi - -# Ensure local bin dir exists -mkdir -p "${HOME}/.local/bin" - -# Download and extract to /tmp, then find and install the binary -RELEASE_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-${RELEASE_ARCH}.tar.gz" -log "Downloading from: ${RELEASE_URL}" - -EXTRACT_DIR="$(mktemp -d /tmp/ironclaw-extract-XXXXXX)" -trap 'rm -rf "${EXTRACT_DIR}"' EXIT - -curl -fsSL "${RELEASE_URL}" | tar xz -C "${EXTRACT_DIR}" - -# Find the ironclaw binary (may be at root or in a subdirectory) -IRONCLAW_BIN="$(find "${EXTRACT_DIR}" -type f -name "ironclaw" | head -1)" -if [[ -z "${IRONCLAW_BIN}" ]]; then - # Some releases use the full target triple as the binary name - IRONCLAW_BIN="$(find "${EXTRACT_DIR}" -type f \( -name "ironclaw*" \) | grep -v '\.tar\|\.gz\|\.md\|\.txt' | head -1)" -fi -if [[ -z "${IRONCLAW_BIN}" ]]; then - fail "Could not locate ironclaw binary in extracted archive. Contents: $(find "${EXTRACT_DIR}" | head -20)" -fi -log "Found binary: ${IRONCLAW_BIN}" - -install -m 755 "${IRONCLAW_BIN}" "${HOME}/.local/bin/ironclaw" - -# Add local bin to PATH for current session -export PATH="${HOME}/.local/bin:$PATH" - -if ! command -v ironclaw &>/dev/null; then - fail "ironclaw command not found after install. Check PATH or install output." -fi - -IRONCLAW_VERSION="$(ironclaw --version 2>/dev/null || echo unknown)" -ok "IronClaw installed: ${IRONCLAW_VERSION}" - -# ── Configure IronClaw ──────────────────────────────────────────────────────── -step "Configuring IronClaw" - -mkdir -p "${HOME}/.ironclaw" - -# Write .env to configure bedrockify as the OpenAI-compatible backend. -# This bypasses the `ironclaw onboard` OAuth wizard entirely. -cat > "${HOME}/.ironclaw/.env" < Date: Tue, 31 Mar 2026 22:41:44 +0000 Subject: [PATCH 014/172] fix: remove fake IRONCLAW_DISABLE_KEYRING env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review finding: IRONCLAW_DISABLE_KEYRING is not a real IronClaw env var — it was guessed. IronClaw uses secret-service/zbus for OS credential store but the LLM_BACKEND=openai_compatible path bypasses it. Replaced with accurate comment about the dbus situation. --- packs/ironclaw/install.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index a1da26c..4b5aaea 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -17,8 +17,10 @@ # LLM_BACKEND=openai_compatible, pointing at bedrockify. # # Known issue: IronClaw may attempt dbus/secret-service for keychain access -# on Linux. On headless EC2, this may fail silently. We set -# IRONCLAW_DISABLE_KEYRING=1 as a workaround. +# on Linux. On headless EC2, this may fail silently. Since we set +# LLM_BACKEND=openai_compatible with explicit credentials in .env, +# the OS credential store path should not be triggered for LLM access. +# If startup fails with dbus errors, install dbus: sudo dnf install -y dbus # # Idempotent: safe to re-run. @@ -148,9 +150,6 @@ LLM_BACKEND=openai_compatible LLM_BASE_URL=http://127.0.0.1:${BEDROCKIFY_PORT}/v1 LLM_API_KEY=not-needed LLM_MODEL=${MODEL} - -# Disable keyring — headless EC2 has no dbus/secret-service -IRONCLAW_DISABLE_KEYRING=1 EOF chmod 600 "${HOME}/.ironclaw/.env" From 428d9761d8dd69ec1eac8e0eee248b9b43dabfc9 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:42:02 +0000 Subject: [PATCH 015/172] =?UTF-8?q?fix:=20sanitize=20pack=5Fchoice=20input?= =?UTF-8?q?=20=E2=80=94=20strip=20non-digits=20to=20prevent=20bash=20arith?= =?UTF-8?q?metic=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review finding: if user enters non-numeric text (e.g. 'abc' or empty), bash arithmetic $(( pack_choice - 1 )) would error on some shells. Now strips non-digit chars and defaults to 1. --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index fbc2c05..2963bdd 100755 --- a/install.sh +++ b/install.sh @@ -428,6 +428,9 @@ for name, cfg in packs.items(): echo "" local pack_choice prompt "Deploy which agent" pack_choice "1" + # Sanitize: strip non-digits, default to 1 + pack_choice="${pack_choice//[^0-9]/}" + [[ -z "$pack_choice" ]] && pack_choice=1 local idx=$(( pack_choice - 1 )) if [[ $idx -lt 0 || $idx -ge ${#pack_names[@]} ]]; then idx=0 # default to first (openclaw) From 60e59014480c51988d2de98f34753842aac87d21 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:48:45 +0000 Subject: [PATCH 016/172] fix: fetch registry.yaml from GitHub when CLONE_DIR is not yet set collect_config() runs before prepare_repo(), so CLONE_DIR is empty and the registry file doesn't exist locally. Now falls back to fetching from the GitHub raw URL. Also guards both Python calls against empty registry path. --- install.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 2963bdd..18b276a 100755 --- a/install.sh +++ b/install.sh @@ -380,7 +380,14 @@ collect_config() { echo "" # ---- Pack selection (dynamically discovered from registry.yaml) ----------- - local registry="${CLONE_DIR}/packs/registry.yaml" + # CLONE_DIR may not be set yet (repo is cloned after config collection). + # If the local file isn't available, fetch from GitHub. + local registry="${CLONE_DIR:-}/packs/registry.yaml" + if [[ ! -f "$registry" ]]; then + local registry_url="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/packs/registry.yaml" + registry="/tmp/loki-registry-$$.yaml" + curl -sfL "$registry_url" -o "$registry" 2>/dev/null || registry="" + fi local -a pack_names=() local -a pack_descs=() local -a pack_experimental=() @@ -390,7 +397,7 @@ collect_config() { pack_names+=("$pname") pack_descs+=("$pdesc") pack_experimental+=("$pexp") - done < <(python3 -c " + done < <([ -n "$registry" ] && python3 -c " import re, sys text = open('$registry').read() # Simple state-machine parser for the flat registry YAML structure @@ -457,7 +464,7 @@ for name, cfg in packs.items(): # Adjust instance size default based on pack registry local default_size_choice="3" # default → t4g.xlarge local pack_instance_type - pack_instance_type=$(python3 -c " + pack_instance_type=$([ -n "$registry" ] && python3 -c " import re text = open('$registry').read() current_pack = None From 3d212346f59ee180a6819c161ad72d496fb599d4 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:50:00 +0000 Subject: [PATCH 017/172] fix: pass registry path and pack name via sys.argv, not string interpolation Code review MEDIUM findings: - $registry path interpolated in Python open() breaks if path has quotes - $PACK_NAME interpolated in Python string is fragile injection risk Both now passed as sys.argv[1]/sys.argv[2] instead. --- install.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index 18b276a..d4f0f51 100755 --- a/install.sh +++ b/install.sh @@ -399,7 +399,7 @@ collect_config() { pack_experimental+=("$pexp") done < <([ -n "$registry" ] && python3 -c " import re, sys -text = open('$registry').read() +text = open(sys.argv[1]).read() # Simple state-machine parser for the flat registry YAML structure current_pack = None packs = {} @@ -420,7 +420,7 @@ for name, cfg in packs.items(): desc = cfg.get('description', name) exp = 'true' if cfg.get('experimental', '').lower() == 'true' else 'false' print(f'{name}|{desc}|{exp}') -" 2>/dev/null || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") +" "$registry" 2>/dev/null || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") echo " Agent to deploy:" local i @@ -465,20 +465,21 @@ for name, cfg in packs.items(): local default_size_choice="3" # default → t4g.xlarge local pack_instance_type pack_instance_type=$([ -n "$registry" ] && python3 -c " -import re -text = open('$registry').read() +import re, sys +text = open(sys.argv[1]).read() +pack_name = sys.argv[2] current_pack = None for line in text.split('\n'): m = re.match(r'^ (\w[\w-]*):\s*$', line) if m: current_pack = m.group(1) continue - if current_pack == '$PACK_NAME': + if current_pack == pack_name: kv = re.match(r'^ instance_type:\s+(.+)$', line) if kv: print(kv.group(1).strip().strip('\"')) break -" 2>/dev/null || echo "t4g.xlarge") +" "$registry" "$PACK_NAME" 2>/dev/null || echo "t4g.xlarge") case "$pack_instance_type" in t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; t4g.large) default_size_choice="2" ;; From a2588095fee56ed0ddde0663158a1daccc50858a Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 22:56:58 +0000 Subject: [PATCH 018/172] improve: extract check_bedrockify_health to common.sh, add ironclaw pack tests - Extract bedrockify health check into shared check_bedrockify_health() helper in common.sh (DRY: same pattern was inlined in every pack) - Refactor ironclaw/install.sh to use the shared helper - Add packs/ironclaw/test.sh with 35 offline tests covering: - manifest.yaml structure validation - Architecture detection (aarch64/arm64/x86_64/unsupported) - Download URL construction - .env config generation (LLM_BACKEND, model, port, no NEAR AI tokens) - Temp dir cleanup on failure/success paths - common.sh integration (shared helper used, inline code removed) - Shell profile resources - Script basics (strict mode, sourcing, idempotency docs) - Live environment tests (skipped when ironclaw/bedrockify unavailable) --- packs/common.sh | 13 ++ packs/ironclaw/install.sh | 7 +- packs/ironclaw/test.sh | 284 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 6 deletions(-) create mode 100755 packs/ironclaw/test.sh diff --git a/packs/common.sh b/packs/common.sh index 0cd14bd..e0ee9bb 100755 --- a/packs/common.sh +++ b/packs/common.sh @@ -66,6 +66,19 @@ pack_config_get() { echo "$default" } +# check_bedrockify_health PORT +# Verify bedrockify is running and healthy on the given port. +# Fails with a clear message if not reachable. +check_bedrockify_health() { + local port="${1:?usage: check_bedrockify_health PORT}" + local health + health="$(curl -sf "http://127.0.0.1:${port}/" 2>&1)" || true + if ! printf '%s' "${health}" | grep -q '"status":"ok"'; then + fail "bedrockify is not running on port ${port}. Install bedrockify pack first." + fi + ok "bedrockify is healthy on port ${port}" +} + # pack_banner NAME ACTION pack_banner() { local name="$1" diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 4b5aaea..74518b7 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -80,12 +80,7 @@ log "region=${REGION} model=${MODEL} bedrockify-port=${BEDROCKIFY_PORT}" step "Checking prerequisites" require_cmd curl tar -# Verify bedrockify is running -HEALTH="$(curl -sf "http://127.0.0.1:${BEDROCKIFY_PORT}/" 2>&1)" || true -if ! printf '%s' "${HEALTH}" | grep -q '"status":"ok"'; then - fail "bedrockify is not running on port ${BEDROCKIFY_PORT}. Install bedrockify pack first." -fi -ok "bedrockify is healthy on port ${BEDROCKIFY_PORT}" +check_bedrockify_health "${BEDROCKIFY_PORT}" # ── Install IronClaw ────────────────────────────────────────────────────────── step "Installing IronClaw" diff --git a/packs/ironclaw/test.sh b/packs/ironclaw/test.sh new file mode 100755 index 0000000..844ebe5 --- /dev/null +++ b/packs/ironclaw/test.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# packs/ironclaw/test.sh — Offline tests for the IronClaw pack +# +# Validates config generation, manifest structure, architecture detection, +# download URL construction, and temp dir cleanup — WITHOUT requiring +# IronClaw installed or bedrockify running. +# +# Usage: bash packs/ironclaw/test.sh +# Exit: 0 = all passed, 1 = failures + +set -uo pipefail + +# ── Test framework ──────────────────────────────────────────────────────────── +_PASS=0 +_FAIL=0 +_SKIP=0 + +pass() { (( _PASS++ )); printf "\033[0;32m ✓ %s\033[0m\n" "$1"; } +fail_test() { (( _FAIL++ )); printf "\033[0;31m ✗ %s\033[0m\n" "$1"; } +skip() { (( _SKIP++ )); printf "\033[0;33m ⊘ %s (skipped: %s)\033[0m\n" "$1" "$2"; } + +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "${expected}" == "${actual}" ]]; then + pass "${desc}" + else + fail_test "${desc}: expected '${expected}', got '${actual}'" + fi +} + +assert_contains() { + local desc="$1" haystack="$2" needle="$3" + if printf '%s' "${haystack}" | grep -qF "${needle}"; then + pass "${desc}" + else + fail_test "${desc}: '${needle}' not found in output" + fi +} + +assert_not_contains() { + local desc="$1" haystack="$2" needle="$3" + if ! printf '%s' "${haystack}" | grep -qF "${needle}"; then + pass "${desc}" + else + fail_test "${desc}: '${needle}' should NOT be in output" + fi +} + +assert_file_exists() { + local desc="$1" path="$2" + if [[ -f "${path}" ]]; then + pass "${desc}" + else + fail_test "${desc}: file not found: ${path}" + fi +} + +summary() { + printf "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + printf " Results: %d passed, %d failed, %d skipped\n" "${_PASS}" "${_FAIL}" "${_SKIP}" + printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + [[ ${_FAIL} -eq 0 ]] +} + +# ── Resolve paths ───────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACK_DIR="${SCRIPT_DIR}" +PACKS_ROOT="$(cd "${PACK_DIR}/.." && pwd)" + +printf "\n🦀 IronClaw Pack Tests\n\n" + +# ══════════════════════════════════════════════════════════════════════════════ +# 1. Manifest validation +# ══════════════════════════════════════════════════════════════════════════════ +printf "── manifest.yaml ──\n" + +MANIFEST="${PACK_DIR}/manifest.yaml" +assert_file_exists "manifest.yaml exists" "${MANIFEST}" + +if command -v python3 &>/dev/null; then + # Use python to parse YAML (no yq dependency) + MANIFEST_JSON="$(python3 -c " +import yaml, json, sys +with open('${MANIFEST}') as f: + print(json.dumps(yaml.safe_load(f))) +" 2>/dev/null)" || MANIFEST_JSON="" + + if [[ -n "${MANIFEST_JSON}" ]]; then + NAME="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))")" + assert_eq "manifest name is 'ironclaw'" "ironclaw" "${NAME}" + + TYPE="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('type',''))")" + assert_eq "manifest type is 'agent'" "agent" "${TYPE}" + + DEPS="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('deps',[])))")" + assert_contains "manifest declares bedrockify dep" "${DEPS}" "bedrockify" + + HEALTH_CMD="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('health_check',{}).get('command',''))")" + assert_eq "health_check uses ironclaw --version" "ironclaw --version" "${HEALTH_CMD}" + + PROVIDES_CMDS="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('provides',{}).get('commands',[])))")" + assert_contains "provides ironclaw command" "${PROVIDES_CMDS}" "ironclaw" + + PROVIDES_SVCS="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('provides',{}).get('services',[])))")" + assert_eq "provides no services (CLI only)" "" "${PROVIDES_SVCS}" + + ARCHES="$(printf '%s' "${MANIFEST_JSON}" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin).get('requirements',{}).get('arch',[])))")" + assert_contains "supports arm64" "${ARCHES}" "arm64" + assert_contains "supports amd64" "${ARCHES}" "amd64" + else + skip "manifest YAML parse" "python3 yaml module not available" + fi +else + skip "manifest validation" "python3 not available" +fi + +# ══════════════════════════════════════════════════════════════════════════════ +# 2. Architecture detection and download URL construction +# ══════════════════════════════════════════════════════════════════════════════ +printf "\n── architecture detection ──\n" + +# Extract the arch-mapping logic from install.sh and test it +test_arch_mapping() { + local arch="$1" expected="$2" + local result + case "${arch}" in + aarch64|arm64) result="aarch64-unknown-linux-musl" ;; + x86_64) result="x86_64-unknown-linux-musl" ;; + *) result="UNSUPPORTED" ;; + esac + assert_eq "arch '${arch}' → '${expected}'" "${expected}" "${result}" +} + +test_arch_mapping "aarch64" "aarch64-unknown-linux-musl" +test_arch_mapping "arm64" "aarch64-unknown-linux-musl" +test_arch_mapping "x86_64" "x86_64-unknown-linux-musl" +test_arch_mapping "i386" "UNSUPPORTED" +test_arch_mapping "riscv64" "UNSUPPORTED" + +# Test URL construction +RELEASE_ARCH="aarch64-unknown-linux-musl" +EXPECTED_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-aarch64-unknown-linux-musl.tar.gz" +ACTUAL_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-${RELEASE_ARCH}.tar.gz" +assert_eq "download URL for aarch64" "${EXPECTED_URL}" "${ACTUAL_URL}" + +RELEASE_ARCH="x86_64-unknown-linux-musl" +EXPECTED_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-x86_64-unknown-linux-musl.tar.gz" +ACTUAL_URL="https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-${RELEASE_ARCH}.tar.gz" +assert_eq "download URL for x86_64" "${EXPECTED_URL}" "${ACTUAL_URL}" + +# ══════════════════════════════════════════════════════════════════════════════ +# 3. .env config generation +# ══════════════════════════════════════════════════════════════════════════════ +printf "\n── .env config generation ──\n" + +TEST_TMP="$(mktemp -d)" +trap 'rm -rf "${TEST_TMP}"' EXIT + +# Simulate what install.sh does for .env generation +TEST_MODEL="us.anthropic.claude-sonnet-4-6-v1" +TEST_PORT="8090" +cat > "${TEST_TMP}/.env" < "${TEST_TMP}/.env2" < "${TEST_TMP}/.env3" </dev/null; then + IC_VER="$(ironclaw --version 2>/dev/null || echo "")" + if [[ -n "${IC_VER}" ]]; then + pass "ironclaw --version: ${IC_VER}" + else + fail_test "ironclaw installed but --version failed" + fi +else + skip "ironclaw --version" "ironclaw not installed" +fi + +if curl -sf "http://127.0.0.1:8090/" 2>/dev/null | grep -q '"status":"ok"'; then + pass "bedrockify is healthy" +else + skip "bedrockify health check" "bedrockify not running" +fi + +if [[ -f "${HOME}/.ironclaw/.env" ]]; then + LIVE_ENV="$(cat "${HOME}/.ironclaw/.env")" + assert_contains "live .env has LLM_BACKEND" "${LIVE_ENV}" "LLM_BACKEND=openai_compatible" +else + skip "live .env validation" ".ironclaw/.env not found" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +summary From 42e1091cf7063d9fcd3aaa94029099e9989ff15a Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:00:22 +0000 Subject: [PATCH 019/172] improve: add tests, fix shell-profile.sh to match pack conventions - Add packs/pi/test.sh (41 tests): manifest validation, install.sh interface, models.json generation with various model ID formats, shell-profile.sh variable checks, idempotency patterns - Fix shell-profile.sh: add comment header, PACK_ALIASES, expand PACK_BANNER_NAME and PACK_BANNER_COMMANDS to match hermes/openclaw pack conventions --- packs/pi/resources/shell-profile.sh | 15 +- packs/pi/test.sh | 366 ++++++++++++++++++++++++++++ 2 files changed, 379 insertions(+), 2 deletions(-) create mode 100755 packs/pi/test.sh diff --git a/packs/pi/resources/shell-profile.sh b/packs/pi/resources/shell-profile.sh index 571181b..f5376ab 100644 --- a/packs/pi/resources/shell-profile.sh +++ b/packs/pi/resources/shell-profile.sh @@ -1,3 +1,14 @@ +# Pi shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +# This file defines aliases and the welcome banner for the Pi pack. + +PACK_ALIASES=' +alias p="pi" +' + +PACK_BANNER_NAME="Pi Coding Agent Environment" PACK_BANNER_EMOJI="🥧" -PACK_BANNER_NAME="Pi" -PACK_BANNER_COMMANDS="pi" +PACK_BANNER_COMMANDS=' + pi → Run Pi coding agent + pi --help → Show Pi options + pi --version → Show installed version +' diff --git a/packs/pi/test.sh b/packs/pi/test.sh new file mode 100755 index 0000000..ab259df --- /dev/null +++ b/packs/pi/test.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +# packs/pi/test.sh — Unit tests for the Pi Coding Agent pack +# +# Validates config generation, manifest structure, shell-profile, and install.sh +# interface WITHOUT requiring Pi to be installed or bedrockify to be running. +# +# Usage: bash packs/pi/test.sh +# Exit: 0 if all tests pass, 1 otherwise. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACK_DIR="${SCRIPT_DIR}" +PACKS_DIR="${SCRIPT_DIR}/.." +COMMON="${PACKS_DIR}/common.sh" + +# ── Test harness ────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +PASS=0 +FAIL=0 +SKIP=0 + +pass() { printf "${GREEN} ✓${NC} %s\n" "$1"; PASS=$((PASS+1)); } +fail() { printf "${RED} ✗${NC} %s\n" "$1"; FAIL=$((FAIL+1)); } +skip() { printf "${YELLOW} ○${NC} %s\n" "$1 (skipped)"; SKIP=$((SKIP+1)); } +header() { printf "\n${BOLD}${CYAN}%s${NC}\n" "$1"; } + +# ── Test: manifest.yaml structure ───────────────────────────────────────────── +header "Test: manifest.yaml" + +MANIFEST="${PACK_DIR}/manifest.yaml" + +if [[ -f "${MANIFEST}" ]]; then + pass "manifest.yaml exists" +else + fail "manifest.yaml missing" +fi + +# Valid YAML +if command -v python3 &>/dev/null; then + if python3 -c "import yaml; yaml.safe_load(open('${MANIFEST}'))" 2>/dev/null; then + pass "manifest.yaml is valid YAML" + else + fail "manifest.yaml is invalid YAML" + fi + + # Required keys + for key in name version type description deps params health_check provides; do + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +sys.exit(0 if '${key}' in data else 1) +" 2>/dev/null; then + pass "manifest.yaml has '${key}' key" + else + fail "manifest.yaml missing '${key}' key" + fi + done + + # Name matches folder + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +sys.exit(0 if data.get('name') == 'pi' else 1) +" 2>/dev/null; then + pass "manifest.yaml name matches folder (pi)" + else + fail "manifest.yaml name does not match folder" + fi + + # Deps include bedrockify + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +sys.exit(0 if 'bedrockify' in data.get('deps', []) else 1) +" 2>/dev/null; then + pass "manifest.yaml deps include bedrockify" + else + fail "manifest.yaml deps missing bedrockify" + fi + + # Health check command references pi + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +hc = data.get('health_check', {}).get('command', '') +sys.exit(0 if 'pi' in hc else 1) +" 2>/dev/null; then + pass "manifest.yaml health_check references pi" + else + fail "manifest.yaml health_check does not reference pi" + fi + + # Provides commands include pi + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +cmds = data.get('provides', {}).get('commands', []) +sys.exit(0 if 'pi' in cmds else 1) +" 2>/dev/null; then + pass "manifest.yaml provides.commands includes pi" + else + fail "manifest.yaml provides.commands missing pi" + fi +else + skip "manifest.yaml structure tests: python3 not available" +fi + +# ── Test: install.sh interface ──────────────────────────────────────────────── +header "Test: install.sh interface" + +INSTALL="${PACK_DIR}/install.sh" + +if [[ -f "${INSTALL}" ]]; then + pass "install.sh exists" +else + fail "install.sh missing" +fi + +if [[ -x "${INSTALL}" ]]; then + pass "install.sh is executable" +else + fail "install.sh is not executable" +fi + +# Shebang check +SHEBANG="$(head -1 "${INSTALL}")" +if [[ "${SHEBANG}" == "#!/usr/bin/env bash" ]]; then + pass "install.sh has correct shebang" +else + fail "install.sh has unexpected shebang: ${SHEBANG}" +fi + +# Sources common.sh +if grep -q 'source.*common\.sh' "${INSTALL}"; then + pass "install.sh sources common.sh" +else + fail "install.sh does not source common.sh" +fi + +# Writes done marker +if grep -q 'write_done_marker.*pi' "${INSTALL}"; then + pass "install.sh writes done marker for 'pi'" +else + fail "install.sh does not write done marker" +fi + +# --help exits 0 +HELP_OUT="$(bash "${INSTALL}" --help 2>&1)" && HELP_RC=0 || HELP_RC=$? +if [[ "${HELP_RC}" -eq 0 ]]; then + pass "install.sh --help exits 0" +else + fail "install.sh --help exits ${HELP_RC}" +fi + +if [[ -n "${HELP_OUT}" ]]; then + pass "install.sh --help produces output" +else + fail "install.sh --help produces no output" +fi + +# --help mentions key flags +for flag in --region --model --bedrockify-port --help; do + if printf '%s' "${HELP_OUT}" | grep -q -- "${flag}"; then + pass "install.sh --help mentions ${flag}" + else + fail "install.sh --help missing ${flag}" + fi +done + +# ── Test: models.json generation ────────────────────────────────────────────── +header "Test: models.json config generation" + +# We test the heredoc template by simulating what install.sh does: variable +# substitution into the JSON template. We extract the template logic and +# run it with different MODEL/BEDROCKIFY_PORT values. + +generate_models_json() { + local MODEL="$1" + local BEDROCKIFY_PORT="$2" + cat </dev/null; then + pass "models.json: valid JSON with default model" +else + fail "models.json: invalid JSON with default model" +fi + +# Verify model ID appears in output +if printf '%s' "${JSON_OUT}" | grep -q '"us.anthropic.claude-sonnet-4-6-v1"'; then + pass "models.json: contains correct model ID" +else + fail "models.json: missing model ID" +fi + +# Verify bedrockify port in baseUrl +if printf '%s' "${JSON_OUT}" | grep -q 'http://127.0.0.1:8090/v1'; then + pass "models.json: baseUrl has correct port" +else + fail "models.json: baseUrl has wrong port" +fi + +# Test with a model ID containing special characters (slashes, dots) +JSON_OUT="$(generate_models_json "anthropic/claude-opus-4.6" "9090")" +if printf '%s' "${JSON_OUT}" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then + pass "models.json: valid JSON with slash/dot model ID" +else + fail "models.json: invalid JSON with slash/dot model ID" +fi + +if printf '%s' "${JSON_OUT}" | grep -q 'http://127.0.0.1:9090/v1'; then + pass "models.json: baseUrl reflects custom port 9090" +else + fail "models.json: baseUrl wrong for custom port" +fi + +# Test with a model ID containing colons +JSON_OUT="$(generate_models_json "us.amazon.nova-premier-v1:0" "8090")" +if printf '%s' "${JSON_OUT}" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then + pass "models.json: valid JSON with colon in model ID" +else + fail "models.json: invalid JSON with colon in model ID" +fi + +# Test with empty model (edge case) +JSON_OUT="$(generate_models_json "" "8090")" +if printf '%s' "${JSON_OUT}" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then + pass "models.json: valid JSON with empty model (edge case)" +else + fail "models.json: invalid JSON with empty model" +fi + +# Verify JSON structure deeply +JSON_OUT="$(generate_models_json "test-model" "8090")" +if python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +p = data['providers']['bedrockify'] +assert p['api'] == 'openai-completions', 'wrong api' +assert p['apiKey'] == 'not-needed', 'wrong apiKey' +assert isinstance(p['models'], list), 'models not a list' +assert len(p['models']) == 1, 'expected 1 model' +assert p['models'][0]['id'] == 'test-model', 'wrong model id' +assert p['compat']['supportsDeveloperRole'] == False, 'wrong compat' +assert p['compat']['supportsReasoningEffort'] == False, 'wrong compat' +assert '/v1' in p['baseUrl'], 'baseUrl missing /v1' +" <<< "${JSON_OUT}" 2>/dev/null; then + pass "models.json: structure validated (api, apiKey, models array, compat)" +else + fail "models.json: structure validation failed" +fi + +# ── Test: shell-profile.sh ─────────────────────────────────────────────────── +header "Test: shell-profile.sh" + +PROFILE="${PACK_DIR}/resources/shell-profile.sh" + +if [[ -f "${PROFILE}" ]]; then + pass "shell-profile.sh exists" +else + fail "shell-profile.sh missing" +fi + +# Sourcing should not error +if bash -c "source '${PROFILE}'" 2>/dev/null; then + pass "shell-profile.sh sources without error" +else + fail "shell-profile.sh sources with error" +fi + +# Required variables +for var in PACK_BANNER_EMOJI PACK_BANNER_NAME PACK_BANNER_COMMANDS; do + VAL="$(bash -c "source '${PROFILE}'; echo \"\${${var}:-}\"" 2>/dev/null)" + if [[ -n "${VAL}" ]]; then + pass "shell-profile.sh: ${var} is set" + else + fail "shell-profile.sh: ${var} is empty or missing" + fi +done + +# PACK_BANNER_COMMANDS should mention 'pi' +CMDS="$(bash -c "source '${PROFILE}'; echo \"\${PACK_BANNER_COMMANDS}\"" 2>/dev/null)" +if printf '%s' "${CMDS}" | grep -q 'pi'; then + pass "shell-profile.sh: PACK_BANNER_COMMANDS mentions 'pi'" +else + fail "shell-profile.sh: PACK_BANNER_COMMANDS does not mention 'pi'" +fi + +# ── Test: idempotency patterns ───────────────────────────────────────────────── +header "Test: idempotency patterns" + +# Check that install.sh handles pi already installed (re-install path) +if grep -q 'command -v pi' "${INSTALL}"; then + pass "install.sh checks if pi is already installed" +else + fail "install.sh does not check for existing pi installation" +fi + +# Check for mkdir -p (idempotent directory creation) +if grep -q 'mkdir -p' "${INSTALL}"; then + pass "install.sh uses mkdir -p for config directory" +else + fail "install.sh does not use mkdir -p" +fi + +# ── Test: bedrockify health check (LIVE — skippable) ───────────────────────── +header "Test: bedrockify health check (live environment)" + +if curl -sf "http://127.0.0.1:8090/" 2>/dev/null | grep -q '"status":"ok"'; then + pass "bedrockify is running on port 8090" +else + skip "bedrockify not running — live tests skipped" +fi + +# ── Test: pi command available (LIVE — skippable) ───────────────────────────── +header "Test: pi command (live environment)" + +if command -v pi &>/dev/null; then + PI_VER="$(pi --version 2>/dev/null || echo unknown)" + pass "pi is installed: ${PI_VER}" +else + skip "pi not installed — live tests skipped" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +printf "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf "${BOLD} Pi Pack Test Results${NC}\n" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf " ${GREEN}Passed:${NC} %d\n" "${PASS}" +printf " ${RED}Failed:${NC} %d\n" "${FAIL}" +printf " ${YELLOW}Skipped:${NC} %d\n" "${SKIP}" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n\n" + +if [[ "${FAIL}" -gt 0 ]]; then + printf "${RED}✗ %d test(s) failed${NC}\n\n" "${FAIL}" + exit 1 +else + printf "${GREEN}✓ All tests passed${NC}\n\n" + exit 0 +fi From d1ab64ee67cb60440e11b08b78145bf1c5ac226b Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:01:28 +0000 Subject: [PATCH 020/172] =?UTF-8?q?ci:=20add=20pack=20test=20workflow=20?= =?UTF-8?q?=E2=80=94=20auto-discovers=20test.sh=20by=20naming=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovers tests at: - packs/*/test.sh (per-pack tests) - tests/test-*.sh (project-level tests) Runs each in a separate matrix job (fail-fast: false). Triggers on pushes to main and feature branches when packs/tests change. --- .github/workflows/pack-tests.yml | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/pack-tests.yml diff --git a/.github/workflows/pack-tests.yml b/.github/workflows/pack-tests.yml new file mode 100644 index 0000000..0434556 --- /dev/null +++ b/.github/workflows/pack-tests.yml @@ -0,0 +1,59 @@ +name: Pack tests + +on: + push: + branches: [main, 'feature/**'] + paths: + - 'packs/**' + - 'tests/**' + - 'scripts/**' + - '.github/workflows/pack-tests.yml' + pull_request: + paths: + - 'packs/**' + - 'tests/**' + - 'scripts/**' + +permissions: + contents: read + +jobs: + discover: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.find.outputs.tests }} + has_tests: ${{ steps.find.outputs.has_tests }} + steps: + - uses: actions/checkout@v4 + + - name: Discover test scripts + id: find + run: | + # Find all test.sh files in packs/ and tests/ by naming convention + tests=$(find packs/*/test.sh tests/test-*.sh -type f 2>/dev/null | jq -Rsc 'split("\n") | map(select(length > 0))') + echo "tests=${tests}" >> "$GITHUB_OUTPUT" + if [[ "$tests" == "[]" ]]; then + echo "has_tests=false" >> "$GITHUB_OUTPUT" + echo "No test scripts found" + else + echo "has_tests=true" >> "$GITHUB_OUTPUT" + echo "Found tests: ${tests}" + fi + + test: + needs: discover + if: needs.discover.outputs.has_tests == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + test: ${{ fromJson(needs.discover.outputs.tests) }} + steps: + - uses: actions/checkout@v4 + + - name: Run ${{ matrix.test }} + run: | + chmod +x "${{ matrix.test }}" + bash "${{ matrix.test }}" + env: + CI: "true" From a5c01ef1d203d5fe109803815e5d6a2c4da1bb7d Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:02:38 +0000 Subject: [PATCH 021/172] improve: extract registry parser to standalone script, add 37 tests - Extract inline Python YAML parser from install.sh into scripts/parse-registry.py - Eliminates DRY violation: both pack listing and instance_type lookup now call the same script with different arguments (list-agents vs get) - Parser has full docstring explaining the state-machine approach - install.sh updated to call the external script instead of inline Python - Add tests/test-registry-parser.sh with 37 test cases covering: - Real registry parsing (all 4 agent packs discovered) - Experimental flag detection - Instance type lookup for all packs - Arbitrary key retrieval - Missing pack/key returns empty - Minimal fixtures, edge cases (empty file, no agents, missing fields) - Special chars in descriptions (dashes, parens, pipes) - Single-quoted YAML values - Malformed YAML (no crash) - CLI error handling (missing args, bad command) --- install.sh | 46 +----- scripts/parse-registry.py | 77 ++++++++++ tests/test-registry-parser.sh | 268 ++++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 41 deletions(-) create mode 100755 scripts/parse-registry.py create mode 100644 tests/test-registry-parser.sh diff --git a/install.sh b/install.sh index d4f0f51..de976b0 100755 --- a/install.sh +++ b/install.sh @@ -392,35 +392,14 @@ collect_config() { local -a pack_descs=() local -a pack_experimental=() - # Parse agent packs from registry.yaml (stdlib only — no PyYAML dependency) + # Parse agent packs from registry.yaml via scripts/parse-registry.py (stdlib only) + local parser="${CLONE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/scripts/parse-registry.py" while IFS='|' read -r pname pdesc pexp; do pack_names+=("$pname") pack_descs+=("$pdesc") pack_experimental+=("$pexp") - done < <([ -n "$registry" ] && python3 -c " -import re, sys -text = open(sys.argv[1]).read() -# Simple state-machine parser for the flat registry YAML structure -current_pack = None -packs = {} -for line in text.split('\n'): - # Top-level pack name (2-space indent under packs:) - m = re.match(r'^ (\w[\w-]*):\s*$', line) - if m: - current_pack = m.group(1) - packs[current_pack] = {} - continue - if current_pack: - # Key-value pairs (4-space indent) - kv = re.match(r'^ (\w[\w-]*):\s+(.+)$', line) - if kv: - packs[current_pack][kv.group(1)] = kv.group(2).strip().strip('\"').strip(\"'\") -for name, cfg in packs.items(): - if cfg.get('type') == 'agent': - desc = cfg.get('description', name) - exp = 'true' if cfg.get('experimental', '').lower() == 'true' else 'false' - print(f'{name}|{desc}|{exp}') -" "$registry" 2>/dev/null || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") + done < <([ -n "$registry" ] && python3 "$parser" "$registry" list-agents 2>/dev/null \ + || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") echo " Agent to deploy:" local i @@ -464,22 +443,7 @@ for name, cfg in packs.items(): # Adjust instance size default based on pack registry local default_size_choice="3" # default → t4g.xlarge local pack_instance_type - pack_instance_type=$([ -n "$registry" ] && python3 -c " -import re, sys -text = open(sys.argv[1]).read() -pack_name = sys.argv[2] -current_pack = None -for line in text.split('\n'): - m = re.match(r'^ (\w[\w-]*):\s*$', line) - if m: - current_pack = m.group(1) - continue - if current_pack == pack_name: - kv = re.match(r'^ instance_type:\s+(.+)$', line) - if kv: - print(kv.group(1).strip().strip('\"')) - break -" "$registry" "$PACK_NAME" 2>/dev/null || echo "t4g.xlarge") + pack_instance_type=$([ -n "$registry" ] && python3 "$parser" "$registry" get "$PACK_NAME" instance_type 2>/dev/null || echo "t4g.xlarge") case "$pack_instance_type" in t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; t4g.large) default_size_choice="2" ;; diff --git a/scripts/parse-registry.py b/scripts/parse-registry.py new file mode 100755 index 0000000..ceaecaf --- /dev/null +++ b/scripts/parse-registry.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Parse packs/registry.yaml without PyYAML (stdlib only). + +Usage: + parse-registry.py list-agents + Print pipe-delimited agent packs: name|description|experimental + + parse-registry.py get + Print a single value for a pack (e.g. instance_type, description) + +The parser is a simple state machine that handles the flat two-level YAML +structure used by registry.yaml (top-level keys under 'packs:' at 2-space +indent, key-value pairs at 4-space indent). It deliberately does NOT +handle arbitrary YAML — only the structure this project uses. +""" + +import re +import sys + + +def parse_registry(text): + """Return dict of {pack_name: {key: value, ...}} from registry YAML text.""" + current_pack = None + packs = {} + for line in text.split("\n"): + # Top-level pack name (2-space indent under packs:) + m = re.match(r"^ (\w[\w-]*):\s*$", line) + if m: + current_pack = m.group(1) + packs[current_pack] = {} + continue + if current_pack: + # Key-value pairs (4-space indent) + kv = re.match(r"^ (\w[\w-]*):\s+(.+)$", line) + if kv: + val = kv.group(2).strip().strip('"').strip("'") + packs[current_pack][kv.group(1)] = val + return packs + + +def list_agents(packs): + """Print pipe-delimited agent packs: name|description|experimental.""" + for name, cfg in packs.items(): + if cfg.get("type") == "agent": + desc = cfg.get("description", name) + exp = "true" if cfg.get("experimental", "").lower() == "true" else "false" + print(f"{name}|{desc}|{exp}") + + +def get_value(packs, pack_name, key): + """Print a single value for a pack, or empty string if missing.""" + cfg = packs.get(pack_name, {}) + print(cfg.get(key, "")) + + +def main(): + if len(sys.argv) < 3: + print(__doc__, file=sys.stderr) + sys.exit(1) + + registry_path = sys.argv[1] + command = sys.argv[2] + + text = open(registry_path).read() + packs = parse_registry(text) + + if command == "list-agents": + list_agents(packs) + elif command == "get" and len(sys.argv) >= 5: + get_value(packs, sys.argv[3], sys.argv[4]) + else: + print(f"Unknown command: {command}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh new file mode 100644 index 0000000..6e13775 --- /dev/null +++ b/tests/test-registry-parser.sh @@ -0,0 +1,268 @@ +#!/usr/bin/env bash +# tests/test-registry-parser.sh — tests for scripts/parse-registry.py +# Run: bash tests/test-registry-parser.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PARSER="${SCRIPT_DIR}/scripts/parse-registry.py" + +PASS=0 +FAIL=0 +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# ---- Assert helpers --------------------------------------------------------- +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo " ✓ $desc" + PASS=$((PASS + 1)) + else + echo " ✗ $desc" + echo " expected: $(printf '%q' "$expected")" + echo " actual: $(printf '%q' "$actual")" + FAIL=$((FAIL + 1)) + fi +} + +assert_contains() { + local desc="$1" needle="$2" haystack="$3" + if [[ "$haystack" == *"$needle"* ]]; then + echo " ✓ $desc" + PASS=$((PASS + 1)) + else + echo " ✗ $desc" + echo " expected to contain: $needle" + echo " actual: $haystack" + FAIL=$((FAIL + 1)) + fi +} + +assert_empty() { + local desc="$1" actual="$2" + if [[ -z "$actual" ]]; then + echo " ✓ $desc" + PASS=$((PASS + 1)) + else + echo " ✗ $desc" + echo " expected empty, got: $actual" + FAIL=$((FAIL + 1)) + fi +} + +assert_line_count() { + local desc="$1" expected="$2" actual_text="$3" + local count + if [[ -z "$actual_text" ]]; then + count=0 + else + count=$(echo "$actual_text" | wc -l | tr -d ' ') + fi + assert_eq "$desc" "$expected" "$count" +} + +assert_exit_nonzero() { + local desc="$1"; shift + if "$@" >/dev/null 2>&1; then + echo " ✗ $desc (expected non-zero exit, got 0)" + FAIL=$((FAIL + 1)) + else + echo " ✓ $desc" + PASS=$((PASS + 1)) + fi +} + +# ---- Fixtures --------------------------------------------------------------- +REAL_REGISTRY="${SCRIPT_DIR}/packs/registry.yaml" + +cat > "$TMPDIR/minimal.yaml" << 'EOF' +version: 1 + +packs: + mybase: + type: base + description: "A base pack" + + alpha: + type: agent + description: "Alpha agent" + instance_type: t4g.xlarge + experimental: false + + beta: + type: agent + description: "Beta — test agent (with parens)" + instance_type: t4g.medium + experimental: true +EOF + +cat > "$TMPDIR/empty.yaml" << 'EOF' +version: 1 +packs: +EOF + +cat > "$TMPDIR/no-agents.yaml" << 'EOF' +version: 1 +packs: + mybase: + type: base + description: "Only base packs here" +EOF + +cat > "$TMPDIR/malformed.yaml" << 'EOF' +this is not valid yaml at all + random indentation: + but no real structure +key without value: + nested: + type: agent +EOF + +cat > "$TMPDIR/missing-fields.yaml" << 'EOF' +version: 1 +packs: + bare: + type: agent + + partial: + type: agent + description: "Has desc but no instance_type" + experimental: true +EOF + +cat > "$TMPDIR/special-chars.yaml" << 'EOF' +version: 1 +packs: + fancy: + type: agent + description: "Fancy — agent with dashes, parens (v2), and pipes" + instance_type: t4g.large + experimental: false +EOF + +cat > "$TMPDIR/single-quotes.yaml" << 'EOF' +version: 1 +packs: + quoted: + type: agent + description: 'Single-quoted description' + instance_type: 't4g.medium' + experimental: 'true' +EOF + +# ---- Tests ------------------------------------------------------------------ + +echo "" +echo "=== Test: real registry.yaml (4 agent packs) ===" +output=$(python3 "$PARSER" "$REAL_REGISTRY" list-agents) +assert_line_count "lists exactly 4 agents" "4" "$output" +assert_contains "includes openclaw" "openclaw|" "$output" +assert_contains "includes hermes" "hermes|" "$output" +assert_contains "includes pi" "pi|" "$output" +assert_contains "includes ironclaw" "ironclaw|" "$output" + +# Verify bedrockify (type: base) is excluded +line_count_with_bedrockify=$(echo "$output" | grep -c '^bedrockify|' || true) +assert_eq "excludes base packs (bedrockify)" "0" "$line_count_with_bedrockify" + +echo "" +echo "=== Test: experimental flag detection ===" +output=$(python3 "$PARSER" "$REAL_REGISTRY" list-agents) +openclaw_line=$(echo "$output" | grep '^openclaw|') +hermes_line=$(echo "$output" | grep '^hermes|') +pi_line=$(echo "$output" | grep '^pi|') +ironclaw_line=$(echo "$output" | grep '^ironclaw|') +assert_contains "openclaw is not experimental" "|false" "$openclaw_line" +assert_contains "hermes is not experimental" "|false" "$hermes_line" +assert_contains "pi is experimental" "|true" "$pi_line" +assert_contains "ironclaw is experimental" "|true" "$ironclaw_line" + +echo "" +echo "=== Test: instance_type lookup ===" +assert_eq "openclaw → t4g.xlarge" "t4g.xlarge" "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw instance_type)" +assert_eq "hermes → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get hermes instance_type)" +assert_eq "pi → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get pi instance_type)" +assert_eq "ironclaw → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get ironclaw instance_type)" + +echo "" +echo "=== Test: get arbitrary keys ===" +assert_eq "openclaw description" "OpenClaw — stateful AI agent with persistent gateway" \ + "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw description)" +assert_eq "openclaw default_model" "us.anthropic.claude-opus-4-6-v1" \ + "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw default_model)" +assert_eq "openclaw brain" "true" \ + "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw brain)" + +echo "" +echo "=== Test: nonexistent pack/key returns empty ===" +assert_empty "nonexistent pack" "$(python3 "$PARSER" "$REAL_REGISTRY" get nonexistent instance_type)" +assert_empty "nonexistent key" "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw nonexistent_key)" + +echo "" +echo "=== Test: minimal fixture (2 agents, 1 base) ===" +output=$(python3 "$PARSER" "$TMPDIR/minimal.yaml" list-agents) +assert_line_count "lists exactly 2 agents" "2" "$output" +assert_contains "alpha is listed" "alpha|Alpha agent|false" "$output" +assert_contains "beta is listed" "beta|" "$output" +assert_contains "beta is experimental" "|true" "$(echo "$output" | grep beta)" +assert_eq "beta instance_type" "t4g.medium" "$(python3 "$PARSER" "$TMPDIR/minimal.yaml" get beta instance_type)" + +echo "" +echo "=== Test: descriptions with special chars ===" +desc=$(python3 "$PARSER" "$TMPDIR/special-chars.yaml" get fancy description) +assert_eq "description with dashes, parens, and pipes" \ + "Fancy — agent with dashes, parens (v2), and pipes" "$desc" + +# Test the pipe-delimited output isn't broken by the pipe in description +output=$(python3 "$PARSER" "$TMPDIR/special-chars.yaml" list-agents) +# The pipe in the description will cause IFS='|' to split wrong — this is a known +# limitation of the pipe-delimited format. But 'get' command should work fine. +assert_contains "special-chars agent listed" "fancy|" "$output" + +echo "" +echo "=== Test: single-quoted values ===" +output=$(python3 "$PARSER" "$TMPDIR/single-quotes.yaml" list-agents) +assert_contains "single-quoted description stripped" "Single-quoted description" "$output" +assert_eq "single-quoted instance_type" "t4g.medium" \ + "$(python3 "$PARSER" "$TMPDIR/single-quotes.yaml" get quoted instance_type)" + +echo "" +echo "=== Test: empty registry (no packs defined) ===" +output=$(python3 "$PARSER" "$TMPDIR/empty.yaml" list-agents) +assert_empty "no output for empty registry" "$output" + +echo "" +echo "=== Test: no agent packs (only base) ===" +output=$(python3 "$PARSER" "$TMPDIR/no-agents.yaml" list-agents) +assert_empty "no output when only base packs" "$output" + +echo "" +echo "=== Test: missing fields (bare pack with only type) ===" +output=$(python3 "$PARSER" "$TMPDIR/missing-fields.yaml" list-agents) +assert_line_count "2 agents even with missing fields" "2" "$output" +# bare pack should use its name as description +assert_contains "bare pack uses name as desc fallback" "bare|bare|false" "$output" +assert_contains "partial pack has description" "partial|Has desc" "$output" +assert_empty "bare pack has no instance_type" \ + "$(python3 "$PARSER" "$TMPDIR/missing-fields.yaml" get bare instance_type)" + +echo "" +echo "=== Test: malformed YAML (no crash) ===" +output=$(python3 "$PARSER" "$TMPDIR/malformed.yaml" list-agents 2>&1) +# Should produce some output or empty, but not crash +# The "nested" block has type: agent but that's at wrong indent — +# with the regex parser it might match or not, but shouldn't crash +echo " ✓ parser did not crash on malformed input" +PASS=$((PASS + 1)) + +echo "" +echo "=== Test: CLI error handling ===" +assert_exit_nonzero "missing args exits non-zero" python3 "$PARSER" +assert_exit_nonzero "bad command exits non-zero" python3 "$PARSER" "$REAL_REGISTRY" bad-command + +echo "" +echo "================================================================" +echo "Results: $PASS passed, $FAIL failed" +echo "================================================================" + +[[ $FAIL -eq 0 ]] && exit 0 || exit 1 From 7dc4f3b9c7d1264e062f0518d38751dc2ed224e3 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:09:22 +0000 Subject: [PATCH 022/172] =?UTF-8?q?refactor:=20remove=20python3=20dependen?= =?UTF-8?q?cy=20=E2=80=94=20use=20jq=20+=20registry.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Converted registry.yaml → registry.json - json_field() and url_encode() now use jq instead of python3 - Registry parser replaced: Python regex script → 3-line jq queries - Removed scripts/parse-registry.py (no longer needed) - Terraform unzip fallback: busybox/jar instead of python3 - Preflight: require jq instead of python3 - Tests rewritten for jq-based parsing (31 pass) python3 is no longer needed on the user's machine. Dependencies: bash, curl, aws-cli, jq. --- install.sh | 39 +++-- packs/registry.json | 71 ++++++++ scripts/parse-registry.py | 77 --------- tests/test-registry-parser.sh | 317 +++++++++++++--------------------- 4 files changed, 209 insertions(+), 295 deletions(-) create mode 100644 packs/registry.json delete mode 100755 scripts/parse-registry.py diff --git a/install.sh b/install.sh index de976b0..22bc291 100755 --- a/install.sh +++ b/install.sh @@ -91,10 +91,10 @@ require_cmd() { command -v "$1" &>/dev/null || fail "$2"; } confirm_or_abort() { confirm "$@" || { echo "Aborted."; exit 0; }; } # Extract a key from JSON on stdin -json_field() { python3 -c "import json,sys; print(json.load(sys.stdin)[sys.argv[1]])" "$1"; } +json_field() { jq -r ".$1" 2>/dev/null; } # URL-encode a string -url_encode() { python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$1"; } +url_encode() { jq -rn --arg s "$1" '$s | @uri'; } # Verify AWS credentials with specific error messages. # On success, sets ACCOUNT_ID and CALLER_ARN from a single STS call. @@ -211,7 +211,7 @@ preflight_checks() { require_cmd aws "AWS CLI not found. Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" ok "AWS CLI: $(aws --version 2>&1 | head -1)" - require_cmd python3 "Python 3 is required but not found. Install it from your package manager." + require_cmd jq "jq is required but not found. Install: https://jqlang.github.io/jq/download/" verify_aws_credentials # ACCOUNT_ID and CALLER_ARN are now set by verify_aws_credentials (single STS call) @@ -379,27 +379,30 @@ collect_config() { info "Configuration" echo "" - # ---- Pack selection (dynamically discovered from registry.yaml) ----------- + # ---- Pack selection (dynamically discovered from registry.json) ----------- # CLONE_DIR may not be set yet (repo is cloned after config collection). # If the local file isn't available, fetch from GitHub. - local registry="${CLONE_DIR:-}/packs/registry.yaml" + local registry="${CLONE_DIR:-}/packs/registry.json" if [[ ! -f "$registry" ]]; then - local registry_url="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/packs/registry.yaml" - registry="/tmp/loki-registry-$$.yaml" + local registry_url="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/packs/registry.json" + registry="/tmp/loki-registry-$$.json" curl -sfL "$registry_url" -o "$registry" 2>/dev/null || registry="" fi local -a pack_names=() local -a pack_descs=() local -a pack_experimental=() - # Parse agent packs from registry.yaml via scripts/parse-registry.py (stdlib only) - local parser="${CLONE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/scripts/parse-registry.py" + # Parse agent packs from registry.json via jq while IFS='|' read -r pname pdesc pexp; do pack_names+=("$pname") pack_descs+=("$pdesc") pack_experimental+=("$pexp") - done < <([ -n "$registry" ] && python3 "$parser" "$registry" list-agents 2>/dev/null \ - || echo "openclaw|OpenClaw — stateful AI agent with persistent gateway|false") + done < <([ -n "$registry" ] && jq -r ' + .packs | to_entries[] + | select(.value.type == "agent") + | "\(.key)|\(.value.description // .key)|\(if .value.experimental then "true" else "false" end)" + ' "$registry" 2>/dev/null \ + || echo "openclaw|OpenClaw -- stateful AI agent with persistent gateway|false") echo " Agent to deploy:" local i @@ -443,7 +446,7 @@ collect_config() { # Adjust instance size default based on pack registry local default_size_choice="3" # default → t4g.xlarge local pack_instance_type - pack_instance_type=$([ -n "$registry" ] && python3 "$parser" "$registry" get "$PACK_NAME" instance_type 2>/dev/null || echo "t4g.xlarge") + pack_instance_type=$([ -n "$registry" ] && jq -r --arg p "$PACK_NAME" '.packs[$p].instance_type // "t4g.xlarge"' "$registry" 2>/dev/null || echo "t4g.xlarge") case "$pack_instance_type" in t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; t4g.large) default_size_choice="2" ;; @@ -781,16 +784,16 @@ install_terraform() { info "Downloading Terraform ${version} (${os}/${arch})..." curl -sfL "$zip_url" -o "$tmp_zip" || fail "Failed to download Terraform from ${zip_url}" - # Unzip — use python3 if unzip not available (CloudShell may not have it) + # Unzip — use busybox or jar as fallback if unzip not available (CloudShell may not have it) mkdir -p "$install_dir" if command -v unzip &>/dev/null; then unzip -o -q "$tmp_zip" -d "$install_dir" + elif command -v busybox &>/dev/null; then + busybox unzip -o -q "$tmp_zip" -d "$install_dir" + elif command -v jar &>/dev/null; then + (cd "$install_dir" && jar xf "$tmp_zip") else - python3 -c " -import zipfile, sys -with zipfile.ZipFile(sys.argv[1]) as z: - z.extractall(sys.argv[2]) -" "$tmp_zip" "$install_dir" + fail "Cannot extract terraform zip — install 'unzip': sudo yum install -y unzip (or sudo apt install unzip)" fi chmod +x "${install_dir}/terraform" diff --git a/packs/registry.json b/packs/registry.json new file mode 100644 index 0000000..2b0a239 --- /dev/null +++ b/packs/registry.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "defaults": { + "ami_filter": "al2023-ami-*-arm64", + "arch": "arm64", + "os": "al2023", + "instance_type": "t4g.xlarge", + "root_volume_gb": 40, + "data_volume_gb": 80, + "bedrock_region": "us-east-1" + }, + "packs": { + "bedrockify": { + "type": "base", + "description": "OpenAI-compatible Bedrock proxy (systemd daemon on localhost)", + "order": 0 + }, + "openclaw": { + "type": "agent", + "description": "OpenClaw -- stateful AI agent with persistent gateway", + "deps": ["bedrockify"], + "instance_type": "t4g.xlarge", + "root_volume_gb": 40, + "data_volume_gb": 80, + "default_model": "us.anthropic.claude-opus-4-6-v1", + "ports": { "gateway": 3001 }, + "brain": true, + "claude_code": true, + "experimental": false + }, + "hermes": { + "type": "agent", + "description": "Hermes -- NousResearch CLI agent via bedrockify", + "deps": ["bedrockify"], + "instance_type": "t4g.medium", + "root_volume_gb": 40, + "data_volume_gb": 0, + "default_model": "anthropic/claude-opus-4.6", + "ports": {}, + "brain": false, + "claude_code": false, + "experimental": false + }, + "pi": { + "type": "agent", + "description": "Pi -- minimal terminal coding harness (experimental)", + "deps": ["bedrockify"], + "instance_type": "t4g.medium", + "root_volume_gb": 40, + "data_volume_gb": 0, + "default_model": "us.anthropic.claude-sonnet-4-6-v1", + "ports": {}, + "brain": false, + "claude_code": false, + "experimental": true + }, + "ironclaw": { + "type": "agent", + "description": "IronClaw -- Rust-based AI agent by NEAR AI (experimental)", + "deps": ["bedrockify"], + "instance_type": "t4g.medium", + "root_volume_gb": 40, + "data_volume_gb": 0, + "default_model": "us.anthropic.claude-sonnet-4-6-v1", + "ports": {}, + "brain": false, + "claude_code": false, + "experimental": true + } + } +} diff --git a/scripts/parse-registry.py b/scripts/parse-registry.py deleted file mode 100755 index ceaecaf..0000000 --- a/scripts/parse-registry.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -"""Parse packs/registry.yaml without PyYAML (stdlib only). - -Usage: - parse-registry.py list-agents - Print pipe-delimited agent packs: name|description|experimental - - parse-registry.py get - Print a single value for a pack (e.g. instance_type, description) - -The parser is a simple state machine that handles the flat two-level YAML -structure used by registry.yaml (top-level keys under 'packs:' at 2-space -indent, key-value pairs at 4-space indent). It deliberately does NOT -handle arbitrary YAML — only the structure this project uses. -""" - -import re -import sys - - -def parse_registry(text): - """Return dict of {pack_name: {key: value, ...}} from registry YAML text.""" - current_pack = None - packs = {} - for line in text.split("\n"): - # Top-level pack name (2-space indent under packs:) - m = re.match(r"^ (\w[\w-]*):\s*$", line) - if m: - current_pack = m.group(1) - packs[current_pack] = {} - continue - if current_pack: - # Key-value pairs (4-space indent) - kv = re.match(r"^ (\w[\w-]*):\s+(.+)$", line) - if kv: - val = kv.group(2).strip().strip('"').strip("'") - packs[current_pack][kv.group(1)] = val - return packs - - -def list_agents(packs): - """Print pipe-delimited agent packs: name|description|experimental.""" - for name, cfg in packs.items(): - if cfg.get("type") == "agent": - desc = cfg.get("description", name) - exp = "true" if cfg.get("experimental", "").lower() == "true" else "false" - print(f"{name}|{desc}|{exp}") - - -def get_value(packs, pack_name, key): - """Print a single value for a pack, or empty string if missing.""" - cfg = packs.get(pack_name, {}) - print(cfg.get(key, "")) - - -def main(): - if len(sys.argv) < 3: - print(__doc__, file=sys.stderr) - sys.exit(1) - - registry_path = sys.argv[1] - command = sys.argv[2] - - text = open(registry_path).read() - packs = parse_registry(text) - - if command == "list-agents": - list_agents(packs) - elif command == "get" and len(sys.argv) >= 5: - get_value(packs, sys.argv[3], sys.argv[4]) - else: - print(f"Unknown command: {command}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh index 6e13775..8a5d730 100644 --- a/tests/test-registry-parser.sh +++ b/tests/test-registry-parser.sh @@ -1,268 +1,185 @@ #!/usr/bin/env bash -# tests/test-registry-parser.sh — tests for scripts/parse-registry.py +# tests/test-registry-parser.sh — tests for registry.json + jq parsing # Run: bash tests/test-registry-parser.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -PARSER="${SCRIPT_DIR}/scripts/parse-registry.py" +REGISTRY="${SCRIPT_DIR}/packs/registry.json" PASS=0 FAIL=0 TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT -# ---- Assert helpers --------------------------------------------------------- +# ---- Helpers ---------------------------------------------------------------- assert_eq() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" == "$actual" ]]; then - echo " ✓ $desc" - PASS=$((PASS + 1)) + echo " ✓ $desc"; PASS=$((PASS + 1)) else - echo " ✗ $desc" - echo " expected: $(printf '%q' "$expected")" - echo " actual: $(printf '%q' "$actual")" - FAIL=$((FAIL + 1)) + echo " ✗ $desc"; echo " expected: $expected"; echo " actual: $actual"; FAIL=$((FAIL + 1)) fi } assert_contains() { local desc="$1" needle="$2" haystack="$3" if [[ "$haystack" == *"$needle"* ]]; then - echo " ✓ $desc" - PASS=$((PASS + 1)) + echo " ✓ $desc"; PASS=$((PASS + 1)) else - echo " ✗ $desc" - echo " expected to contain: $needle" - echo " actual: $haystack" - FAIL=$((FAIL + 1)) + echo " ✗ $desc"; echo " missing: $needle"; FAIL=$((FAIL + 1)) fi } -assert_empty() { - local desc="$1" actual="$2" - if [[ -z "$actual" ]]; then - echo " ✓ $desc" - PASS=$((PASS + 1)) +assert_not_contains() { + local desc="$1" needle="$2" haystack="$3" + if [[ "$haystack" != *"$needle"* ]]; then + echo " ✓ $desc"; PASS=$((PASS + 1)) else - echo " ✗ $desc" - echo " expected empty, got: $actual" - FAIL=$((FAIL + 1)) + echo " ✗ $desc"; echo " should not contain: $needle"; FAIL=$((FAIL + 1)) fi } -assert_line_count() { - local desc="$1" expected="$2" actual_text="$3" +assert_count() { + local desc="$1" expected="$2" actual="$3" local count - if [[ -z "$actual_text" ]]; then - count=0 + count=$(echo "$actual" | grep -c . || true) + if [[ "$count" -eq "$expected" ]]; then + echo " ✓ $desc (count=$count)"; PASS=$((PASS + 1)) else - count=$(echo "$actual_text" | wc -l | tr -d ' ') + echo " ✗ $desc"; echo " expected count: $expected, got: $count"; FAIL=$((FAIL + 1)) fi - assert_eq "$desc" "$expected" "$count" } -assert_exit_nonzero() { - local desc="$1"; shift - if "$@" >/dev/null 2>&1; then - echo " ✗ $desc (expected non-zero exit, got 0)" - FAIL=$((FAIL + 1)) - else - echo " ✓ $desc" - PASS=$((PASS + 1)) - fi +# jq query that mirrors what install.sh does for pack listing +list_agents() { + local file="$1" + jq -r '.packs | to_entries[] | select(.value.type == "agent") | "\(.key)|\(.value.description // .key)|\(if .value.experimental then "true" else "false" end)"' "$file" 2>/dev/null } -# ---- Fixtures --------------------------------------------------------------- -REAL_REGISTRY="${SCRIPT_DIR}/packs/registry.yaml" - -cat > "$TMPDIR/minimal.yaml" << 'EOF' -version: 1 - -packs: - mybase: - type: base - description: "A base pack" - - alpha: - type: agent - description: "Alpha agent" - instance_type: t4g.xlarge - experimental: false - - beta: - type: agent - description: "Beta — test agent (with parens)" - instance_type: t4g.medium - experimental: true -EOF - -cat > "$TMPDIR/empty.yaml" << 'EOF' -version: 1 -packs: -EOF - -cat > "$TMPDIR/no-agents.yaml" << 'EOF' -version: 1 -packs: - mybase: - type: base - description: "Only base packs here" -EOF - -cat > "$TMPDIR/malformed.yaml" << 'EOF' -this is not valid yaml at all - random indentation: - but no real structure -key without value: - nested: - type: agent -EOF - -cat > "$TMPDIR/missing-fields.yaml" << 'EOF' -version: 1 -packs: - bare: - type: agent - - partial: - type: agent - description: "Has desc but no instance_type" - experimental: true -EOF - -cat > "$TMPDIR/special-chars.yaml" << 'EOF' -version: 1 -packs: - fancy: - type: agent - description: "Fancy — agent with dashes, parens (v2), and pipes" - instance_type: t4g.large - experimental: false -EOF - -cat > "$TMPDIR/single-quotes.yaml" << 'EOF' -version: 1 -packs: - quoted: - type: agent - description: 'Single-quoted description' - instance_type: 't4g.medium' - experimental: 'true' -EOF - -# ---- Tests ------------------------------------------------------------------ +# jq query that mirrors what install.sh does for key lookup +get_value() { + local file="$1" pack="$2" key="$3" + jq -r --arg p "$pack" --arg k "$key" '.packs[$p][$k] // empty' "$file" 2>/dev/null +} -echo "" -echo "=== Test: real registry.yaml (4 agent packs) ===" -output=$(python3 "$PARSER" "$REAL_REGISTRY" list-agents) -assert_line_count "lists exactly 4 agents" "4" "$output" +# ---- Test: real registry.json ----------------------------------------------- +echo "=== Test: real registry.json (4 agent packs) ===" +output=$(list_agents "$REGISTRY") +assert_count "lists exactly 4 agents" 4 "$output" assert_contains "includes openclaw" "openclaw|" "$output" assert_contains "includes hermes" "hermes|" "$output" assert_contains "includes pi" "pi|" "$output" assert_contains "includes ironclaw" "ironclaw|" "$output" +bedrockify_as_pack=$(echo "$output" | grep -c '^bedrockify|' || true) +assert_eq "excludes base packs (bedrockify)" "0" "$bedrockify_as_pack" -# Verify bedrockify (type: base) is excluded -line_count_with_bedrockify=$(echo "$output" | grep -c '^bedrockify|' || true) -assert_eq "excludes base packs (bedrockify)" "0" "$line_count_with_bedrockify" - +# ---- Test: experimental flag ------------------------------------------------ echo "" echo "=== Test: experimental flag detection ===" -output=$(python3 "$PARSER" "$REAL_REGISTRY" list-agents) -openclaw_line=$(echo "$output" | grep '^openclaw|') -hermes_line=$(echo "$output" | grep '^hermes|') -pi_line=$(echo "$output" | grep '^pi|') -ironclaw_line=$(echo "$output" | grep '^ironclaw|') -assert_contains "openclaw is not experimental" "|false" "$openclaw_line" -assert_contains "hermes is not experimental" "|false" "$hermes_line" -assert_contains "pi is experimental" "|true" "$pi_line" -assert_contains "ironclaw is experimental" "|true" "$ironclaw_line" +assert_contains "openclaw is not experimental" "openclaw|OpenClaw" "$output" +assert_contains "openclaw experimental=false" "|false" "$(echo "$output" | grep openclaw)" +assert_contains "pi is experimental" "|true" "$(echo "$output" | grep '^pi|')" +assert_contains "ironclaw is experimental" "|true" "$(echo "$output" | grep ironclaw)" +# ---- Test: instance_type lookup --------------------------------------------- echo "" echo "=== Test: instance_type lookup ===" -assert_eq "openclaw → t4g.xlarge" "t4g.xlarge" "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw instance_type)" -assert_eq "hermes → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get hermes instance_type)" -assert_eq "pi → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get pi instance_type)" -assert_eq "ironclaw → t4g.medium" "t4g.medium" "$(python3 "$PARSER" "$REAL_REGISTRY" get ironclaw instance_type)" +assert_eq "openclaw → t4g.xlarge" "t4g.xlarge" "$(get_value "$REGISTRY" openclaw instance_type)" +assert_eq "hermes → t4g.medium" "t4g.medium" "$(get_value "$REGISTRY" hermes instance_type)" +assert_eq "pi → t4g.medium" "t4g.medium" "$(get_value "$REGISTRY" pi instance_type)" +assert_eq "ironclaw → t4g.medium" "t4g.medium" "$(get_value "$REGISTRY" ironclaw instance_type)" +# ---- Test: get arbitrary keys ----------------------------------------------- echo "" echo "=== Test: get arbitrary keys ===" -assert_eq "openclaw description" "OpenClaw — stateful AI agent with persistent gateway" \ - "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw description)" -assert_eq "openclaw default_model" "us.anthropic.claude-opus-4-6-v1" \ - "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw default_model)" -assert_eq "openclaw brain" "true" \ - "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw brain)" - -echo "" -echo "=== Test: nonexistent pack/key returns empty ===" -assert_empty "nonexistent pack" "$(python3 "$PARSER" "$REAL_REGISTRY" get nonexistent instance_type)" -assert_empty "nonexistent key" "$(python3 "$PARSER" "$REAL_REGISTRY" get openclaw nonexistent_key)" +assert_contains "openclaw description" "OpenClaw" "$(get_value "$REGISTRY" openclaw description)" +assert_eq "openclaw default_model" "us.anthropic.claude-opus-4-6-v1" "$(get_value "$REGISTRY" openclaw default_model)" +assert_eq "openclaw brain" "true" "$(get_value "$REGISTRY" openclaw brain)" +# ---- Test: nonexistent pack/key returns empty ------------------------------- echo "" -echo "=== Test: minimal fixture (2 agents, 1 base) ===" -output=$(python3 "$PARSER" "$TMPDIR/minimal.yaml" list-agents) -assert_line_count "lists exactly 2 agents" "2" "$output" -assert_contains "alpha is listed" "alpha|Alpha agent|false" "$output" -assert_contains "beta is listed" "beta|" "$output" -assert_contains "beta is experimental" "|true" "$(echo "$output" | grep beta)" -assert_eq "beta instance_type" "t4g.medium" "$(python3 "$PARSER" "$TMPDIR/minimal.yaml" get beta instance_type)" +echo "=== Test: nonexistent pack/key ===" +assert_eq "nonexistent pack" "" "$(get_value "$REGISTRY" doesnotexist instance_type)" +assert_eq "nonexistent key" "" "$(get_value "$REGISTRY" openclaw doesnotexist)" +# ---- Test: fixture with special chars --------------------------------------- echo "" -echo "=== Test: descriptions with special chars ===" -desc=$(python3 "$PARSER" "$TMPDIR/special-chars.yaml" get fancy description) -assert_eq "description with dashes, parens, and pipes" \ - "Fancy — agent with dashes, parens (v2), and pipes" "$desc" - -# Test the pipe-delimited output isn't broken by the pipe in description -output=$(python3 "$PARSER" "$TMPDIR/special-chars.yaml" list-agents) -# The pipe in the description will cause IFS='|' to split wrong — this is a known -# limitation of the pipe-delimited format. But 'get' command should work fine. -assert_contains "special-chars agent listed" "fancy|" "$output" - -echo "" -echo "=== Test: single-quoted values ===" -output=$(python3 "$PARSER" "$TMPDIR/single-quotes.yaml" list-agents) -assert_contains "single-quoted description stripped" "Single-quoted description" "$output" -assert_eq "single-quoted instance_type" "t4g.medium" \ - "$(python3 "$PARSER" "$TMPDIR/single-quotes.yaml" get quoted instance_type)" +echo "=== Test: fixture with special chars ===" +cat > "$TMPDIR/special.json" <<'EOF' +{ + "packs": { + "base": { "type": "base", "description": "not an agent" }, + "alpha": { + "type": "agent", + "description": "Alpha -- has dashes, (parens), and pipes | in desc", + "experimental": false, + "instance_type": "t4g.large" + }, + "beta": { + "type": "agent", + "description": "Beta (experimental)", + "experimental": true, + "instance_type": "t4g.small" + } + } +} +EOF +special_output=$(list_agents "$TMPDIR/special.json") +assert_count "lists 2 agents from fixture" 2 "$special_output" +assert_contains "alpha is listed" "alpha|" "$special_output" +assert_contains "beta is experimental" "beta|Beta (experimental)|true" "$special_output" +assert_eq "alpha instance_type" "t4g.large" "$(get_value "$TMPDIR/special.json" alpha instance_type)" +# ---- Test: empty registry --------------------------------------------------- echo "" -echo "=== Test: empty registry (no packs defined) ===" -output=$(python3 "$PARSER" "$TMPDIR/empty.yaml" list-agents) -assert_empty "no output for empty registry" "$output" +echo "=== Test: empty registry ===" +echo '{"packs":{}}' > "$TMPDIR/empty.json" +empty_output=$(list_agents "$TMPDIR/empty.json") +assert_eq "no output for empty registry" "" "$empty_output" +# ---- Test: no agent packs --------------------------------------------------- echo "" echo "=== Test: no agent packs (only base) ===" -output=$(python3 "$PARSER" "$TMPDIR/no-agents.yaml" list-agents) -assert_empty "no output when only base packs" "$output" +echo '{"packs":{"base":{"type":"base"}}}' > "$TMPDIR/base-only.json" +base_output=$(list_agents "$TMPDIR/base-only.json") +assert_eq "no output when only base packs" "" "$base_output" +# ---- Test: missing fields --------------------------------------------------- echo "" -echo "=== Test: missing fields (bare pack with only type) ===" -output=$(python3 "$PARSER" "$TMPDIR/missing-fields.yaml" list-agents) -assert_line_count "2 agents even with missing fields" "2" "$output" -# bare pack should use its name as description -assert_contains "bare pack uses name as desc fallback" "bare|bare|false" "$output" -assert_contains "partial pack has description" "partial|Has desc" "$output" -assert_empty "bare pack has no instance_type" \ - "$(python3 "$PARSER" "$TMPDIR/missing-fields.yaml" get bare instance_type)" +echo "=== Test: missing fields ===" +cat > "$TMPDIR/minimal.json" <<'EOF' +{ + "packs": { + "bare": { "type": "agent" }, + "partial": { "type": "agent", "description": "has desc", "instance_type": "t4g.nano" } + } +} +EOF +minimal_output=$(list_agents "$TMPDIR/minimal.json") +assert_count "2 agents from minimal fixture" 2 "$minimal_output" +assert_contains "bare uses key as desc fallback" "bare|bare|false" "$minimal_output" +assert_eq "partial instance_type" "t4g.nano" "$(get_value "$TMPDIR/minimal.json" partial instance_type)" +assert_eq "bare has no instance_type" "" "$(get_value "$TMPDIR/minimal.json" bare instance_type)" +# ---- Test: malformed JSON --------------------------------------------------- echo "" -echo "=== Test: malformed YAML (no crash) ===" -output=$(python3 "$PARSER" "$TMPDIR/malformed.yaml" list-agents 2>&1) -# Should produce some output or empty, but not crash -# The "nested" block has type: agent but that's at wrong indent — -# with the regex parser it might match or not, but shouldn't crash -echo " ✓ parser did not crash on malformed input" -PASS=$((PASS + 1)) +echo "=== Test: malformed JSON (no crash) ===" +echo "not json at all" > "$TMPDIR/bad.json" +bad_output=$(list_agents "$TMPDIR/bad.json" || true) +assert_eq "parser returns empty on malformed input" "" "$bad_output" +# ---- Test: registry.json is valid JSON -------------------------------------- echo "" -echo "=== Test: CLI error handling ===" -assert_exit_nonzero "missing args exits non-zero" python3 "$PARSER" -assert_exit_nonzero "bad command exits non-zero" python3 "$PARSER" "$REAL_REGISTRY" bad-command - +echo "=== Test: registry.json validity ===" +if jq empty "$REGISTRY" 2>/dev/null; then + echo " ✓ registry.json is valid JSON"; PASS=$((PASS + 1)) +else + echo " ✗ registry.json is NOT valid JSON"; FAIL=$((FAIL + 1)) +fi + +# ---- Results ---------------------------------------------------------------- echo "" echo "================================================================" -echo "Results: $PASS passed, $FAIL failed" +echo "Results: ${PASS} passed, ${FAIL} failed" echo "================================================================" - -[[ $FAIL -eq 0 ]] && exit 0 || exit 1 +[[ $FAIL -eq 0 ]] || exit 1 From 86f50648c90494562a9a37cabfd067dfb484335c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 23:16:55 +0000 Subject: [PATCH 023/172] ci: bump version to 0.5.9 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 22bc291..635d799 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.8" +INSTALLER_VERSION="0.5.9" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From d34152098a2574f53e2d2d250246dae25fff4b24 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:22:38 +0000 Subject: [PATCH 024/172] =?UTF-8?q?ci:=20bump=20actions/checkout=20v4=20?= =?UTF-8?q?=E2=86=92=20v5=20(Node.js=2020=20deprecation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pack-tests.yml | 4 ++-- .github/workflows/secret-scan.yml | 2 +- .github/workflows/stamp-version.yml | 2 +- .github/workflows/version-bump.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pack-tests.yml b/.github/workflows/pack-tests.yml index 0434556..dd2966e 100644 --- a/.github/workflows/pack-tests.yml +++ b/.github/workflows/pack-tests.yml @@ -24,7 +24,7 @@ jobs: tests: ${{ steps.find.outputs.tests }} has_tests: ${{ steps.find.outputs.has_tests }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Discover test scripts id: find @@ -49,7 +49,7 @@ jobs: matrix: test: ${{ fromJson(needs.discover.outputs.tests) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run ${{ matrix.test }} run: | diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 2d433e9..1ceba54 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -10,7 +10,7 @@ jobs: git-secrets: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/stamp-version.yml b/.github/workflows/stamp-version.yml index 8d06731..e4a91a6 100644 --- a/.github/workflows/stamp-version.yml +++ b/.github/workflows/stamp-version.yml @@ -11,7 +11,7 @@ jobs: stamp: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index e634dd3..78be769 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -15,7 +15,7 @@ jobs: # Skip if this commit was made by the bot (prevent infinite loop) if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} From 1e45ef0cd0f7c140e3683fc6ae8441e682af228c Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:28:17 +0000 Subject: [PATCH 025/172] docs: add Pi + IronClaw to README, mark Hermes as experimental - Pack table updated with all 4 agent packs - Hermes marked experimental in registry.json and registry.yaml - Available Packs section updated with Pi and IronClaw descriptions --- README.md | 10 +++++++--- packs/registry.json | 4 ++-- packs/registry.yaml | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 19ecafb..939787f 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ Run the install command from the TL;DR above. The installer verifies AWS permiss | Pack | Description | Instance | Data Volume | |------|-------------|----------|-------------| | **OpenClaw** (default) | Stateful AI agent with 24/7 gateway, persistent memory, Telegram/Discord/Slack | t4g.xlarge recommended | 80GB | -| **Hermes** | NousResearch CLI agent — lighter, terminal-focused | t4g.medium sufficient | None needed (set to 0) | +| **Hermes** *(experimental)* | NousResearch CLI agent — lighter, terminal-focused, self-improving skills | t4g.medium sufficient | None needed (set to 0) | +| **Pi** *(experimental)* | Minimal terminal coding harness — read, write, edit, bash tools | t4g.medium sufficient | None needed (set to 0) | +| **IronClaw** *(experimental)* | Rust-based AI agent by NEAR AI — static binary, fast startup | t4g.medium sufficient | None needed (set to 0) | -The installer asks which pack to deploy and adjusts defaults accordingly. +The installer discovers packs dynamically and asks which to deploy. Experimental packs are clearly marked. > **Works from AWS CloudShell!** You can run the installer directly from [AWS CloudShell](https://console.aws.amazon.com/cloudshell/) — no local setup needed. CloudShell already has AWS credentials configured via your console session. If you pick Terraform as the deployment method, the installer will offer to install it automatically (no root required). @@ -151,7 +153,9 @@ Loki uses a **pack-based architecture** for deploying different AI agent runtime |------|------|-------------| | `bedrockify` | Base (auto-installed) | OpenAI-compatible proxy for Amazon Bedrock. Runs as a systemd daemon on port 8090. All agent packs depend on this. | | `openclaw` | Agent | Full stateful AI agent with 24/7 gateway, persistent memory, multi-channel support (Telegram, Discord, Slack). Includes Claude Code. | -| `hermes` | Agent | NousResearch Hermes CLI agent. Lightweight, terminal-focused, uses bedrockify for model access. | +| `hermes` | Agent *(experimental)* | NousResearch Hermes CLI agent. Self-improving skills, learning loop, lightweight. Uses bedrockify for model access. | +| `pi` | Agent *(experimental)* | Pi Coding Agent. Minimal terminal coding harness with read, write, edit, bash tools. Pure Node.js. | +| `ironclaw` | Agent *(experimental)* | IronClaw by NEAR AI. Rust-based agent with shell/file tools, MCP support. Single static binary. | ### How It Works diff --git a/packs/registry.json b/packs/registry.json index 2b0a239..ad615d2 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -30,7 +30,7 @@ }, "hermes": { "type": "agent", - "description": "Hermes -- NousResearch CLI agent via bedrockify", + "description": "Hermes -- NousResearch CLI agent via bedrockify (experimental)", "deps": ["bedrockify"], "instance_type": "t4g.medium", "root_volume_gb": 40, @@ -39,7 +39,7 @@ "ports": {}, "brain": false, "claude_code": false, - "experimental": false + "experimental": true }, "pi": { "type": "agent", diff --git a/packs/registry.yaml b/packs/registry.yaml index 968658d..276a954 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -32,7 +32,7 @@ packs: hermes: type: agent - description: "Hermes — NousResearch CLI agent via bedrockify" + description: "Hermes — NousResearch CLI agent via bedrockify (experimental)" deps: - bedrockify instance_type: t4g.medium @@ -42,7 +42,7 @@ packs: ports: {} brain: false claude_code: false - experimental: false + experimental: true pi: type: agent From b68730d59cf7a11e264a743e8c2b402726b3e088 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 23:28:26 +0000 Subject: [PATCH 026/172] ci: bump version to 0.5.10 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 635d799..55168fb 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.9" +INSTALLER_VERSION="0.5.10" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 268c5c046a6653f7403572a2340a5f685c9b81aa Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 31 Mar 2026 23:30:55 +0000 Subject: [PATCH 027/172] docs: add Pi + IronClaw sections to all bootstrap files --- .../essential/BOOTSTRAP-DAILY-UPDATE.md | 34 ++++++++++++++ bootstraps/essential/BOOTSTRAP-MCPORTER.md | 31 +++++++++++++ .../essential/BOOTSTRAP-MEMORY-SEARCH.md | 20 +++++++++ .../essential/BOOTSTRAP-MODEL-CONFIG.md | 37 +++++++++++++++ bootstraps/essential/BOOTSTRAP-PLAYWRIGHT.md | 19 +++++++- bootstraps/essential/BOOTSTRAP-SECRETS-AWS.md | 45 ++++++++++++++++++- bootstraps/essential/BOOTSTRAP-SKILLS.md | 21 +++++++++ .../telegram/BOOTSTRAP-TELEGRAM-GROUP.md | 22 +++++++++ bootstraps/telegram/BOOTSTRAP-TELEGRAM.md | 34 ++++++++++++++ 9 files changed, 261 insertions(+), 2 deletions(-) diff --git a/bootstraps/essential/BOOTSTRAP-DAILY-UPDATE.md b/bootstraps/essential/BOOTSTRAP-DAILY-UPDATE.md index 7a98384..5ce697c 100644 --- a/bootstraps/essential/BOOTSTRAP-DAILY-UPDATE.md +++ b/bootstraps/essential/BOOTSTRAP-DAILY-UPDATE.md @@ -126,6 +126,40 @@ Or ask Loki: *"Run the daily briefing now"* **Only included if findings exist:** security issues, EC2 problems **Silent (don't wake the operator):** routine low/medium security findings already known +## Pi-Specific Configuration + +Pi has no built-in cron system. Use the system crontab to schedule daily briefings, wrapping `pi -p "prompt"` as a one-shot invocation: + +```bash +# Add to crontab: crontab -e +0 8 * * * /usr/local/bin/pi -p "Run the daily AWS account briefing. Check costs, security findings, pipeline health, and EC2 status. Format for Telegram (no tables, bullet lists). Keep under 400 words." >> /tmp/pi-daily-briefing.log 2>&1 +``` + +Pi has no built-in Telegram delivery — pipe the output to a script that sends it via the Telegram Bot API, or to another delivery mechanism. Pi is a one-shot CLI tool; it will run the task and exit. + +## IronClaw-Specific Configuration + +IronClaw has **built-in scheduled routines** — configure daily briefings natively: + +```bash +# In ~/.ironclaw/.env or IronClaw's routine config +ROUTINE_DAILY_BRIEFING_SCHEDULE=0 8 * * * +ROUTINE_DAILY_BRIEFING_PROMPT="Run the daily AWS account briefing. Check costs, security findings, pipeline health, and EC2 status. Format for Telegram (no tables, bullet lists). Keep under 400 words." +ROUTINE_DAILY_BRIEFING_CHANNEL=telegram +``` + +Or via IronClaw's TOML config if supported: + +```toml +[[routines]] +name = "daily-briefing" +schedule = "0 8 * * *" +prompt = "Run the daily AWS account briefing..." +channel = "telegram" +``` + +IronClaw delivers the output via its built-in Telegram channel — no external scripting needed. Refer to IronClaw's documentation for the exact routine config format. + ## Finish ```bash diff --git a/bootstraps/essential/BOOTSTRAP-MCPORTER.md b/bootstraps/essential/BOOTSTRAP-MCPORTER.md index 9a4d32d..0abdd56 100644 --- a/bootstraps/essential/BOOTSTRAP-MCPORTER.md +++ b/bootstraps/essential/BOOTSTRAP-MCPORTER.md @@ -96,6 +96,37 @@ After configuring servers, update `TOOLS.md` with: This is your cheat sheet — future-you will thank present-you. +## Pi-Specific Configuration + +**Not applicable.** Pi has no MCP support. MCPorter cannot be used with Pi. For extended capabilities, use Pi's TypeScript extensions system instead — see `BOOTSTRAP-SKILLS.md`. + +## IronClaw-Specific Configuration + +IronClaw has **native MCP support** built in — no MCPorter needed. Configure MCP servers directly in IronClaw's config file (check `~/.ironclaw/.env` or `~/.ironclaw/config.toml` depending on your version): + +```toml +# ~/.ironclaw/config.toml (if supported) +[[mcp_servers]] +name = "aws-mcp" +command = "npx" +args = ["-y", "@anthropic-ai/aws-mcp"] +env = { AWS_REGION = "us-east-1" } + +[[mcp_servers]] +name = "filesystem" +command = "npx" +args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/ec2-user"] +``` + +Or via environment variables in `~/.ironclaw/.env`: + +```bash +MCP_SERVER_AWS_MCP_COMMAND=npx +MCP_SERVER_AWS_MCP_ARGS=-y @anthropic-ai/aws-mcp +``` + +IronClaw auto-discovers MCP tools at startup and makes them available to the agent. Refer to IronClaw's documentation for the exact config format. + ## 6. Finish After completing all steps: diff --git a/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md b/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md index f5def39..f208908 100644 --- a/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md +++ b/bootstraps/essential/BOOTSTRAP-MEMORY-SEARCH.md @@ -211,6 +211,26 @@ sudo systemctl edit bedrockify sudo systemctl restart bedrockify ``` +## Pi-Specific Configuration + +Pi has no built-in memory system. There is no `memory_search` tool or persistent session storage. + +To build custom memory, use bedrockify's `/v1/embeddings` endpoint (available on `localhost:8090`) to generate and store vectors in a file or SQLite database, then query them manually via a Pi extension or bash tool. This is opt-in and requires custom implementation. + +## IronClaw-Specific Configuration + +IronClaw has a built-in state database (PostgreSQL or embedded libSQL at `~/.ironclaw/state.db`). It may provide its own memory/session search — check IronClaw's documentation for available search tools. + +Bedrockify embeddings are also available on `localhost:8090` for custom semantic search workflows: + +```bash +curl -s -X POST http://127.0.0.1:8090/v1/embeddings \ + -H "Content-Type: application/json" \ + -d '{"input": "your text here", "model": "amazon.titan-embed-text-v2:0"}' +``` + +These can be used alongside IronClaw's native state DB for hybrid retrieval if needed. + ## Finish ```bash diff --git a/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md b/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md index 5f97688..3a88784 100644 --- a/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md +++ b/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md @@ -137,6 +137,43 @@ model: "anthropic/claude-sonnet-4.6" Then restart Hermes. Bedrockify maps these OpenAI-style model IDs to Bedrock model IDs automatically. +## Pi-Specific Configuration + +Pi uses bedrockify (OpenAI-compatible Bedrock proxy) for model access. Set the model in `~/.pi/agent/models.json` under the `bedrockify` provider entry: + +```json +{ + "providers": { + "bedrockify": { + "models": [ + { "id": "anthropic/claude-opus-4.6" } + ] + } + } +} +``` + +To switch to Sonnet for cost savings, change the `id` to `"anthropic/claude-sonnet-4.6"`. Pi has no cron or heartbeat system, so there's no separate heartbeat model to configure. + +## IronClaw-Specific Configuration + +IronClaw uses bedrockify via its OpenAI-compatible backend. Set the model in `~/.ironclaw/.env`: + +```bash +LLM_BACKEND=openai_compatible +LLM_BASE_URL=http://127.0.0.1:8090/v1 +LLM_API_KEY=not-needed +LLM_MODEL=anthropic/claude-opus-4.6 +``` + +For scheduled routines (cron-equivalent), set a lighter model to save costs: + +```bash +LLM_ROUTINE_MODEL=anthropic/claude-sonnet-4.6 +``` + +Restart IronClaw after editing `.env`. + ## Finish ```bash diff --git a/bootstraps/essential/BOOTSTRAP-PLAYWRIGHT.md b/bootstraps/essential/BOOTSTRAP-PLAYWRIGHT.md index 0b73c1d..0493401 100644 --- a/bootstraps/essential/BOOTSTRAP-PLAYWRIGHT.md +++ b/bootstraps/essential/BOOTSTRAP-PLAYWRIGHT.md @@ -50,4 +50,21 @@ npx playwright --version npx playwright open --headless https://example.com 2>/dev/null && echo "Playwright OK" ``` -If the Chromium binary is present (checked above), Hermes can use browser automation out of the box. No additional configuration needed — Hermes invokes Playwright directly as part of its agent toolchain. +## Pi-Specific Configuration + +Pi has no built-in browser automation. There is no native Playwright integration. + +As a potential workaround: if Pi gains MCP support via a future extension, the Playwright MCP server could be loaded. For now, browser tasks are not natively supported in Pi — use OpenClaw or Hermes for browser automation needs. + +The Chromium binary at `/home/ec2-user/.cache/ms-playwright/` is available on the instance regardless. + +## IronClaw-Specific Configuration + +IronClaw has no documented native browser automation support. The Playwright Chromium binary is pre-installed on the instance and available for manual invocation via IronClaw's `shell` tool: + +```bash +# IronClaw can invoke Playwright via its shell tool +npx playwright chromium --headless https://example.com +``` + +For richer browser automation, consider adding the Playwright MCP server to IronClaw's MCP config (see `BOOTSTRAP-MCPORTER.md` for the server definition). This gives IronClaw structured browser control via MCP tools. diff --git a/bootstraps/essential/BOOTSTRAP-SECRETS-AWS.md b/bootstraps/essential/BOOTSTRAP-SECRETS-AWS.md index 888c5c9..08052c3 100644 --- a/bootstraps/essential/BOOTSTRAP-SECRETS-AWS.md +++ b/bootstraps/essential/BOOTSTRAP-SECRETS-AWS.md @@ -347,4 +347,47 @@ hermes config set TELEGRAM_BOT_TOKEN $(aws secretsmanager get-secret-value \ --secret-id "openclaw/telegram-bot-token" --query SecretString --output text) ``` -The EC2 Instance Role Permissions and Storing Secrets sections above apply to both agents — secrets are stored the same way in AWS Secrets Manager regardless of which agent reads them. +## Pi-Specific Configuration + +Pi stores secrets in `~/.pi/agent/models.json` under the provider's `apiKey` field. To use bedrockify (which handles AWS auth itself), no API key is needed — set it to a placeholder: + +```json +{ + "providers": { + "bedrockify": { + "baseUrl": "http://127.0.0.1:8090/v1", + "apiKey": "not-needed" + } + } +} +``` + +For other secrets (e.g. GitHub token for a Pi task), fetch from Secrets Manager and pass via environment variable or inline in the task prompt. Pi has no native secrets resolution — secrets must be injected externally before invoking Pi. + +## IronClaw-Specific Configuration + +IronClaw secrets go in `~/.ironclaw/.env`. For bedrockify, set: + +```bash +LLM_API_KEY=not-needed +LLM_BASE_URL=http://127.0.0.1:8090/v1 +``` + +For other secrets, add them as environment variables in `.env`: + +```bash +GITHUB_TOKEN=ghp_xxx +TELEGRAM_BOT_TOKEN=your-bot-token +``` + +To fetch from AWS Secrets Manager at setup time: + +```bash +GITHUB_TOKEN=$(aws secretsmanager get-secret-value \ + --secret-id "faststart/github-token" --query SecretString --output text) +echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> ~/.ironclaw/.env +``` + +IronClaw reads `.env` at startup — restart after adding secrets. Do not commit `.env` to version control. + +The EC2 Instance Role Permissions section above applies to both agents — secrets are stored the same way in AWS Secrets Manager regardless of which agent reads them. diff --git a/bootstraps/essential/BOOTSTRAP-SKILLS.md b/bootstraps/essential/BOOTSTRAP-SKILLS.md index a709a68..e242cbd 100644 --- a/bootstraps/essential/BOOTSTRAP-SKILLS.md +++ b/bootstraps/essential/BOOTSTRAP-SKILLS.md @@ -49,6 +49,27 @@ cd ~/.openclaw/workspace/skills && git pull Consider adding this to a weekly cron to stay current. +## Pi-Specific Configuration + +Pi has a TypeScript extensions system. Load FastStart skills by placing them (or symlinks) in `~/.pi/agent/extensions/`. Extensions must export a standard Pi extension interface. + +The FastStart skills library is written for OpenClaw/Hermes — they won't load directly as Pi extensions. However, the reference docs and prompts in each skill directory are still useful as context when crafting Pi tasks. + +To install a skill as a Pi extension, copy or adapt the skill's logic into a TypeScript file under `~/.pi/agent/extensions/`: + +```bash +# Example: symlink a compatible extension +ln -s ~/.openclaw/workspace/skills/my-skill/pi-extension.ts ~/.pi/agent/extensions/my-skill.ts +``` + +Check the FastStart skills library for any skills that ship a `pi-extension.ts` file. + +## IronClaw-Specific Configuration + +IronClaw extends capabilities via MCP servers rather than a native skills system. Point IronClaw at the MCP servers that back each FastStart skill — see `BOOTSTRAP-MCPORTER.md` for server configs. + +The FastStart skills library's reference docs are still useful as context. For skill-equivalent functionality in IronClaw, configure the corresponding MCP server in `~/.ironclaw/.env` or IronClaw's MCP config section. + ## 4. Finish After completing all steps: diff --git a/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md b/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md index 149932f..c2aa7ed 100644 --- a/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md +++ b/bootstraps/telegram/BOOTSTRAP-TELEGRAM-GROUP.md @@ -213,6 +213,28 @@ hermes gateway stop && hermes gateway start - Bot-to-bot messages are ignored because bot user IDs aren't in `TELEGRAM_ALLOWED_USERS` - Alternatively, promote the bot to group admin instead of disabling privacy mode — admin bots see all messages regardless of privacy setting +### Pi Configuration + +**Not applicable.** Pi has no Telegram support and cannot participate in group chats. + +### IronClaw Configuration + +IronClaw supports Telegram groups via its WASM channel. Set the group chat ID and configure mention behavior in `~/.ironclaw/.env`: + +```bash +TELEGRAM_HOME_CHANNEL=GROUP_CHAT_ID # Negative number, e.g. -1001234567890 +TELEGRAM_ALLOWED_USERS=OWNER_USER_ID +TELEGRAM_REQUIRE_MENTION=true # Only respond when @mentioned or replied to +``` + +Add custom broadcast trigger patterns if supported: + +```bash +TELEGRAM_MENTION_PATTERNS=@fleet,@all +``` + +Restart IronClaw after editing `.env`. The same BotFather privacy mode disable (Part 1) is required for IronClaw — it needs to see all group messages, not just commands. + --- ## Part 4: Verify diff --git a/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md b/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md index 01a19a5..a107102 100644 --- a/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md +++ b/bootstraps/telegram/BOOTSTRAP-TELEGRAM.md @@ -268,6 +268,40 @@ Reactions are available but use them sparingly — at most 1 per 5–10 exchange > **Note:** These formatting rules apply regardless of agent type — Telegram renders markdown the same way for both OpenClaw and Hermes. +## Pi-Specific Configuration + +**Not applicable.** Pi is a CLI tool with no Telegram support. It cannot send or receive Telegram messages natively. For Telegram delivery of Pi output, pipe results to a script that calls the Telegram Bot API directly. + +## IronClaw-Specific Configuration + +IronClaw supports Telegram via WASM channels. Configure the bot token and allowed users in `~/.ironclaw/.env`: + +```bash +TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE +TELEGRAM_ALLOWED_USERS=YOUR_CHAT_ID +``` + +To fetch the token from Secrets Manager: + +```bash +BOT_TOKEN=$(aws secretsmanager get-secret-value \ + --secret-id /faststart/telegram-bot-token \ + --query SecretString --output text --region us-east-1) + +echo "TELEGRAM_BOT_TOKEN=${BOT_TOKEN}" >> ~/.ironclaw/.env +echo "TELEGRAM_ALLOWED_USERS=YOUR_CHAT_ID" >> ~/.ironclaw/.env +``` + +Start IronClaw with the Telegram channel enabled: + +```bash +ironclaw --channel telegram +``` + +Or configure it to start with Telegram as the default channel. Refer to IronClaw's documentation for channel startup flags and systemd service setup. + +> **Note:** The formatting rules in Part 2 apply regardless of agent type — Telegram renders markdown the same way for all agents. + --- ## Finish From 2fae2643f95250b035ac91b9f7ebc7a66c73a730 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 23:31:04 +0000 Subject: [PATCH 028/172] ci: bump version to 0.5.11 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 55168fb..8b3fef5 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.10" +INSTALLER_VERSION="0.5.11" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 04d7218fbff1a802bda7a016ba2935314be10590 Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 1 Apr 2026 06:24:39 +0000 Subject: [PATCH 029/172] fix: ironclaw PACK_ALIASES unbound var + add pack contract CI + auto-fetch logs on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: ironclaw shell-profile.sh was missing PACK_ALIASES. bootstrap.sh runs with set -u, so referencing the unset var aborted at line 389. Changes: - packs/ironclaw/resources/shell-profile.sh: add PACK_ALIASES + full banner - deploy/bootstrap.sh: default all PACK_* vars before sourcing shell-profile.sh (belt + suspenders — packs should define them, bootstrap won't crash if they don't) - install.sh: auto-fetch /var/log/loki-bootstrap.log via SSM on failure (saves users from manually SSM-ing in and cat-ing the log) - tests/test-pack-contracts.sh: NEW — CI validation that all agent packs declare required vars (PACK_ALIASES, PACK_BANNER_NAME, PACK_BANNER_EMOJI, PACK_BANNER_COMMANDS), have valid manifests, install.sh, resources/, and registry entries. Fails CI if any contract is violated. - packs/test-packs.sh: dynamic pack discovery instead of hardcoded PACKS array - .github/workflows/pack-tests.yml: add deploy/** and install.sh to trigger paths --- .github/workflows/pack-tests.yml | 4 + deploy/bootstrap.sh | 5 + install.sh | 35 +++- packs/ironclaw/resources/shell-profile.sh | 15 +- packs/test-packs.sh | 26 ++- tests/test-pack-contracts.sh | 240 ++++++++++++++++++++++ 6 files changed, 314 insertions(+), 11 deletions(-) create mode 100755 tests/test-pack-contracts.sh diff --git a/.github/workflows/pack-tests.yml b/.github/workflows/pack-tests.yml index dd2966e..39b6712 100644 --- a/.github/workflows/pack-tests.yml +++ b/.github/workflows/pack-tests.yml @@ -7,12 +7,16 @@ on: - 'packs/**' - 'tests/**' - 'scripts/**' + - 'deploy/**' + - 'install.sh' - '.github/workflows/pack-tests.yml' pull_request: paths: - 'packs/**' - 'tests/**' - 'scripts/**' + - 'deploy/**' + - 'install.sh' permissions: contents: read diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 51ff951..5dcb311 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -383,6 +383,11 @@ ok "mise + Node.js setup complete" PACK_PROFILE="${PACKS_DIR}/${PACK_NAME}/resources/shell-profile.sh" if [[ -f "$PACK_PROFILE" ]]; then # Source pack profile to get PACK_ALIASES, PACK_BANNER_NAME, etc. + # Default all expected vars first — packs may omit optional ones + PACK_ALIASES="" + PACK_BANNER_NAME="${PACK_NAME}" + PACK_BANNER_EMOJI="🤖" + PACK_BANNER_COMMANDS="" source "$PACK_PROFILE" # Write aliases to ec2-user .bashrc diff --git a/install.sh b/install.sh index 8b3fef5..636c7a1 100755 --- a/install.sh +++ b/install.sh @@ -993,10 +993,39 @@ wait_for_bootstrap() { echo -e " ${BOLD}Last log output:${NC}" echo "$fail_log" | tail -20 | sed 's/^/ /' fi + + # Auto-fetch full bootstrap log via SSM (saves the user from manual SSM + cat) echo "" - echo " To debug, connect via SSM:" - echo " $(ssm_connect_cmd "$INSTANCE_ID")" - echo " Then check: cat /var/log/loki-bootstrap.log" + info "Fetching full bootstrap log from instance..." + local log_cmd_id + log_cmd_id=$(aws ssm send-command --instance-ids "$INSTANCE_ID" \ + --document-name AWS-RunShellScript \ + --parameters 'commands=["cat /var/log/loki-bootstrap.log 2>/dev/null || echo LOG_NOT_FOUND"]' \ + --region "$DEPLOY_REGION" --output text --query 'Command.CommandId' 2>/dev/null || echo "") + if [[ -n "$log_cmd_id" ]]; then + sleep 8 # give SSM time to execute + local full_log + full_log=$(aws ssm get-command-invocation --command-id "$log_cmd_id" \ + --instance-id "$INSTANCE_ID" --region "$DEPLOY_REGION" \ + --query 'StandardOutputContent' --output text 2>/dev/null || echo "") + if [[ -n "$full_log" && "$full_log" != "LOG_NOT_FOUND" ]]; then + local log_file="/tmp/loki-bootstrap-${INSTANCE_ID}.log" + echo "$full_log" > "$log_file" + ok "Full bootstrap log saved to: ${log_file}" + echo "" + echo -e " ${BOLD}Last 30 lines:${NC}" + echo "$full_log" | tail -30 | sed 's/^/ /' + else + warn "Could not retrieve bootstrap log via SSM" + echo " Connect manually: $(ssm_connect_cmd "$INSTANCE_ID")" + echo " Then check: cat /var/log/loki-bootstrap.log" + fi + else + warn "SSM command failed — instance may not be reachable yet" + echo " Connect manually: $(ssm_connect_cmd "$INSTANCE_ID")" + echo " Then check: cat /var/log/loki-bootstrap.log" + fi + echo "" return 1 fi diff --git a/packs/ironclaw/resources/shell-profile.sh b/packs/ironclaw/resources/shell-profile.sh index bea99e3..17c39d8 100644 --- a/packs/ironclaw/resources/shell-profile.sh +++ b/packs/ironclaw/resources/shell-profile.sh @@ -1,3 +1,14 @@ +# IronClaw shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +# This file defines aliases and the welcome banner for the IronClaw pack. + +PACK_ALIASES=' +alias ic="ironclaw" +' + +PACK_BANNER_NAME="IronClaw Agent Environment" PACK_BANNER_EMOJI="🦀" -PACK_BANNER_NAME="IronClaw" -PACK_BANNER_COMMANDS="ironclaw" +PACK_BANNER_COMMANDS=' + ironclaw → Run IronClaw CLI agent + ironclaw --help → Show IronClaw options + ironclaw --version → Show installed version +' diff --git a/packs/test-packs.sh b/packs/test-packs.sh index d564f01..1a5e717 100755 --- a/packs/test-packs.sh +++ b/packs/test-packs.sh @@ -44,7 +44,13 @@ yaml_parse_ok() { } # ── Packs to test ───────────────────────────────────────────────────────────── -PACKS=(bedrockify openclaw hermes) +# Dynamically discover all packs (directories with manifest.yaml) +PACKS=() +for _dir in "${SCRIPT_DIR}"/*/; do + _pack=$(basename "$_dir") + [[ -f "${_dir}/manifest.yaml" ]] && PACKS+=("$_pack") +done +unset _dir _pack # ── Test: common.sh ─────────────────────────────────────────────────────────── header "Test: common.sh" @@ -290,7 +296,7 @@ done # ── Test: resources/ directory ──────────────────────────────────────────────── header "Test: resources/" -# Expected resources per pack +# Expected resources per pack (packs not listed here skip the per-file check) declare -A PACK_RESOURCES PACK_RESOURCES[bedrockify]="bedrockify.service.tpl" PACK_RESOURCES[openclaw]="config-gen.py openclaw-gateway.service.tpl" @@ -306,6 +312,12 @@ for pack in "${PACKS[@]}"; do continue fi + # Skip per-file checks for packs without a resource manifest + if [[ -z "${PACK_RESOURCES[$pack]+x}" ]]; then + skip "${pack}/resources/ — no expected resources defined (directory exists, OK)" + continue + fi + for resource in ${PACK_RESOURCES[$pack]}; do RFILE="${RESOURCES_DIR}/${resource}" if [[ -f "${RFILE}" ]]; then @@ -327,10 +339,12 @@ done header "Test: shellcheck (lint)" if command -v shellcheck &>/dev/null; then - for script in "${SCRIPT_DIR}/common.sh" \ - "${SCRIPT_DIR}/bedrockify/install.sh" \ - "${SCRIPT_DIR}/openclaw/install.sh" \ - "${SCRIPT_DIR}/hermes/install.sh"; do + # Dynamically lint common.sh + all pack install scripts + _lint_scripts=("${SCRIPT_DIR}/common.sh") + for _p in "${PACKS[@]}"; do + [[ -f "${SCRIPT_DIR}/${_p}/install.sh" ]] && _lint_scripts+=("${SCRIPT_DIR}/${_p}/install.sh") + done + for script in "${_lint_scripts[@]}"; do if [[ -f "${script}" ]]; then SCRIPT_NAME="$(basename "$(dirname "${script}")")/$(basename "${script}")" if shellcheck -S warning "${script}" 2>/dev/null; then diff --git a/tests/test-pack-contracts.sh b/tests/test-pack-contracts.sh new file mode 100755 index 0000000..82547ba --- /dev/null +++ b/tests/test-pack-contracts.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +# tests/test-pack-contracts.sh — CI contract validation for all agent packs +# +# Ensures every agent pack declares all variables and files expected by +# the bootstrap dispatcher (deploy/bootstrap.sh) and installer (install.sh). +# +# Checks: +# 1. shell-profile.sh defines PACK_ALIASES, PACK_BANNER_NAME, PACK_BANNER_EMOJI, PACK_BANNER_COMMANDS +# 2. manifest.yaml exists with required keys (name, version, type, description, params, provides) +# 3. manifest.yaml name matches directory name +# 4. install.sh exists, is executable, has bash shebang, references common.sh, writes done marker +# 5. resources/ directory exists +# 6. Pack is listed in registry.yaml AND registry.json +# +# Usage: bash tests/test-pack-contracts.sh +# Exit: 0 if all pass, 1 if any fail (breaks CI) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PACKS_DIR="${SCRIPT_DIR}/packs" +REGISTRY_YAML="${PACKS_DIR}/registry.yaml" +REGISTRY_JSON="${PACKS_DIR}/registry.json" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +PASS=0; FAIL=0; SKIP=0 + +pass() { printf "${GREEN} ✓${NC} %s\n" "$1"; PASS=$((PASS + 1)); } +fail() { printf "${RED} ✗${NC} %s\n" "$1"; FAIL=$((FAIL + 1)); } +skip() { printf "${YELLOW} ○${NC} %s\n" "$1"; SKIP=$((SKIP + 1)); } +header() { printf "\n${BOLD}${CYAN}%s${NC}\n" "$1"; } + +# Discover ALL agent packs dynamically (not a hardcoded list) +discover_agent_packs() { + local -a packs=() + for dir in "${PACKS_DIR}"/*/; do + local pack + pack=$(basename "$dir") + [[ "$pack" == "common.sh" ]] && continue # not a pack dir + + # Check if it's an agent pack (has manifest with type: agent, or is in registry as agent) + local manifest="${dir}/manifest.yaml" + if [[ -f "$manifest" ]]; then + local ptype + ptype=$(python3 -c "import yaml; d=yaml.safe_load(open('${manifest}')); print(d.get('type',''))" 2>/dev/null || echo "") + if [[ "$ptype" == "agent" ]]; then + packs+=("$pack") + fi + fi + done + echo "${packs[@]}" +} + +# Required variables that bootstrap.sh expects from shell-profile.sh +REQUIRED_SHELL_VARS=(PACK_ALIASES PACK_BANNER_NAME PACK_BANNER_EMOJI PACK_BANNER_COMMANDS) + +# Required keys in manifest.yaml +REQUIRED_MANIFEST_KEYS=(name version type description params provides) + +# ── Discover packs ──────────────────────────────────────────────────────────── +PACKS=( $(discover_agent_packs) ) + +if [[ ${#PACKS[@]} -eq 0 ]]; then + echo "No agent packs found in ${PACKS_DIR}" + exit 1 +fi + +header "Discovered ${#PACKS[@]} agent pack(s): ${PACKS[*]}" + +# ── Contract 1: shell-profile.sh variables ──────────────────────────────────── +header "Contract: shell-profile.sh declares all required variables" + +for pack in "${PACKS[@]}"; do + local_profile="${PACKS_DIR}/${pack}/resources/shell-profile.sh" + + if [[ ! -f "$local_profile" ]]; then + fail "${pack}: resources/shell-profile.sh does not exist" + continue + fi + + for var in "${REQUIRED_SHELL_VARS[@]}"; do + # Check the variable is assigned (not just referenced) + if grep -qE "^${var}=" "$local_profile" 2>/dev/null; then + pass "${pack}: shell-profile.sh defines ${var}" + else + fail "${pack}: shell-profile.sh MISSING ${var} (bootstrap will crash with 'unbound variable')" + fi + done + + # Bonus: verify sourcing it doesn't error under set -u + if bash -c "set -euo pipefail; source '$local_profile'" 2>/dev/null; then + pass "${pack}: shell-profile.sh sources cleanly under set -euo pipefail" + else + fail "${pack}: shell-profile.sh errors when sourced with set -euo pipefail" + fi +done + +# ── Contract 2: manifest.yaml ──────────────────────────────────────────────── +header "Contract: manifest.yaml has required keys" + +for pack in "${PACKS[@]}"; do + manifest="${PACKS_DIR}/${pack}/manifest.yaml" + + if [[ ! -f "$manifest" ]]; then + fail "${pack}: manifest.yaml does not exist" + continue + fi + + # Valid YAML + if python3 -c "import yaml; yaml.safe_load(open('${manifest}'))" 2>/dev/null; then + pass "${pack}: manifest.yaml is valid YAML" + else + fail "${pack}: manifest.yaml is invalid YAML" + continue + fi + + # Required keys + for key in "${REQUIRED_MANIFEST_KEYS[@]}"; do + if python3 -c "import yaml,sys; d=yaml.safe_load(open('${manifest}')); sys.exit(0 if '${key}' in d else 1)" 2>/dev/null; then + pass "${pack}: manifest.yaml has '${key}'" + else + fail "${pack}: manifest.yaml MISSING '${key}'" + fi + done + + # Name matches directory + if python3 -c "import yaml,sys; d=yaml.safe_load(open('${manifest}')); sys.exit(0 if d.get('name')=='${pack}' else 1)" 2>/dev/null; then + pass "${pack}: manifest name matches directory" + else + fail "${pack}: manifest name does NOT match directory '${pack}'" + fi +done + +# ── Contract 3: install.sh ─────────────────────────────────────────────────── +header "Contract: install.sh exists and is well-formed" + +for pack in "${PACKS[@]}"; do + install="${PACKS_DIR}/${pack}/install.sh" + + if [[ ! -f "$install" ]]; then + fail "${pack}: install.sh does not exist" + continue + fi + + pass "${pack}: install.sh exists" + + if [[ -x "$install" ]]; then + pass "${pack}: install.sh is executable" + else + fail "${pack}: install.sh is NOT executable (chmod +x needed)" + fi + + # Bash shebang + _shebang=$(head -1 "$install") + if [[ "$_shebang" == "#!/usr/bin/env bash" || "$_shebang" == "#!/bin/bash" ]]; then + pass "${pack}: install.sh has bash shebang" + else + fail "${pack}: install.sh has unexpected shebang: ${_shebang}" + fi + + # References common.sh + if grep -q 'common\.sh' "$install" 2>/dev/null; then + pass "${pack}: install.sh references common.sh" + else + fail "${pack}: install.sh does NOT reference common.sh" + fi + + # Writes done marker + if grep -q 'write_done_marker\|pack-.*-done' "$install" 2>/dev/null; then + pass "${pack}: install.sh writes done marker" + else + fail "${pack}: install.sh does NOT write done marker" + fi +done + +# ── Contract 4: resources/ directory ───────────────────────────────────────── +header "Contract: resources/ directory exists" + +for pack in "${PACKS[@]}"; do + if [[ -d "${PACKS_DIR}/${pack}/resources" ]]; then + pass "${pack}: resources/ directory exists" + else + fail "${pack}: resources/ directory MISSING" + fi +done + +# ── Contract 5: pack listed in registries ──────────────────────────────────── +header "Contract: pack listed in registry.yaml and registry.json" + +for pack in "${PACKS[@]}"; do + # registry.yaml + if python3 -c "import yaml,sys; d=yaml.safe_load(open('${REGISTRY_YAML}')); sys.exit(0 if '${pack}' in d.get('packs',{}) else 1)" 2>/dev/null; then + pass "${pack}: listed in registry.yaml" + else + fail "${pack}: NOT listed in registry.yaml" + fi + + # registry.json + if [[ -f "$REGISTRY_JSON" ]]; then + if jq -e --arg p "$pack" '.packs[$p]' "$REGISTRY_JSON" >/dev/null 2>&1; then + pass "${pack}: listed in registry.json" + else + fail "${pack}: NOT listed in registry.json" + fi + else + skip "${pack}: registry.json not found" + fi +done + +# ── Contract 6: health_check in manifest (warning only) ───────────────────── +header "Contract: health_check defined (recommended)" + +for pack in "${PACKS[@]}"; do + manifest="${PACKS_DIR}/${pack}/manifest.yaml" + [[ -f "$manifest" ]] || continue + + if python3 -c "import yaml,sys; d=yaml.safe_load(open('${manifest}')); sys.exit(0 if 'health_check' in d else 1)" 2>/dev/null; then + pass "${pack}: manifest has health_check" + else + skip "${pack}: manifest missing health_check (recommended but not required)" + fi +done + +# ── Summary ────────────────────────────────────────────────────────────────── +printf "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf "${BOLD} Pack Contract Validation${NC}\n" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf " ${GREEN}Passed:${NC} %d\n" "${PASS}" +printf " ${RED}Failed:${NC} %d\n" "${FAIL}" +printf " ${YELLOW}Skipped:${NC} %d\n" "${SKIP}" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n\n" + +if [[ "${FAIL}" -gt 0 ]]; then + printf "${RED}✗ %d contract violation(s) — fix before merging${NC}\n\n" "${FAIL}" + exit 1 +else + printf "${GREEN}✓ All pack contracts satisfied${NC}\n\n" + exit 0 +fi From c7607d9200d160a03ad0589c2f9102eebf806d17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 06:24:57 +0000 Subject: [PATCH 030/172] ci: bump version to 0.5.12 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 636c7a1..e3d6b3b 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.11" +INSTALLER_VERSION="0.5.12" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 3a14eeaaf5d34391aef3bd7e9cb3b1d43c15a44f Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 1 Apr 2026 06:32:41 +0000 Subject: [PATCH 031/172] feat: add verify-pack CLI + pack-checklist.md New tools for pack authors: - scripts/verify-pack: One-liner pack validator Usage: verify-pack ./packs/my-pack [--fix] or verify-pack --all Checks: manifest.yaml (all required keys, name match, param completeness), install.sh (shebang, set -euo, common.sh, write_done_marker, --help, shellcheck), shell-profile.sh (all 4 PACK_* vars, sources cleanly under strict mode), registry entries (yaml + json), test.sh presence. --fix mode shows contextual fix suggestions for every failure. - docs/pack-checklist.md: Complete guide for creating new packs Covers directory structure, manifest schema, install.sh patterns, shell-profile.sh variables, registry entries, test.sh, common mistakes, and available helpers from common.sh. --- docs/pack-checklist.md | 315 ++++++++++++++++++++++++++++++ scripts/verify-pack | 429 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 744 insertions(+) create mode 100644 docs/pack-checklist.md create mode 100755 scripts/verify-pack diff --git a/docs/pack-checklist.md b/docs/pack-checklist.md new file mode 100644 index 0000000..8955cba --- /dev/null +++ b/docs/pack-checklist.md @@ -0,0 +1,315 @@ +# Pack Checklist — Creating a New Loki Agent Pack + +This document defines everything needed to create a new pack for the loki-agent deployer. +Use `scripts/verify-pack` to validate your pack before submitting. + +```bash +# Verify a single pack +scripts/verify-pack ./packs/my-pack + +# With fix suggestions +scripts/verify-pack ./packs/my-pack --fix + +# Verify all packs +scripts/verify-pack --all +``` + +--- + +## Quick Start + +```bash +# 1. Create pack directory +mkdir -p packs/my-agent/resources + +# 2. Create required files (see sections below) +touch packs/my-agent/manifest.yaml +touch packs/my-agent/install.sh +touch packs/my-agent/resources/shell-profile.sh +touch packs/my-agent/test.sh + +# 3. Make scripts executable +chmod +x packs/my-agent/install.sh +chmod +x packs/my-agent/test.sh + +# 4. Verify +scripts/verify-pack ./packs/my-agent --fix +``` + +--- + +## Directory Structure + +Every agent pack lives in `packs//` and follows this layout: + +``` +packs/my-agent/ +├── manifest.yaml # REQUIRED — pack metadata, params, deps, health check +├── install.sh # REQUIRED — idempotent installer script +├── resources/ +│ └── shell-profile.sh # REQUIRED (agent packs) — aliases + welcome banner +│ └── *.tpl # Optional — service files, config templates +└── test.sh # RECOMMENDED — offline unit tests +``` + +--- + +## 1. manifest.yaml (REQUIRED) + +The manifest declares what the pack is, what it needs, and what it provides. +**All keys below are required.** The name must match the directory name. + +```yaml +name: my-agent # Must match directory name +version: "1.0.0" # Semver string +type: agent # "agent" or "base" +description: "My Agent — one-line desc" # Human-readable, used in installer menu + +deps: # Pack dependencies (installed first) + - bedrockify # Most agent packs need this + +requirements: + arch: # Supported architectures + - arm64 + - amd64 + os: # Supported operating systems + - al2023 + - ubuntu2204 + min_instance_type: t4g.medium # Minimum EC2 instance size + +params: # Config parameters (each needs all 3 fields) + - name: region + description: "AWS region for Bedrock" + default: us-east-1 + - name: model + description: "Model ID for the agent" + default: "us.anthropic.claude-sonnet-4-6-v1" + - name: bedrockify-port + description: "Port where bedrockify is running" + default: "8090" + +health_check: # How bootstrap verifies the install + command: "my-agent --version" # OR url: "http://127.0.0.1:${port}/" + timeout: 10 # Seconds + +provides: + commands: # CLI commands this pack makes available + - my-agent + services: [] # Systemd services (empty if none) +``` + +### Param rules +- Every param must have `name`, `description`, and `default` +- Common params: `region`, `model`, `bedrockify-port` +- Params are injected into `/tmp/loki-pack-config.json` by the bootstrap dispatcher + +### Health check types +- **command**: `command: "my-agent --version"` — run a shell command, expect exit 0 +- **url**: `url: "http://127.0.0.1:${port}/"` + `expect: "\"status\":\"ok\""` — HTTP check + +--- + +## 2. install.sh (REQUIRED) + +The installer script runs on the target EC2 instance during bootstrap. +It must be **idempotent** (safe to re-run). + +### Required patterns + +```bash +#!/usr/bin/env bash +# packs/my-agent/install.sh — Install My Agent +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Read config from bootstrap dispatcher ───────────────────────────────────── +REGION="$(pack_config_get region "us-east-1")" +MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat <"` on success +- [ ] Is executable (`chmod +x`) +- [ ] Idempotent (safe to run twice) + +### Available helpers from common.sh +| Function | Purpose | +|----------|---------| +| `ok "msg"` | Print success message | +| `warn "msg"` | Print warning | +| `fail "msg"` | Print error and exit 1 | +| `step "name"` | Announce a major phase (updates SSM progress) | +| `require_cmd cmd [cmd...]` | Fail if commands not found | +| `write_done_marker "name"` | Write `/tmp/pack-name-done` marker | +| `pack_config_get "key" "default"` | Read from bootstrap config JSON | +| `check_bedrockify_health PORT` | Verify bedrockify is running | + +--- + +## 3. resources/shell-profile.sh (REQUIRED for agent packs) + +Defines shell aliases and the welcome banner shown when users SSM into the instance. +**All four variables must be defined** (even if empty string) — the bootstrap runs +with `set -u` and will crash on any missing variable. + +```bash +# My Agent shell profile — sourced by bootstrap for .bashrc +# This file defines aliases and the welcome banner for the my-agent pack. + +PACK_ALIASES=' +alias ma="my-agent" +alias mah="my-agent --help" +' + +PACK_BANNER_NAME="My Agent Environment" +PACK_BANNER_EMOJI="🤖" +PACK_BANNER_COMMANDS=' + my-agent → Run My Agent + my-agent --help → Show options + my-agent --version → Show installed version +' +``` + +### Required variables +| Variable | Purpose | Example | +|----------|---------|---------| +| `PACK_ALIASES` | Shell aliases written to `.bashrc` | `alias ma="my-agent"` | +| `PACK_BANNER_NAME` | Name shown in welcome banner | `"My Agent Environment"` | +| `PACK_BANNER_EMOJI` | Emoji for the banner | `"🤖"` | +| `PACK_BANNER_COMMANDS` | Commands listed in welcome banner | Multi-line string | + +> **Why this matters:** The bootstrap dispatcher sources this file under `set -euo pipefail`. +> If ANY variable is undefined, bash treats it as an unbound variable error and the +> entire bootstrap aborts. This was the root cause of the IronClaw deploy failure. + +--- + +## 4. Registry Entries (REQUIRED for deployment) + +Your pack must be listed in **both** registry files for the installer to discover it. + +### registry.yaml +```yaml +packs: + # ... existing packs ... + my-agent: + type: agent + description: "My Agent — one-line description" + deps: + - bedrockify + instance_type: t4g.medium + root_volume_gb: 40 + data_volume_gb: 0 + default_model: "us.anthropic.claude-sonnet-4-6-v1" + ports: {} + brain: false + claude_code: false + experimental: true # Set true for new packs +``` + +### registry.json +Same data in JSON format. Must stay in sync with registry.yaml. + +--- + +## 5. test.sh (RECOMMENDED) + +Offline tests that validate your pack without requiring the agent to be installed. +CI auto-discovers and runs `packs//test.sh` files. + +Good things to test: +- manifest.yaml parses and has correct values +- install.sh --help exits 0 +- shell-profile.sh sources without error +- Config generation produces valid output +- Download URLs are correctly constructed +- Template rendering works + +See `packs/ironclaw/test.sh` or `packs/pi/test.sh` for examples. + +--- + +## 6. Dependency Rules + +- **Agent packs** almost always depend on `bedrockify` (provides the OpenAI-compatible proxy) +- **Base packs** have no dependencies (they ARE the dependency) +- Dependencies are installed before your pack by the bootstrap dispatcher +- List deps in manifest.yaml AND registry.yaml/json + +--- + +## Common Mistakes + +| Mistake | What happens | Prevention | +|---------|-------------|-----------| +| Missing `PACK_ALIASES` in shell-profile.sh | Bootstrap crashes: "unbound variable" | verify-pack catches this | +| install.sh not executable | Bootstrap skips install silently | `chmod +x` + verify-pack | +| Hardcoded region/model in install.sh | Config from installer is ignored | Use `pack_config_get` | +| Missing `write_done_marker` | Bootstrap can't verify install succeeded | verify-pack catches this | +| Pack not in registry.yaml/json | Pack won't appear in installer menu | verify-pack catches this | +| manifest name ≠ directory name | Bootstrap can't find pack resources | verify-pack catches this | + +--- + +## Verification + +Before submitting a PR, always run: + +```bash +# Verify your pack +scripts/verify-pack ./packs/my-agent --fix + +# Run your pack tests +bash packs/my-agent/test.sh + +# Run full test suite +bash packs/test-packs.sh +bash tests/test-pack-contracts.sh +``` + +CI will reject PRs where pack contracts are violated. diff --git a/scripts/verify-pack b/scripts/verify-pack new file mode 100755 index 0000000..ad2db88 --- /dev/null +++ b/scripts/verify-pack @@ -0,0 +1,429 @@ +#!/usr/bin/env bash +# scripts/verify-pack — Validate a pack directory meets all loki-agent requirements +# +# Usage: +# verify-pack ./packs/my-pack Verify a single pack +# verify-pack ./packs/my-pack --fix Show fix suggestions for failures +# verify-pack --all Verify all packs in packs/ +# +# Exit: 0 = all checks pass, 1 = failures found +# +# This is the authoritative pre-submit check for pack authors. +# The same contracts are enforced in CI (tests/test-pack-contracts.sh). + +set -euo pipefail + +# ── Colors ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' + +PASS=0; FAIL=0; WARN=0; SKIP=0 +FIX_MODE=false + +pass() { printf " ${GREEN}✓${NC} %s\n" "$1"; PASS=$((PASS + 1)); } +fail_c() { printf " ${RED}✗${NC} %s\n" "$1"; FAIL=$((FAIL + 1)); } +warn_c() { printf " ${YELLOW}⚠${NC} %s\n" "$1"; WARN=$((WARN + 1)); } +skip_c() { printf " ${DIM}○${NC} %s\n" "$1"; SKIP=$((SKIP + 1)); } +hint() { $FIX_MODE && printf " ${DIM}→ %s${NC}\n" "$1" || true; } +section() { printf "\n${BOLD}${CYAN}── %s${NC}\n" "$1"; } + +# ── Usage ───────────────────────────────────────────────────────────────────── +usage() { + cat < [--fix] + verify-pack --all [--fix] + +Validates a loki-agent pack against all required contracts. + +Options: + --fix Show fix suggestions for each failure + --all Verify all packs in packs/ directory + --help Show this help + +Examples: + verify-pack ./packs/ironclaw + verify-pack ./packs/ironclaw --fix + verify-pack --all +EOF + exit 0 +} + +# ── Resolve repo root ──────────────────────────────────────────────────────── +# Try to find the repo root (where packs/, deploy/, install.sh live) +find_repo_root() { + local dir="$1" + # Walk up from the pack dir looking for packs/ sibling or install.sh + local d + d=$(cd "$dir" 2>/dev/null && pwd) + while [[ "$d" != "/" ]]; do + if [[ -f "$d/install.sh" && -d "$d/packs" ]]; then + echo "$d"; return 0 + fi + d=$(dirname "$d") + done + # Fallback: check if we're already in the repo + if [[ -f "./install.sh" && -d "./packs" ]]; then + pwd; return 0 + fi + echo "" +} + +# ── Pack validation ─────────────────────────────────────────────────────────── +verify_single_pack() { + local pack_dir="$1" + + # Resolve to absolute path + pack_dir=$(cd "$pack_dir" 2>/dev/null && pwd) || { + fail_c "Directory not found: $1" + return + } + + local pack_name + pack_name=$(basename "$pack_dir") + local repo_root + repo_root=$(find_repo_root "$pack_dir") + + printf "\n${BOLD}╔══════════════════════════════════════════════════════════╗${NC}\n" + printf "${BOLD}║ Verifying pack: %-39s║${NC}\n" "$pack_name" + printf "${BOLD}╚══════════════════════════════════════════════════════════╝${NC}\n" + + # ── 1. Directory structure ────────────────────────────────────────────────── + section "1. Directory Structure" + + if [[ -d "$pack_dir" ]]; then + pass "Pack directory exists: $pack_name/" + else + fail_c "Pack directory missing" + return + fi + + if [[ -d "$pack_dir/resources" ]]; then + pass "resources/ directory exists" + else + fail_c "resources/ directory missing" + hint "mkdir -p $pack_name/resources" + fi + + # ── 2. manifest.yaml ─────────────────────────────────────────────────────── + section "2. manifest.yaml" + + local manifest="$pack_dir/manifest.yaml" + if [[ ! -f "$manifest" ]]; then + fail_c "manifest.yaml missing" + hint "Create $pack_name/manifest.yaml — see docs/pack-checklist.md" + return + fi + pass "manifest.yaml exists" + + # Valid YAML + if python3 -c "import yaml; yaml.safe_load(open('$manifest'))" 2>/dev/null; then + pass "manifest.yaml is valid YAML" + else + fail_c "manifest.yaml is invalid YAML" + hint "Check for indentation errors, missing colons, or unquoted special chars" + return + fi + + # Required keys + local required_keys=(name version type description deps requirements params health_check provides) + for key in "${required_keys[@]}"; do + if python3 -c "import yaml,sys; d=yaml.safe_load(open('$manifest')); sys.exit(0 if '$key' in d else 1)" 2>/dev/null; then + pass "manifest has '$key'" + else + fail_c "manifest MISSING '$key'" + case "$key" in + name) hint "$key: $pack_name" ;; + version) hint "$key: \"1.0.0\"" ;; + type) hint "$key: agent # or 'base'" ;; + deps) hint "$key:\n - bedrockify # most agent packs depend on this" ;; + requirements) hint "$key:\n arch: [arm64, amd64]\n os: [al2023, ubuntu2204]\n min_instance_type: t4g.medium" ;; + params) hint "$key:\n - name: region\n description: \"AWS region\"\n default: us-east-1" ;; + health_check) hint "$key:\n command: \"$pack_name --version\"\n timeout: 10" ;; + provides) hint "$key:\n commands: [$pack_name]\n services: []" ;; + esac + fi + done + + # Name matches directory + local manifest_name + manifest_name=$(python3 -c "import yaml; print(yaml.safe_load(open('$manifest')).get('name',''))" 2>/dev/null || echo "") + if [[ "$manifest_name" == "$pack_name" ]]; then + pass "manifest name matches directory ($pack_name)" + else + fail_c "manifest name '$manifest_name' does NOT match directory '$pack_name'" + hint "Change name: in manifest.yaml to '$pack_name'" + fi + + # Type validation + local pack_type + pack_type=$(python3 -c "import yaml; print(yaml.safe_load(open('$manifest')).get('type',''))" 2>/dev/null || echo "") + if [[ "$pack_type" == "agent" || "$pack_type" == "base" ]]; then + pass "manifest type is valid: $pack_type" + else + fail_c "manifest type is invalid: '$pack_type' (must be 'agent' or 'base')" + hint "type: agent # or 'base' for infrastructure-only packs like bedrockify" + fi + + # Params have name + description + default + local param_count + param_count=$(python3 -c "import yaml; d=yaml.safe_load(open('$manifest')); print(len(d.get('params',[])))" 2>/dev/null || echo "0") + if [[ "$param_count" -gt 0 ]]; then + local bad_params + bad_params=$(python3 -c " +import yaml +d = yaml.safe_load(open('$manifest')) +bad = [] +for p in d.get('params', []): + missing = [k for k in ('name','description','default') if k not in p] + if missing: + bad.append(f\"{p.get('name','?')}: missing {', '.join(missing)}\") +if bad: + print('\n'.join(bad)) +" 2>/dev/null || echo "") + if [[ -z "$bad_params" ]]; then + pass "all $param_count params have name, description, default" + else + fail_c "some params incomplete:" + echo "$bad_params" | while IFS= read -r line; do + printf " ${RED}→${NC} %s\n" "$line" + done + fi + else + warn_c "manifest has no params (unusual but valid)" + fi + + # ── 3. install.sh ────────────────────────────────────────────────────────── + section "3. install.sh" + + local install="$pack_dir/install.sh" + if [[ ! -f "$install" ]]; then + fail_c "install.sh missing" + hint "Create $pack_name/install.sh — must source ../common.sh and call write_done_marker" + return + fi + pass "install.sh exists" + + # Executable + if [[ -x "$install" ]]; then + pass "install.sh is executable" + else + fail_c "install.sh is NOT executable" + hint "chmod +x $pack_name/install.sh" + fi + + # Bash shebang + local shebang + shebang=$(head -1 "$install") + if [[ "$shebang" == "#!/usr/bin/env bash" || "$shebang" == "#!/bin/bash" ]]; then + pass "install.sh has bash shebang" + else + fail_c "install.sh has unexpected shebang: $shebang" + hint "First line must be: #!/usr/bin/env bash" + fi + + # set -euo pipefail + if grep -q 'set -euo pipefail' "$install" 2>/dev/null; then + pass "install.sh uses set -euo pipefail" + else + fail_c "install.sh missing 'set -euo pipefail'" + hint "Add 'set -euo pipefail' near the top of install.sh" + fi + + # Sources common.sh + if grep -q 'common\.sh' "$install" 2>/dev/null; then + pass "install.sh sources common.sh" + else + fail_c "install.sh does NOT source common.sh" + hint 'Add: source "\${SCRIPT_DIR}/../common.sh"' + fi + + # Writes done marker + if grep -q 'write_done_marker' "$install" 2>/dev/null; then + pass "install.sh calls write_done_marker" + else + fail_c "install.sh does NOT call write_done_marker" + hint "Add at end: write_done_marker \"$pack_name\"" + fi + + # --help support + if grep -q '\-\-help' "$install" 2>/dev/null; then + pass "install.sh supports --help" + else + warn_c "install.sh has no --help handling (recommended)" + hint "Add a usage() function and check for --help in args" + fi + + # --help actually works + if grep -q '\-\-help' "$install" 2>/dev/null; then + local help_rc=0 + bash "$install" --help >/dev/null 2>&1 || help_rc=$? + if [[ $help_rc -eq 0 ]]; then + pass "install.sh --help exits 0" + else + fail_c "install.sh --help exits $help_rc (should exit 0)" + fi + fi + + # Uses pack_config_get + if grep -q 'pack_config_get' "$install" 2>/dev/null; then + pass "install.sh reads config via pack_config_get" + else + warn_c "install.sh doesn't use pack_config_get (OK if no config needed)" + fi + + # shellcheck (if available) + if command -v shellcheck &>/dev/null; then + if shellcheck -S warning "$install" 2>/dev/null; then + pass "install.sh passes shellcheck" + else + warn_c "install.sh has shellcheck warnings" + hint "Run: shellcheck -S warning $pack_name/install.sh" + fi + else + skip_c "shellcheck not installed — skipping lint" + fi + + # ── 4. shell-profile.sh (agent packs only) ───────────────────────────────── + if [[ "$pack_type" == "agent" ]]; then + section "4. shell-profile.sh" + + local profile="$pack_dir/resources/shell-profile.sh" + if [[ ! -f "$profile" ]]; then + fail_c "resources/shell-profile.sh missing (required for agent packs)" + hint "Create $pack_name/resources/shell-profile.sh with PACK_ALIASES, PACK_BANNER_NAME, PACK_BANNER_EMOJI, PACK_BANNER_COMMANDS" + else + pass "resources/shell-profile.sh exists" + + local required_vars=(PACK_ALIASES PACK_BANNER_NAME PACK_BANNER_EMOJI PACK_BANNER_COMMANDS) + for var in "${required_vars[@]}"; do + if grep -qE "^${var}=" "$profile" 2>/dev/null; then + pass "shell-profile.sh defines $var" + else + fail_c "shell-profile.sh MISSING $var" + case "$var" in + PACK_ALIASES) + hint "PACK_ALIASES='\nalias ${pack_name:0:2}=\"$pack_name\"\n'" ;; + PACK_BANNER_NAME) + hint "PACK_BANNER_NAME=\"${pack_name^} Agent Environment\"" ;; + PACK_BANNER_EMOJI) + hint "PACK_BANNER_EMOJI=\"🤖\"" ;; + PACK_BANNER_COMMANDS) + hint "PACK_BANNER_COMMANDS='\n $pack_name → Run $pack_name\n $pack_name --help → Show options\n'" ;; + esac + fi + done + + # Source test under strict mode + if bash -c "set -euo pipefail; source '$profile'" 2>/dev/null; then + pass "shell-profile.sh sources cleanly under set -euo pipefail" + else + fail_c "shell-profile.sh errors when sourced with set -euo pipefail" + hint "Run: bash -c \"set -euo pipefail; source '$profile'\" to see the error" + fi + fi + fi + + # ── 5. Registry entries ───────────────────────────────────────────────────── + if [[ -n "$repo_root" ]]; then + section "5. Registry Entries" + + local reg_yaml="$repo_root/packs/registry.yaml" + local reg_json="$repo_root/packs/registry.json" + + if [[ -f "$reg_yaml" ]]; then + if python3 -c "import yaml,sys; d=yaml.safe_load(open('$reg_yaml')); sys.exit(0 if '$pack_name' in d.get('packs',{}) else 1)" 2>/dev/null; then + pass "Listed in registry.yaml" + else + fail_c "NOT listed in registry.yaml" + hint "Add '$pack_name' entry under packs: in registry.yaml" + fi + else + skip_c "registry.yaml not found" + fi + + if [[ -f "$reg_json" ]]; then + if jq -e --arg p "$pack_name" '.packs[$p]' "$reg_json" >/dev/null 2>&1; then + pass "Listed in registry.json" + else + fail_c "NOT listed in registry.json" + hint "Add '$pack_name' entry in registry.json under .packs" + fi + else + skip_c "registry.json not found" + fi + else + skip_c "Could not find repo root — skipping registry checks" + fi + + # ── 6. test.sh (recommended) ─────────────────────────────────────────────── + section "6. Tests" + + if [[ -f "$pack_dir/test.sh" ]]; then + pass "test.sh exists" + if [[ -x "$pack_dir/test.sh" ]]; then + pass "test.sh is executable" + else + warn_c "test.sh is not executable" + hint "chmod +x $pack_name/test.sh" + fi + else + warn_c "No test.sh — strongly recommended for CI" + hint "Create $pack_name/test.sh for offline validation (see ironclaw/test.sh or pi/test.sh as examples)" + fi +} + +# ── Main ────────────────────────────────────────────────────────────────────── +TARGETS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --help|-h) usage ;; + --fix) FIX_MODE=true; shift ;; + --all) + # Find all pack directories + REPO=$(find_repo_root ".") + if [[ -z "$REPO" ]]; then + echo "Error: Could not find loki-agent repo root. Run from within the repo." >&2 + exit 1 + fi + for d in "$REPO"/packs/*/; do + [[ -f "$d/manifest.yaml" ]] && TARGETS+=("$d") + done + shift + ;; + *) + TARGETS+=("$1") + shift + ;; + esac +done + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "Error: No pack directory specified." + echo "" + usage +fi + +for target in "${TARGETS[@]}"; do + verify_single_pack "$target" +done + +# ── Summary ─────────────────────────────────────────────────────────────────── +printf "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf "${BOLD} Pack Verification Summary${NC}\n" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf " ${GREEN}Passed:${NC} %d\n" "$PASS" +printf " ${RED}Failed:${NC} %d\n" "$FAIL" +printf " ${YELLOW}Warnings:${NC} %d\n" "$WARN" +printf " ${DIM}Skipped:${NC} %d\n" "$SKIP" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n\n" + +if [[ $FAIL -gt 0 ]]; then + printf "${RED}✗ %d check(s) failed — fix before submitting${NC}\n" "$FAIL" + [[ "$FIX_MODE" == false ]] && printf "${DIM} Tip: re-run with --fix to see fix suggestions${NC}\n" + echo "" + exit 1 +else + printf "${GREEN}✓ Pack is ready to submit!${NC}\n\n" + exit 0 +fi From 45055ad0ed4f70fb88c26c8bc2a3565ff9ae3583 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 06:32:55 +0000 Subject: [PATCH 032/172] ci: bump version to 0.5.13 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e3d6b3b..097818a 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.12" +INSTALLER_VERSION="0.5.13" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From e9fff07761a75a09ac555e289800ac4234625dec Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 1 Apr 2026 06:48:03 +0000 Subject: [PATCH 033/172] fix(ironclaw): install PostgreSQL 15 + pgvector + D-Bus to prevent segfault IronClaw requires PostgreSQL 15+ with pgvector for its workspace/memory. Without a database, it always drops into the onboard wizard, which segfaults on headless EC2 because the secret-service crate (zbus) crashes when no D-Bus session bus is available. Changes to packs/ironclaw/install.sh: - Install dbus-daemon + start system/session bus (prevents segfault) - Persist DBUS_SESSION_BUS_ADDRESS in .bashrc for future SSM sessions - Install PostgreSQL 15 server, init cluster, create role + database - Build pgvector 0.8.0 from source (not in AL2023 repos) - Enable vector extension in ironclaw database - Add DATABASE_URL to .env alongside LLM config - Sanity checks: verify PG connection + pgvector extension - Idempotent: all steps check if already done before acting --- packs/ironclaw/install.sh | 206 ++++++++++++++++++++++++++++++++++---- 1 file changed, 189 insertions(+), 17 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 74518b7..1221433 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -12,15 +12,13 @@ # IronClaw is a single static Rust binary — no Rust/Cargo needed at runtime. # We download the pre-built musl binary from GitHub releases. # -# Note: IronClaw has an `ironclaw onboard` wizard that tries browser-based -# NEAR AI OAuth. We bypass this entirely by writing .env directly with -# LLM_BACKEND=openai_compatible, pointing at bedrockify. +# This script also installs: +# - PostgreSQL 15 + pgvector (IronClaw requires PG with vector extension) +# - D-Bus daemon (IronClaw's secret-service crate needs D-Bus on Linux; +# without it, `ironclaw onboard` segfaults on headless EC2) # -# Known issue: IronClaw may attempt dbus/secret-service for keychain access -# on Linux. On headless EC2, this may fail silently. Since we set -# LLM_BACKEND=openai_compatible with explicit credentials in .env, -# the OS credential store path should not be triggered for LLM access. -# If startup fails with dbus errors, install dbus: sudo dnf install -y dbus +# NEAR AI OAuth is bypassed entirely — we write .env with +# LLM_BACKEND=openai_compatible, pointing at bedrockify. # # Idempotent: safe to re-run. @@ -49,8 +47,8 @@ Options: --bedrockify-port Port where bedrockify listens (default: 8090) --help Show this help message -Note: IronClaw is a CLI tool — no systemd service is created. - NEAR AI OAuth is bypassed; bedrockify handles all LLM access. +Installs: IronClaw binary, PostgreSQL 15 + pgvector, D-Bus. +NEAR AI OAuth is bypassed; bedrockify handles all LLM access. Examples: ./install.sh --region us-east-1 @@ -73,6 +71,9 @@ REGION="${PACK_ARG_REGION}" MODEL="${PACK_ARG_MODEL}" BEDROCKIFY_PORT="${PACK_ARG_BEDROCKIFY_PORT}" +IC_DB_NAME="ironclaw" +IC_DB_USER="ec2-user" + pack_banner "ironclaw" log "region=${REGION} model=${MODEL} bedrockify-port=${BEDROCKIFY_PORT}" @@ -82,8 +83,158 @@ require_cmd curl tar check_bedrockify_health "${BEDROCKIFY_PORT}" -# ── Install IronClaw ────────────────────────────────────────────────────────── -step "Installing IronClaw" +# ── D-Bus (prevents segfault in IronClaw's secret-service crate) ────────────── +step "D-Bus (secret-service dependency)" + +# IronClaw links secret-service + zbus on Linux for keychain access. +# Without a D-Bus session bus, `ironclaw onboard` segfaults. +# We install dbus-daemon and start a session bus so the crate initializes safely. +if command -v dbus-daemon &>/dev/null; then + ok "dbus-daemon already installed" +else + log "Installing dbus-daemon..." + sudo dnf install -y dbus dbus-daemon >/dev/null 2>&1 + ok "dbus-daemon installed" +fi + +# Ensure system D-Bus is running (needed by secret-service) +if ! pgrep -x dbus-daemon &>/dev/null; then + log "Starting D-Bus system bus..." + sudo systemctl start dbus 2>/dev/null || sudo dbus-daemon --system --fork 2>/dev/null || true +fi + +# Ensure session bus env var is available for ec2-user +# This prevents segfaults even if the user runs `ironclaw onboard` manually +if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then + # Launch a session bus for this user if not already running + if command -v dbus-launch &>/dev/null; then + eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true + fi +fi + +# Persist DBUS_SESSION_BUS_ADDRESS in .bashrc so future SSM sessions have it +if ! grep -q 'DBUS_SESSION_BUS_ADDRESS' "${HOME}/.bashrc" 2>/dev/null; then + cat >> "${HOME}/.bashrc" <<'DBUS_BLOCK' + +# D-Bus session bus for IronClaw secret-service (prevents segfault on headless) +if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then + if command -v dbus-launch &>/dev/null; then + eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true + fi +fi +DBUS_BLOCK + ok "D-Bus session bus auto-start added to .bashrc" +fi +ok "D-Bus ready" + +# ── PostgreSQL 15 + pgvector ────────────────────────────────────────────────── +step "PostgreSQL 15" + +# IronClaw requires PostgreSQL 15+ with the pgvector extension for semantic search. +# On AL2023, we install from the Amazon Linux repo; pgvector must be compiled from source. + +if command -v psql &>/dev/null && psql --version 2>/dev/null | grep -q "15\.\|16\.\|17\."; then + ok "PostgreSQL client already installed: $(psql --version)" +else + log "Installing PostgreSQL 15..." + sudo dnf install -y postgresql15 postgresql15-server postgresql15-contrib \ + postgresql15-server-devel >/dev/null 2>&1 + ok "PostgreSQL 15 installed" +fi + +# Initialize database cluster if not done yet +PG_DATA="/var/lib/pgsql/data" +if [[ ! -f "${PG_DATA}/PG_VERSION" ]]; then + log "Initializing PostgreSQL database cluster..." + sudo postgresql-setup --initdb 2>/dev/null \ + || sudo -u postgres /usr/bin/initdb -D "${PG_DATA}" 2>/dev/null \ + || sudo /usr/bin/postgresql-setup initdb 2>/dev/null + ok "PostgreSQL cluster initialized" +else + ok "PostgreSQL cluster already initialized" +fi + +# Configure pg_hba.conf for local peer authentication (ec2-user can connect) +PG_HBA="${PG_DATA}/pg_hba.conf" +if ! sudo grep -q "^local.*${IC_DB_NAME}.*${IC_DB_USER}" "${PG_HBA}" 2>/dev/null; then + log "Configuring pg_hba.conf for local peer auth..." + # Add before the first 'local' line: allow ec2-user to connect to ironclaw db + sudo sed -i "/^local/i local ${IC_DB_NAME} ${IC_DB_USER} peer" "${PG_HBA}" 2>/dev/null \ + || echo "local ${IC_DB_NAME} ${IC_DB_USER} peer" | sudo tee -a "${PG_HBA}" >/dev/null +fi + +# Start PostgreSQL +if sudo systemctl is-active --quiet postgresql 2>/dev/null; then + ok "PostgreSQL already running" + # Reload config in case we changed pg_hba.conf + sudo systemctl reload postgresql 2>/dev/null || true +else + log "Starting PostgreSQL..." + sudo systemctl enable postgresql >/dev/null 2>&1 + sudo systemctl start postgresql + ok "PostgreSQL started and enabled" +fi + +# Create database user (if not exists) and database +if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${IC_DB_USER}'" 2>/dev/null | grep -q 1; then + ok "PostgreSQL role '${IC_DB_USER}' exists" +else + log "Creating PostgreSQL role '${IC_DB_USER}'..." + sudo -u postgres createuser "${IC_DB_USER}" 2>/dev/null || true + ok "PostgreSQL role '${IC_DB_USER}' created" +fi + +if sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${IC_DB_NAME}'" 2>/dev/null | grep -q 1; then + ok "Database '${IC_DB_NAME}' exists" +else + log "Creating database '${IC_DB_NAME}'..." + sudo -u postgres createdb -O "${IC_DB_USER}" "${IC_DB_NAME}" + ok "Database '${IC_DB_NAME}' created (owner: ${IC_DB_USER})" +fi + +# ── pgvector extension ──────────────────────────────────────────────────────── +step "pgvector extension" + +# Check if pgvector is already available +if psql -d "${IC_DB_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name='vector'" 2>/dev/null | grep -q 1; then + ok "pgvector extension already available" +else + log "Building pgvector from source (not available in AL2023 repos)..." + + # Build dependencies + sudo dnf install -y gcc make postgresql15-server-devel >/dev/null 2>&1 + + PGVECTOR_VERSION="0.8.0" + PGVECTOR_DIR="$(mktemp -d)" + + curl -fsSL "https://github.com/pgvector/pgvector/archive/refs/tags/v${PGVECTOR_VERSION}.tar.gz" \ + -o "${PGVECTOR_DIR}/pgvector.tar.gz" + tar xzf "${PGVECTOR_DIR}/pgvector.tar.gz" -C "${PGVECTOR_DIR}" + + ( + cd "${PGVECTOR_DIR}/pgvector-${PGVECTOR_VERSION}" + # pg_config tells make where PG headers and lib dirs are + PG_CONFIG="$(command -v pg_config || echo /usr/bin/pg_config)" + make PG_CONFIG="${PG_CONFIG}" -j"$(nproc)" 2>&1 | tail -3 + sudo make PG_CONFIG="${PG_CONFIG}" install 2>&1 | tail -3 + ) + + rm -rf "${PGVECTOR_DIR}" + ok "pgvector ${PGVECTOR_VERSION} built and installed" +fi + +# Enable the vector extension in the ironclaw database +if psql -d "${IC_DB_NAME}" -tAc "SELECT 1 FROM pg_extension WHERE extname='vector'" 2>/dev/null | grep -q 1; then + ok "pgvector extension already enabled in ${IC_DB_NAME}" +else + log "Enabling pgvector extension in ${IC_DB_NAME}..." + psql -d "${IC_DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null \ + || sudo -u postgres psql -d "${IC_DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS vector;" + ok "pgvector extension enabled" +fi + +# ── Install IronClaw binary ────────────────────────────────────────────────── +step "Installing IronClaw binary" if command -v ironclaw &>/dev/null; then IC_EXISTING="$(ironclaw --version 2>/dev/null || echo unknown)" @@ -137,18 +288,23 @@ step "Configuring IronClaw" mkdir -p "${HOME}/.ironclaw" -# Write .env with bedrockify config — bypasses NEAR AI OAuth entirely +# Write .env with bedrockify + PostgreSQL config — bypasses NEAR AI OAuth entirely cat > "${HOME}/.ironclaw/.env" </dev/null 2>&1; then + ok "PostgreSQL connection: OK (${IC_DB_NAME})" +else + warn "PostgreSQL connection failed — IronClaw may prompt for database setup" +fi + +# Verify pgvector +if psql -d "${IC_DB_NAME}" -tAc "SELECT extversion FROM pg_extension WHERE extname='vector'" 2>/dev/null | grep -q .; then + PGV_VER="$(psql -d "${IC_DB_NAME}" -tAc "SELECT extversion FROM pg_extension WHERE extname='vector'" 2>/dev/null)" + ok "pgvector extension: v${PGV_VER}" +else + warn "pgvector extension not detected — semantic search may not work" +fi + # ── Done ───────────────────────────────────────────────────────────────────── write_done_marker "ironclaw" -printf "\n[PACK:ironclaw] INSTALLED — ironclaw CLI ready (model: %s via bedrockify:%s)\n" \ - "${MODEL}" "${BEDROCKIFY_PORT}" +printf "\n[PACK:ironclaw] INSTALLED — ironclaw CLI ready\n" +printf " model: %s via bedrockify:%s\n" "${MODEL}" "${BEDROCKIFY_PORT}" +printf " database: postgresql://%s@localhost/%s (pgvector enabled)\n" "${IC_DB_USER}" "${IC_DB_NAME}" From d926f8169bf351439c31ce28b217a6548a5f8c7b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 06:48:13 +0000 Subject: [PATCH 034/172] ci: bump version to 0.5.14 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 097818a..dcb8f31 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.13" +INSTALLER_VERSION="0.5.14" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 8cf6771b9d6917b8e5ab2972cd26e7be7e175cb6 Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 1 Apr 2026 06:59:01 +0000 Subject: [PATCH 035/172] fix: persist mise activation to .bashrc so SSM sessions have node/openclaw on PATH mise was activated 5 times during bootstrap execution but never written to .bashrc. When users SSM into the instance, bash starts without mise activation, so node, npm, openclaw, and any other mise-managed tools are not on PATH. Adds idempotent .bashrc block that: 1. Exports ~/.local/bin to PATH (where mise binary lives) 2. Evals mise activate bash (adds mise shims to PATH) --- deploy/bootstrap.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 5dcb311..3bcf76d 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -371,6 +371,15 @@ export PATH="/home/ec2-user/.local/bin:$PATH" eval "$(/home/ec2-user/.local/bin/mise activate bash)" ok "mise installed: $(mise --version 2>/dev/null || echo unknown)" +# Persist mise activation to .bashrc so SSM sessions have node/npm/openclaw on PATH +if ! grep -q 'mise activate' "${HOME}/.bashrc" 2>/dev/null; then + echo '' >> "${HOME}/.bashrc" + echo '# mise — runtime version manager (node, python, etc.)' >> "${HOME}/.bashrc" + echo 'export PATH="${HOME}/.local/bin:${PATH}"' >> "${HOME}/.bashrc" + echo 'eval "$(~/.local/bin/mise activate bash 2>/dev/null)"' >> "${HOME}/.bashrc" + ok "mise activation added to .bashrc" +fi + step "Node.js" export MISE_NODE_VERIFY=false mise use -g node@latest From 1b128775509592886ef66fb14939a5b17e1cbd22 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 06:59:12 +0000 Subject: [PATCH 036/172] ci: bump version to 0.5.15 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index dcb8f31..3dd07fa 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.14" +INSTALLER_VERSION="0.5.15" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 2aa0af522434ec0acbf50ac20b0dbcd1be41c723 Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 1 Apr 2026 07:50:38 +0000 Subject: [PATCH 037/172] fix: ensure ec2-user has passwordless sudo for pack installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pack install scripts (hermes, ironclaw, etc.) run as ec2-user and need sudo for dnf install, systemctl, pg setup, etc. Cloud-init usually creates a NOPASSWD rule, but some AMIs or custom configs may not. Adds /etc/sudoers.d/ec2-user-nopasswd early in bootstrap (before any pack install runs). Idempotent — skips if already present. --- deploy/bootstrap.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 3bcf76d..f3e6f0c 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -323,6 +323,18 @@ ok "SSM log publisher running (pid=$SSM_PUB_PID)" # ---- System updates ---- step "System Updates" + +# Ensure ec2-user has passwordless sudo (cloud-init usually sets this, but +# some AMIs or custom configs may not; pack install scripts need sudo for +# dnf, systemctl, etc.) +if [[ ! -f /etc/sudoers.d/ec2-user-nopasswd ]]; then + echo "ec2-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ec2-user-nopasswd + chmod 440 /etc/sudoers.d/ec2-user-nopasswd + ok "Passwordless sudo configured for ec2-user" +else + ok "Passwordless sudo already configured" +fi + dnf update -y 2>&1 | tail -5 ok "System updated" From 3bb7259649a22b3f5abc407a0ddf0480fe7c9369 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 07:50:49 +0000 Subject: [PATCH 038/172] ci: bump version to 0.5.16 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3dd07fa..8d6c255 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.15" +INSTALLER_VERSION="0.5.16" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From e6ef111bfa1e742eb9e9e51865838e20642f1ea0 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 08:23:08 +0000 Subject: [PATCH 039/172] fix: handle diverged repo on re-deploy (no more ff-only abort) When the remote repo history changes between deploys, 'git pull --ff-only' fails fatally. Now: fetch + try ff-merge, fall back to hard reset with a warning. Install scripts don't need to preserve local edits. --- install.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 8d6c255..191d195 100755 --- a/install.sh +++ b/install.sh @@ -608,8 +608,14 @@ prepare_repo() { info "Cloning loki-agent into ${CLONE_DIR}..." if [[ -d "$CLONE_DIR/.git" ]]; then - info "Directory exists, pulling latest..." - git -C "$CLONE_DIR" pull --ff-only 2>&1 | tail -1 + info "Directory exists, syncing to latest..." + git -C "$CLONE_DIR" fetch origin 2>&1 | tail -1 + local branch + branch=$(git -C "$CLONE_DIR" symbolic-ref --short HEAD 2>/dev/null || echo "main") + if ! git -C "$CLONE_DIR" merge --ff-only "origin/$branch" 2>/dev/null; then + warn "Local repo diverged from remote — resetting to origin/$branch" + git -C "$CLONE_DIR" reset --hard "origin/$branch" 2>&1 | tail -1 + fi clean_stale_terraform "$CLONE_DIR" else rm -rf "$CLONE_DIR" 2>/dev/null || true From 23a3fd0d2e2aefb9a640ba2bf568459c1339ddba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 08:23:23 +0000 Subject: [PATCH 040/172] ci: bump version to 0.5.17 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 191d195..c2aa11c 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.16" +INSTALLER_VERSION="0.5.17" # Deploy method constants DEPLOY_CFN_CONSOLE=1 From 26959b2c961f8fd103343f9da0b3e3fbf9fc92d9 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 08:27:39 +0000 Subject: [PATCH 041/172] feat: add --yes/-y flag for non-interactive install with defaults Adds AUTO_YES mode that skips all interactive prompts: - prompt() returns default value immediately - confirm() always returns true - toggle() uses default value - Clone destination auto-picks ~/.loki-agent - Deploy summary still prints but doesn't pause - Banner indicates auto mode is active Usage: bash install.sh --yes --- install.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/install.sh b/install.sh index c2aa11c..ebc8a3a 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Loki Agent — One-Shot Installer # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh +# Flags: --yes / -y Accept all defaults (non-interactive deploy) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -33,6 +34,14 @@ TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/ma SSM_DOC_NAME="Loki-Session" INSTALLER_VERSION="0.5.17" +# --yes / -y: accept all defaults, minimal prompts +AUTO_YES=false +for arg in "$@"; do + case "$arg" in + --yes|-y) AUTO_YES=true ;; + esac +done + # Deploy method constants DEPLOY_CFN_CONSOLE=1 DEPLOY_CFN_CLI=2 @@ -60,6 +69,10 @@ fail() { echo -e "${RED}✗${NC} $1"; exit 1; } prompt() { local text="$1" var="$2" default="${3:-}" + if [[ "$AUTO_YES" == true && -n "$default" ]]; then + printf -v "$var" '%s' "$default" + return + fi local display="$text"; [[ -n "$default" ]] && display="$text [$default]" read -rp "$(echo -e "${BOLD}${display}:${NC} ")" value printf -v "$var" '%s' "${value:-$default}" @@ -67,6 +80,7 @@ prompt() { confirm() { local text="$1" default="${2:-default_no}" + if [[ "$AUTO_YES" == true ]]; then return 0; fi local hint="[y/N]"; [[ "$default" == "default_yes" ]] && hint="[Y/n]" read -rp "$(echo -e "${BOLD}${text} ${hint}:${NC} ")" answer case "$default" in @@ -77,6 +91,10 @@ confirm() { toggle() { local text="$1" var="$2" default="${3:-true}" + if [[ "$AUTO_YES" == true ]]; then + printf -v "$var" '%s' "$default" + return + fi local hint="[Y/n]"; [[ "$default" == "false" ]] && hint="[y/N]" read -rp "$(echo -e " ${text} ${hint}: ")" answer case "$default" in @@ -199,6 +217,10 @@ show_banner() { echo -e "${BOLD}║ 🤖 Loki Agent — AWS Installer ║${NC}" printf "${BOLD}║${NC} %-42s${BOLD}║${NC}\n" "$version_line" echo -e "${BOLD}╚══════════════════════════════════════════════╝${NC}" + if [[ "$AUTO_YES" == true ]]; then + echo "" + info "Running in auto mode (--yes) — using defaults, minimal prompts" + fi echo "" } @@ -583,6 +605,13 @@ show_summary() { prepare_repo() { echo "" local current; current=$(pwd) + + if [[ "$AUTO_YES" == true ]]; then + # Auto mode: pick ~/.loki-agent (persistent, safe default) + CLONE_DIR="$HOME/.loki-agent" + mkdir -p "$(dirname "$CLONE_DIR")" + info "Clone destination: ${CLONE_DIR} (auto)" + else echo " Clone destination:" echo " 1) Current directory -- ${current}/loki-agent" echo " 2) ~/.loki-agent -- persistent home directory" @@ -603,6 +632,7 @@ prepare_repo() { 3) CLONE_DIR="/tmp/loki-agent-$$" ;; *) CLONE_DIR="${current}/loki-agent" ;; esac + fi echo "" info "Cloning loki-agent into ${CLONE_DIR}..." From 337da293cf6633f1d424c4706461fdb995d064fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 08:28:17 +0000 Subject: [PATCH 042/172] ci: bump version to 0.5.18 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index ebc8a3a..99eff49 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.17" +INSTALLER_VERSION="0.5.18" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From ce1d5ec67dde8eff9fe46b1536606137dc7f97b9 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 08:29:38 +0000 Subject: [PATCH 043/172] docs: add --yes express install to README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 939787f..b19c672 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,12 @@ > curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh > ``` > -> Requires: AWS CLI configured, admin access on a dedicated AWS account. The script walks you through everything. +> **Express install (accept all defaults, zero prompts):** +> ```sh +> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --yes +> ``` +> +> Requires: AWS CLI configured, admin access on a dedicated AWS account. Without `--yes`, the script walks you through everything interactively. > > ⚠️ **We highly recommend deploying Loki in a brand-new, dedicated AWS account.** Loki has admin-level access and LLMs can make mistakes — a clean account limits the blast radius. Start with prototyping work as you learn and get acquainted with its capabilities. Like any powerful tool, it carries risks; isolating it in its own account is the simplest way to manage them. > @@ -35,6 +40,8 @@ Run the install command from the TL;DR above. The installer verifies AWS permissions, lets you select your **agent pack**, instance size, and deployment method (CloudFormation / SAM / Terraform), then deploys automatically. +Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. + **Agent packs available:** | Pack | Description | Instance | Data Volume | |------|-------------|----------|-------------| From 29e4a561722b0e708f56d6580811996b4a176c9c Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Fri, 3 Apr 2026 23:48:58 +0300 Subject: [PATCH 044/172] Revise README title for clarity and conciseness --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b19c672..1aa45b9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# Loki: A Harness for Safely Deploying Stateful Full-Stack Builder Agents based on OpenClaw, Hermes and more into your AWS Account. - +# Loki: Turn OpenClaw and friends into FullStack Builder Agents in your AWS account. [![Loki Agent Demo](https://img.youtube.com/vi/dJSk8DYlHvI/maxresdefault.jpg)](https://www.youtube.com/watch?v=dJSk8DYlHvI) ▶️ [Watch the full walkthrough on YouTube](https://www.youtube.com/watch?v=dJSk8DYlHvI) From 66025bce3e9624c63e59f5e616726b80048b37b2 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 22:50:20 +0000 Subject: [PATCH 045/172] =?UTF-8?q?feat:=20add=20claude-code=20pack=20?= =?UTF-8?q?=E2=80=94=20Anthropic's=20coding=20agent=20with=20native=20Bedr?= =?UTF-8?q?ock=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Native installer (not npm), auto-updates - Direct Bedrock integration via CLAUDE_CODE_USE_BEDROCK=1 - No bedrockify dependency — talks to Bedrock natively - Full tool permissions configured by default - Model pinning via ANTHROPIC_MODEL + ANTHROPIC_DEFAULT_HAIKU_MODEL --- deploy/cloudformation/template.yaml | 1 + deploy/sam/template.yaml | 1 + deploy/terraform/variables.tf | 6 +- packs/claude-code/install.sh | 145 +++++++++++ packs/claude-code/manifest.yaml | 35 +++ packs/claude-code/resources/.gitkeep | 0 packs/claude-code/test.sh | 377 +++++++++++++++++++++++++++ packs/registry.json | 13 + 8 files changed, 575 insertions(+), 3 deletions(-) create mode 100755 packs/claude-code/install.sh create mode 100644 packs/claude-code/manifest.yaml create mode 100644 packs/claude-code/resources/.gitkeep create mode 100755 packs/claude-code/test.sh diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 2c7b636..5c42c98 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -107,6 +107,7 @@ Parameters: Default: openclaw AllowedValues: - openclaw + - claude-code - hermes - pi - ironclaw diff --git a/deploy/sam/template.yaml b/deploy/sam/template.yaml index 3a6bd0c..8f42011 100644 --- a/deploy/sam/template.yaml +++ b/deploy/sam/template.yaml @@ -108,6 +108,7 @@ Parameters: Default: openclaw AllowedValues: - openclaw + - claude-code - hermes - pi - ironclaw diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 6b6f2e2..cc8bdec 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -5,12 +5,12 @@ variable "aws_region" { } variable "pack_name" { - description = "Agent pack to deploy (openclaw, hermes, pi, or ironclaw)" + description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, or ironclaw)" type = string default = "openclaw" validation { - condition = contains(["openclaw", "hermes", "pi", "ironclaw"], var.pack_name) - error_message = "pack_name must be openclaw, hermes, pi, or ironclaw." + condition = contains(["openclaw", "claude-code", "hermes", "pi", "ironclaw"], var.pack_name) + error_message = "pack_name must be openclaw, claude-code, hermes, pi, or ironclaw." } } diff --git a/packs/claude-code/install.sh b/packs/claude-code/install.sh new file mode 100755 index 0000000..42b75b5 --- /dev/null +++ b/packs/claude-code/install.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# packs/claude-code/install.sh — Install Claude Code and configure it for AWS Bedrock +# +# Usage: +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6] \ +# [--haiku-model us.anthropic.claude-haiku-4-5-20251001-v1:0] +# +# Assumes: +# - curl is available +# - EC2 instance has an IAM role with bedrock:InvokeModel permissions +# +# Idempotent: safe to re-run. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" +PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6")" +PACK_ARG_HAIKU_MODEL="$(pack_config_get haiku_model "us.anthropic.claude-haiku-4-5-20251001-v1:0")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat </dev/null; then + fail "AWS credentials not available. Ensure the EC2 instance has an IAM role with Bedrock permissions." +fi +ok "AWS credentials verified (IAM role or env)" + +# ── Install Claude Code ─────────────────────────────────────────────────────── +step "Installing Claude Code" + +if command -v claude &>/dev/null; then + CLAUDE_EXISTING="$(claude --version 2>/dev/null || echo unknown)" + log "claude already installed (${CLAUDE_EXISTING}) — reinstalling" +fi + +# Use the official Claude Code native installer +curl -fsSL https://claude.ai/install.sh | bash + +# Add ~/.local/bin to PATH for current session (installer places binary there) +export PATH="${HOME}/.local/bin:${PATH}" + +if ! command -v claude &>/dev/null; then + fail "claude command not found after install. Check PATH or install output." +fi + +CLAUDE_VERSION="$(claude --version 2>/dev/null || echo unknown)" +ok "Claude Code installed: ${CLAUDE_VERSION}" + +# ── Configure Bedrock environment ───────────────────────────────────────────── +step "Configuring Bedrock environment" + +# Write /etc/profile.d script so env vars are available to all login sessions +cat > /etc/profile.d/claude-code-bedrock.sh < "${HOME}/.claude/settings.json" <<'EOF' +{ + "permissions": { + "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)"], + "deny": [] + } +} +EOF + +chmod 600 "${HOME}/.claude/settings.json" +ok "Claude Code permissions written: ${HOME}/.claude/settings.json" + +# ── Sanity check ───────────────────────────────────────────────────────────── +step "Sanity check" + +CLAUDE_VER="$(claude --version 2>/dev/null || echo unknown)" +ok "claude --version: ${CLAUDE_VER}" + +# ── Done ───────────────────────────────────────────────────────────────────── +write_done_marker "claude-code" +printf "\n[PACK:claude-code] INSTALLED — claude CLI ready (model: %s via Bedrock region: %s)\n" \ + "${MODEL}" "${REGION}" diff --git a/packs/claude-code/manifest.yaml b/packs/claude-code/manifest.yaml new file mode 100644 index 0000000..16b8d4d --- /dev/null +++ b/packs/claude-code/manifest.yaml @@ -0,0 +1,35 @@ +name: claude-code +version: "1.0.0" +type: agent +description: "Claude Code — Anthropic's coding agent with native Bedrock support" + +deps: [] + +requirements: + arch: + - arm64 + - amd64 + os: + - al2023 + - ubuntu2204 + min_instance_type: t4g.large + +params: + - name: region + description: "AWS region for Bedrock" + default: us-east-1 + - name: model + description: "Bedrock model ID for Claude Code (ANTHROPIC_MODEL env var)" + default: "us.anthropic.claude-sonnet-4-6" + - name: haiku-model + description: "Bedrock model ID for Claude Code's Haiku fast-path (ANTHROPIC_DEFAULT_HAIKU_MODEL env var)" + default: "us.anthropic.claude-haiku-4-5-20251001-v1:0" + +health_check: + command: "claude --version" + timeout: 10 + +provides: + commands: + - claude + services: [] diff --git a/packs/claude-code/resources/.gitkeep b/packs/claude-code/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packs/claude-code/test.sh b/packs/claude-code/test.sh new file mode 100755 index 0000000..e9be9e0 --- /dev/null +++ b/packs/claude-code/test.sh @@ -0,0 +1,377 @@ +#!/usr/bin/env bash +# packs/claude-code/test.sh — Unit tests for the Claude Code pack +# +# Validates manifest structure, install.sh interface, profile.d config, +# and settings.json WITHOUT requiring Claude Code to be installed or +# Bedrock credentials to be active. +# +# Usage: bash packs/claude-code/test.sh +# Exit: 0 if all tests pass, 1 otherwise. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACK_DIR="${SCRIPT_DIR}" +PACKS_DIR="${SCRIPT_DIR}/.." +COMMON="${PACKS_DIR}/common.sh" + +# ── Test harness ────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +PASS=0 +FAIL=0 +SKIP=0 + +pass() { printf "${GREEN} ✓${NC} %s\n" "$1"; PASS=$((PASS+1)); } +fail() { printf "${RED} ✗${NC} %s\n" "$1"; FAIL=$((FAIL+1)); } +skip() { printf "${YELLOW} ○${NC} %s\n" "$1 (skipped)"; SKIP=$((SKIP+1)); } +header() { printf "\n${BOLD}${CYAN}%s${NC}\n" "$1"; } + +# ── Test: manifest.yaml structure ───────────────────────────────────────────── +header "Test: manifest.yaml" + +MANIFEST="${PACK_DIR}/manifest.yaml" + +if [[ -f "${MANIFEST}" ]]; then + pass "manifest.yaml exists" +else + fail "manifest.yaml missing" +fi + +if command -v python3 &>/dev/null; then + if python3 -c "import yaml; yaml.safe_load(open('${MANIFEST}'))" 2>/dev/null; then + pass "manifest.yaml is valid YAML" + else + fail "manifest.yaml is invalid YAML" + fi + + # Required keys + for key in name version type description deps params health_check provides; do + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +sys.exit(0 if '${key}' in data else 1) +" 2>/dev/null; then + pass "manifest.yaml has '${key}' key" + else + fail "manifest.yaml missing '${key}' key" + fi + done + + # Name matches folder + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +sys.exit(0 if data.get('name') == 'claude-code' else 1) +" 2>/dev/null; then + pass "manifest.yaml name matches folder (claude-code)" + else + fail "manifest.yaml name does not match folder" + fi + + # deps is empty (no bedrockify) + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +deps = data.get('deps', []) +sys.exit(0 if isinstance(deps, list) and len(deps) == 0 else 1) +" 2>/dev/null; then + pass "manifest.yaml deps is empty (no bedrockify dependency)" + else + fail "manifest.yaml deps should be empty — claude-code talks to Bedrock natively" + fi + + # Health check references claude + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +hc = data.get('health_check', {}).get('command', '') +sys.exit(0 if 'claude' in hc else 1) +" 2>/dev/null; then + pass "manifest.yaml health_check references claude" + else + fail "manifest.yaml health_check does not reference claude" + fi + + # Provides commands include claude + if python3 -c " +import yaml, sys +data = yaml.safe_load(open('${MANIFEST}')) +cmds = data.get('provides', {}).get('commands', []) +sys.exit(0 if 'claude' in cmds else 1) +" 2>/dev/null; then + pass "manifest.yaml provides.commands includes claude" + else + fail "manifest.yaml provides.commands missing claude" + fi +else + skip "manifest.yaml structure tests: python3 not available" +fi + +# ── Test: install.sh interface ──────────────────────────────────────────────── +header "Test: install.sh interface" + +INSTALL="${PACK_DIR}/install.sh" + +if [[ -f "${INSTALL}" ]]; then + pass "install.sh exists" +else + fail "install.sh missing" +fi + +if [[ -x "${INSTALL}" ]]; then + pass "install.sh is executable" +else + fail "install.sh is not executable" +fi + +# Shebang check +SHEBANG="$(head -1 "${INSTALL}")" +if [[ "${SHEBANG}" == "#!/usr/bin/env bash" ]]; then + pass "install.sh has correct shebang" +else + fail "install.sh has unexpected shebang: ${SHEBANG}" +fi + +# Sources common.sh +if grep -q 'source.*common\.sh' "${INSTALL}"; then + pass "install.sh sources common.sh" +else + fail "install.sh does not source common.sh" +fi + +# Writes done marker +if grep -q 'write_done_marker.*claude-code' "${INSTALL}"; then + pass "install.sh writes done marker for 'claude-code'" +else + fail "install.sh does not write done marker" +fi + +# --help exits 0 +HELP_OUT="$(bash "${INSTALL}" --help 2>&1)" && HELP_RC=0 || HELP_RC=$? +if [[ "${HELP_RC}" -eq 0 ]]; then + pass "install.sh --help exits 0" +else + fail "install.sh --help exits ${HELP_RC}" +fi + +if [[ -n "${HELP_OUT}" ]]; then + pass "install.sh --help produces output" +else + fail "install.sh --help produces no output" +fi + +# --help mentions key flags +for flag in --region --model --haiku-model --help; do + if printf '%s' "${HELP_OUT}" | grep -q -- "${flag}"; then + pass "install.sh --help mentions ${flag}" + else + fail "install.sh --help missing ${flag}" + fi +done + +# No functional bedrockify dependency (health check or curl to bedrockify port) +if grep -E 'bedrockify.*running|curl.*bedrockify|bedrockify.*port|BEDROCKIFY_PORT' "${INSTALL}" &>/dev/null; then + fail "install.sh should NOT depend on bedrockify — claude-code uses native Bedrock" +else + pass "install.sh does not depend on bedrockify (correct — uses native Bedrock)" +fi + +# Uses native installer +if grep -q 'claude.ai/install.sh' "${INSTALL}"; then + pass "install.sh uses Claude Code native installer (claude.ai/install.sh)" +else + fail "install.sh does not use Claude Code native installer" +fi + +# ── Test: profile.d config generation ──────────────────────────────────────── +header "Test: /etc/profile.d/claude-code-bedrock.sh generation" + +# Simulate what install.sh writes to /etc/profile.d +generate_bedrock_profile() { + local REGION="$1" + local MODEL="$2" + local HAIKU_MODEL="$3" + cat </dev/null; then + pass "profile.d: valid shell syntax" +else + fail "profile.d: invalid shell syntax" +fi + +# Test with model IDs containing colons (common for Bedrock) +PROFILE_COLON="$(generate_bedrock_profile "eu-west-1" "us.amazon.nova-premier-v1:0" "us.anthropic.claude-haiku-4-5-20251001-v1:0")" +if bash -n <(echo "${PROFILE_COLON}") 2>/dev/null; then + pass "profile.d: valid shell syntax with colon in model ID" +else + fail "profile.d: invalid shell syntax with colon in model ID" +fi + +# ── Test: settings.json structure ──────────────────────────────────────────── +header "Test: ~/.claude/settings.json" + +# The settings.json content is hardcoded (not parameterised) in install.sh +SETTINGS_JSON='{ + "permissions": { + "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)"], + "deny": [] + } +}' + +if command -v python3 &>/dev/null; then + if python3 -c "import json, sys; json.loads(sys.stdin.read())" <<< "${SETTINGS_JSON}" 2>/dev/null; then + pass "settings.json: valid JSON" + else + fail "settings.json: invalid JSON" + fi + + if python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +perms = data.get('permissions', {}) +allow = perms.get('allow', []) +deny = perms.get('deny', []) +assert 'Bash(*)' in allow, 'Bash(*) missing' +assert 'Read(*)' in allow, 'Read(*) missing' +assert 'Write(*)' in allow, 'Write(*) missing' +assert 'Edit(*)' in allow, 'Edit(*) missing' +assert isinstance(deny, list) and len(deny) == 0, 'deny should be empty' +" <<< "${SETTINGS_JSON}" 2>/dev/null; then + pass "settings.json: permissions allow Bash(*), Read(*), Write(*), Edit(*) with empty deny" + else + fail "settings.json: permissions structure invalid" + fi +else + skip "settings.json structure tests: python3 not available" +fi + +# install.sh writes settings.json +if grep -q 'settings.json' "${INSTALL}"; then + pass "install.sh writes settings.json" +else + fail "install.sh does not write settings.json" +fi + +# install.sh creates ~/.claude directory +if grep -q 'mkdir.*\.claude' "${INSTALL}"; then + pass "install.sh creates ~/.claude directory" +else + fail "install.sh does not create ~/.claude directory" +fi + +# ── Test: idempotency patterns ───────────────────────────────────────────────── +header "Test: idempotency patterns" + +if grep -q 'command -v claude' "${INSTALL}"; then + pass "install.sh checks if claude is already installed" +else + fail "install.sh does not check for existing claude installation" +fi + +if grep -q 'mkdir -p' "${INSTALL}"; then + pass "install.sh uses mkdir -p for directory creation" +else + fail "install.sh does not use mkdir -p" +fi + +# ── Test: claude command available (LIVE — skippable) ───────────────────────── +header "Test: claude command (live environment)" + +if command -v claude &>/dev/null; then + CLAUDE_VER="$(claude --version 2>/dev/null || echo unknown)" + pass "claude is installed: ${CLAUDE_VER}" +else + skip "claude not installed — live tests skipped" +fi + +# ── Test: CLAUDE_CODE_USE_BEDROCK in profile.d (LIVE — skippable) ───────────── +header "Test: Bedrock profile.d (live environment)" + +PROFILE_D="/etc/profile.d/claude-code-bedrock.sh" + +if [[ -f "${PROFILE_D}" ]]; then + pass "profile.d exists: ${PROFILE_D}" + if grep -q 'CLAUDE_CODE_USE_BEDROCK=1' "${PROFILE_D}"; then + pass "profile.d sets CLAUDE_CODE_USE_BEDROCK=1" + else + fail "profile.d missing CLAUDE_CODE_USE_BEDROCK=1" + fi +else + skip "${PROFILE_D} not present — live profile.d tests skipped" +fi + +# ── Test: ~/.claude/settings.json (LIVE — skippable) ────────────────────────── +header "Test: ~/.claude/settings.json (live environment)" + +SETTINGS="${HOME}/.claude/settings.json" + +if [[ -f "${SETTINGS}" ]]; then + pass "~/.claude/settings.json exists" + if command -v python3 &>/dev/null; then + if python3 -c "import json; json.load(open('${SETTINGS}'))" 2>/dev/null; then + pass "~/.claude/settings.json is valid JSON" + else + fail "~/.claude/settings.json is invalid JSON" + fi + fi +else + skip "~/.claude/settings.json not present — live settings tests skipped" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +printf "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf "${BOLD} Claude Code Pack Test Results${NC}\n" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" +printf " ${GREEN}Passed:${NC} %d\n" "${PASS}" +printf " ${RED}Failed:${NC} %d\n" "${FAIL}" +printf " ${YELLOW}Skipped:${NC} %d\n" "${SKIP}" +printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n\n" + +if [[ "${FAIL}" -gt 0 ]]; then + printf "${RED}✗ %d test(s) failed${NC}\n\n" "${FAIL}" + exit 1 +else + printf "${GREEN}✓ All tests passed${NC}\n\n" + exit 0 +fi diff --git a/packs/registry.json b/packs/registry.json index ad615d2..8650868 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -66,6 +66,19 @@ "brain": false, "claude_code": false, "experimental": true + }, + "claude-code": { + "type": "agent", + "description": "Claude Code -- Anthropic's coding agent with native Bedrock support", + "deps": [], + "instance_type": "t4g.large", + "root_volume_gb": 40, + "data_volume_gb": 0, + "default_model": "us.anthropic.claude-sonnet-4-6", + "ports": {}, + "brain": false, + "claude_code": false, + "experimental": false } } } From 29b8bb59e4ef830dde6535de29a4392dbbc2677b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 22:50:44 +0000 Subject: [PATCH 046/172] ci: bump version to 0.5.19 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 99eff49..e5b9955 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.18" +INSTALLER_VERSION="0.5.19" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From e0344880d59f8cb56dea8c1d54f4324478c011ca Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 22:54:32 +0000 Subject: [PATCH 047/172] docs: add claude-code pack to README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1aa45b9..96ff7a1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, O | Pack | Description | Instance | Data Volume | |------|-------------|----------|-------------| | **OpenClaw** (default) | Stateful AI agent with 24/7 gateway, persistent memory, Telegram/Discord/Slack | t4g.xlarge recommended | 80GB | +| **Claude Code** | Anthropic's coding agent — native Bedrock support, auto-updates, full tool access | t4g.large recommended | None needed (set to 0) | | **Hermes** *(experimental)* | NousResearch CLI agent — lighter, terminal-focused, self-improving skills | t4g.medium sufficient | None needed (set to 0) | | **Pi** *(experimental)* | Minimal terminal coding harness — read, write, edit, bash tools | t4g.medium sufficient | None needed (set to 0) | | **IronClaw** *(experimental)* | Rust-based AI agent by NEAR AI — static binary, fast startup | t4g.medium sufficient | None needed (set to 0) | @@ -157,8 +158,9 @@ Loki uses a **pack-based architecture** for deploying different AI agent runtime | Pack | Type | Description | |------|------|-------------| -| `bedrockify` | Base (auto-installed) | OpenAI-compatible proxy for Amazon Bedrock. Runs as a systemd daemon on port 8090. All agent packs depend on this. | +| `bedrockify` | Base (auto-installed) | OpenAI-compatible proxy for Amazon Bedrock. Runs as a systemd daemon on port 8090. Most agent packs depend on this. | | `openclaw` | Agent | Full stateful AI agent with 24/7 gateway, persistent memory, multi-channel support (Telegram, Discord, Slack). Includes Claude Code. | +| `claude-code` | Agent | Anthropic's Claude Code CLI. Native Bedrock support (no bedrockify needed), auto-updates, full tool permissions. | | `hermes` | Agent *(experimental)* | NousResearch Hermes CLI agent. Self-improving skills, learning loop, lightweight. Uses bedrockify for model access. | | `pi` | Agent *(experimental)* | Pi Coding Agent. Minimal terminal coding harness with read, write, edit, bash tools. Pure Node.js. | | `ironclaw` | Agent *(experimental)* | IronClaw by NEAR AI. Rust-based agent with shell/file tools, MCP support. Single static binary. | From e136a946247b1484a445280771b382e651185364 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 23:01:11 +0000 Subject: [PATCH 048/172] =?UTF-8?q?fix(claude-code):=20address=20code=20re?= =?UTF-8?q?view=20findings=20=E2=80=94=203=20critical,=203=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: add 'aws' to require_cmd (was missing, caused misleading error) C2: fallback to ~/.claude/bedrock-env.sh when not root (was hardcoded /etc/profile.d/) C3: download installer to temp file before executing (no more curl|bash pipe) W1: fix haiku-model param name to match manifest (hyphen, not underscore) W2: fix description mismatch — em dash in registry.json to match manifest W3: guard PyYAML import in test.sh (skip if not installed, not fail) --- packs/claude-code/install.sh | 31 +++++++++++++++++++++++-------- packs/claude-code/test.sh | 4 ++-- packs/registry.json | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packs/claude-code/install.sh b/packs/claude-code/install.sh index 42b75b5..f1b524a 100755 --- a/packs/claude-code/install.sh +++ b/packs/claude-code/install.sh @@ -21,7 +21,7 @@ source "${SCRIPT_DIR}/../common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── PACK_ARG_REGION="$(pack_config_get region "us-east-1")" PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6")" -PACK_ARG_HAIKU_MODEL="$(pack_config_get haiku_model "us.anthropic.claude-haiku-4-5-20251001-v1:0")" +PACK_ARG_HAIKU_MODEL="$(pack_config_get "haiku-model" "us.anthropic.claude-haiku-4-5-20251001-v1:0")" # ── Help ────────────────────────────────────────────────────────────────────── usage() { @@ -66,7 +66,7 @@ log "region=${REGION} model=${MODEL} haiku-model=${HAIKU_MODEL}" # ── Prerequisites ───────────────────────────────────────────────────────────── step "Checking prerequisites" -require_cmd curl +require_cmd curl aws # Verify AWS credentials are available (instance profile or env vars) if ! aws sts get-caller-identity --region "${REGION}" &>/dev/null; then @@ -83,7 +83,10 @@ if command -v claude &>/dev/null; then fi # Use the official Claude Code native installer -curl -fsSL https://claude.ai/install.sh | bash +# Download first, then execute — avoids partial-download execution race +curl -fsSL https://claude.ai/install.sh -o /tmp/claude-code-install.sh +bash /tmp/claude-code-install.sh +rm -f /tmp/claude-code-install.sh # Add ~/.local/bin to PATH for current session (installer places binary there) export PATH="${HOME}/.local/bin:${PATH}" @@ -98,8 +101,20 @@ ok "Claude Code installed: ${CLAUDE_VERSION}" # ── Configure Bedrock environment ───────────────────────────────────────────── step "Configuring Bedrock environment" -# Write /etc/profile.d script so env vars are available to all login sessions -cat > /etc/profile.d/claude-code-bedrock.sh </dev/null; then + printf '\n[ -f "%s/.claude/bedrock-env.sh" ] && source "%s/.claude/bedrock-env.sh"\n' "${HOME}" "${HOME}" >> "${HOME}/.bashrc" + fi +fi + +mkdir -p "$(dirname "${PROFILE_TARGET}")" +cat > "${PROFILE_TARGET}" </dev/null; then +if command -v python3 &>/dev/null && python3 -c "import yaml" 2>/dev/null; then if python3 -c "import yaml; yaml.safe_load(open('${MANIFEST}'))" 2>/dev/null; then pass "manifest.yaml is valid YAML" else @@ -110,7 +110,7 @@ sys.exit(0 if 'claude' in cmds else 1) fail "manifest.yaml provides.commands missing claude" fi else - skip "manifest.yaml structure tests: python3 not available" + skip "manifest.yaml YAML tests: python3 or pyyaml not available" fi # ── Test: install.sh interface ──────────────────────────────────────────────── diff --git a/packs/registry.json b/packs/registry.json index 8650868..63629d9 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -69,7 +69,7 @@ }, "claude-code": { "type": "agent", - "description": "Claude Code -- Anthropic's coding agent with native Bedrock support", + "description": "Claude Code — Anthropic's coding agent with native Bedrock support", "deps": [], "instance_type": "t4g.large", "root_volume_gb": 40, From 0d24ac3328700c28aa4d80ccf3fc530eff288a2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 23:01:20 +0000 Subject: [PATCH 049/172] ci: bump version to 0.5.20 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e5b9955..7fe74b5 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.19" +INSTALLER_VERSION="0.5.20" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From 9e556a4dbf743b9b1beedb244b1d2c1640f21a6f Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 23:17:57 +0000 Subject: [PATCH 050/172] feat: reuse existing Loki VPC instead of creating new one When an existing Loki-managed VPC is detected in the account/region: - Installer offers to reuse it (default: yes) - --yes mode auto-selects the first existing VPC - VPC quota check skipped when reusing - CFN/Terraform/SAM templates accept ExistingVpcId + ExistingSubnetId - Empty = create new (backward compatible) --- deploy/cloudformation/template.yaml | 26 +++++++-- deploy/sam/template.yaml | 26 +++++++-- deploy/terraform/main.tf | 23 +++++--- deploy/terraform/outputs.tf | 2 +- deploy/terraform/variables.tf | 12 +++++ install.sh | 83 +++++++++++++++++++++++++++-- 6 files changed, 150 insertions(+), 22 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 5c42c98..e9e3f42 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -269,6 +269,16 @@ Parameters: Default: 'loki-agent' Description: "Custom identifier tag applied to all resources. Use to distinguish multiple Loki deployments or mark team ownership (e.g. 'team-alpha', 'dev-loki')." + ExistingVpcId: + Type: String + Default: '' + Description: "Reuse an existing Loki VPC instead of creating a new one. Leave empty to create a new VPC." + + ExistingSubnetId: + Type: String + Default: '' + Description: "Public subnet ID in the existing VPC. Required if ExistingVpcId is set." + # ============================================================================ # CONDITIONS # ============================================================================ @@ -278,6 +288,8 @@ Conditions: IsLiteLLM: !Equals [!Ref ModelMode, 'litellm'] IsApiKey: !Equals [!Ref ModelMode, 'api-key'] IsBedrock: !Equals [!Ref ModelMode, 'bedrock'] + CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] + UseExistingVpc: !Not [!Equals [!Ref ExistingVpcId, '']] # ============================================================================ # RESOURCES @@ -289,6 +301,7 @@ Resources: # -------------------------------------------------------------------------- VPC: Type: AWS::EC2::VPC + Condition: CreateNewVpc Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: true @@ -307,6 +320,7 @@ Resources: InternetGateway: Type: AWS::EC2::InternetGateway + Condition: CreateNewVpc Properties: Tags: - Key: Name @@ -320,12 +334,14 @@ Resources: VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment + Condition: CreateNewVpc Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway PublicSubnet: Type: AWS::EC2::Subnet + Condition: CreateNewVpc Properties: VpcId: !Ref VPC CidrBlock: !Ref PublicSubnetCidr @@ -343,6 +359,7 @@ Resources: PublicRouteTable: Type: AWS::EC2::RouteTable + Condition: CreateNewVpc Properties: VpcId: !Ref VPC Tags: @@ -357,6 +374,7 @@ Resources: PublicRoute: Type: AWS::EC2::Route + Condition: CreateNewVpc DependsOn: VPCGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable @@ -365,6 +383,7 @@ Resources: PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNewVpc Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable @@ -377,7 +396,7 @@ Resources: Properties: GroupName: !Sub '${EnvironmentName}-sg' GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance' - VpcId: !Ref VPC + VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 @@ -907,7 +926,6 @@ Resources: Instance: Type: AWS::EC2::Instance DependsOn: - - VPCGatewayAttachment - BedrockFormCustomResource CreationPolicy: ResourceSignal: @@ -917,7 +935,7 @@ Resources: ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}' IamInstanceProfile: !Ref InstanceProfile KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref 'AWS::NoValue'] - SubnetId: !Ref PublicSubnet + SubnetId: !If [CreateNewVpc, !Ref PublicSubnet, !Ref ExistingSubnetId] SecurityGroupIds: - !Ref InstanceSecurityGroup EbsOptimized: true @@ -1019,7 +1037,7 @@ Outputs: VpcId: Description: VPC ID - Value: !Ref VPC + Value: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupId: Description: Security Group ID diff --git a/deploy/sam/template.yaml b/deploy/sam/template.yaml index 8f42011..1651b63 100644 --- a/deploy/sam/template.yaml +++ b/deploy/sam/template.yaml @@ -270,6 +270,16 @@ Parameters: Default: 'loki-agent' Description: "Custom identifier tag applied to all resources. Use to distinguish multiple Loki deployments or mark team ownership (e.g. 'team-alpha', 'dev-loki')." + ExistingVpcId: + Type: String + Default: '' + Description: "Reuse an existing Loki VPC instead of creating a new one. Leave empty to create a new VPC." + + ExistingSubnetId: + Type: String + Default: '' + Description: "Public subnet ID in the existing VPC. Required if ExistingVpcId is set." + # ============================================================================ # CONDITIONS # ============================================================================ @@ -279,6 +289,8 @@ Conditions: IsLiteLLM: !Equals [!Ref ModelMode, 'litellm'] IsApiKey: !Equals [!Ref ModelMode, 'api-key'] IsBedrock: !Equals [!Ref ModelMode, 'bedrock'] + CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] + UseExistingVpc: !Not [!Equals [!Ref ExistingVpcId, '']] # ============================================================================ # RESOURCES @@ -290,6 +302,7 @@ Resources: # -------------------------------------------------------------------------- VPC: Type: AWS::EC2::VPC + Condition: CreateNewVpc Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: true @@ -308,6 +321,7 @@ Resources: InternetGateway: Type: AWS::EC2::InternetGateway + Condition: CreateNewVpc Properties: Tags: - Key: Name @@ -321,12 +335,14 @@ Resources: VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment + Condition: CreateNewVpc Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway PublicSubnet: Type: AWS::EC2::Subnet + Condition: CreateNewVpc Properties: VpcId: !Ref VPC CidrBlock: !Ref PublicSubnetCidr @@ -344,6 +360,7 @@ Resources: PublicRouteTable: Type: AWS::EC2::RouteTable + Condition: CreateNewVpc Properties: VpcId: !Ref VPC Tags: @@ -358,6 +375,7 @@ Resources: PublicRoute: Type: AWS::EC2::Route + Condition: CreateNewVpc DependsOn: VPCGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable @@ -366,6 +384,7 @@ Resources: PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNewVpc Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable @@ -378,7 +397,7 @@ Resources: Properties: GroupName: !Sub '${EnvironmentName}-sg' GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance' - VpcId: !Ref VPC + VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 @@ -850,7 +869,6 @@ Resources: Instance: Type: AWS::EC2::Instance DependsOn: - - VPCGatewayAttachment - BedrockFormCustomResource CreationPolicy: ResourceSignal: @@ -860,7 +878,7 @@ Resources: ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}' IamInstanceProfile: !Ref InstanceProfile KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref 'AWS::NoValue'] - SubnetId: !Ref PublicSubnet + SubnetId: !If [CreateNewVpc, !Ref PublicSubnet, !Ref ExistingSubnetId] SecurityGroupIds: - !Ref InstanceSecurityGroup EbsOptimized: true @@ -962,7 +980,7 @@ Outputs: VpcId: Description: VPC ID - Value: !Ref VPC + Value: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupId: Description: Security Group ID diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 8898b58..409a335 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -17,12 +17,15 @@ locals { "loki:version" = "1.0" "loki:pack" = var.pack_name } + vpc_id = var.existing_vpc_id != "" ? var.existing_vpc_id : (length(aws_vpc.main) > 0 ? aws_vpc.main[0].id : "") + subnet_id = var.existing_subnet_id != "" ? var.existing_subnet_id : (length(aws_subnet.public) > 0 ? aws_subnet.public[0].id : "") } # ============================================================================ # VPC & Networking # ============================================================================ resource "aws_vpc" "main" { + count = var.existing_vpc_id == "" ? 1 : 0 cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true @@ -33,7 +36,8 @@ resource "aws_vpc" "main" { } resource "aws_internet_gateway" "main" { - vpc_id = aws_vpc.main.id + count = var.existing_vpc_id == "" ? 1 : 0 + vpc_id = aws_vpc.main[0].id tags = merge(local.loki_tags, { Name = "${var.environment_name}-igw" @@ -41,7 +45,8 @@ resource "aws_internet_gateway" "main" { } resource "aws_subnet" "public" { - vpc_id = aws_vpc.main.id + count = var.existing_vpc_id == "" ? 1 : 0 + vpc_id = aws_vpc.main[0].id cidr_block = var.public_subnet_cidr map_public_ip_on_launch = true availability_zone = data.aws_availability_zones.available.names[0] @@ -52,11 +57,12 @@ resource "aws_subnet" "public" { } resource "aws_route_table" "public" { - vpc_id = aws_vpc.main.id + count = var.existing_vpc_id == "" ? 1 : 0 + vpc_id = aws_vpc.main[0].id route { cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.main.id + gateway_id = aws_internet_gateway.main[0].id } tags = merge(local.loki_tags, { @@ -65,8 +71,9 @@ resource "aws_route_table" "public" { } resource "aws_route_table_association" "public" { - subnet_id = aws_subnet.public.id - route_table_id = aws_route_table.public.id + count = var.existing_vpc_id == "" ? 1 : 0 + subnet_id = aws_subnet.public[0].id + route_table_id = aws_route_table.public[0].id } # ============================================================================ @@ -75,7 +82,7 @@ resource "aws_route_table_association" "public" { resource "aws_security_group" "main" { name = "${var.environment_name}-sg" description = "Security group for ${var.environment_name} EC2 instance" - vpc_id = aws_vpc.main.id + vpc_id = local.vpc_id ingress { from_port = 22 @@ -605,7 +612,7 @@ resource "aws_instance" "main" { instance_type = var.instance_type iam_instance_profile = aws_iam_instance_profile.main.name key_name = var.key_pair_name != "" ? var.key_pair_name : null - subnet_id = aws_subnet.public.id + subnet_id = local.subnet_id vpc_security_group_ids = [aws_security_group.main.id] ebs_optimized = true diff --git a/deploy/terraform/outputs.tf b/deploy/terraform/outputs.tf index f8c7ba4..c3220dd 100644 --- a/deploy/terraform/outputs.tf +++ b/deploy/terraform/outputs.tf @@ -15,7 +15,7 @@ output "private_ip" { output "vpc_id" { description = "VPC ID" - value = aws_vpc.main.id + value = local.vpc_id } output "security_group_id" { diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index cc8bdec..9d88546 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -193,3 +193,15 @@ variable "loki_watermark" { default = "loki-agent" description = "Custom identifier tag applied to all resources. Use to distinguish multiple Loki deployments or mark team ownership (e.g. 'team-alpha', 'dev-loki')." } + +variable "existing_vpc_id" { + type = string + default = "" + description = "Reuse an existing Loki VPC. Leave empty to create a new VPC." +} + +variable "existing_subnet_id" { + type = string + default = "" + description = "Public subnet ID in the existing VPC. Required if existing_vpc_id is set." +} diff --git a/install.sh b/install.sh index 7fe74b5..cb200cf 100755 --- a/install.sh +++ b/install.sh @@ -338,12 +338,71 @@ check_existing_deployments() { local count; count=$(echo "$vpcs" | wc -l | tr -d ' ') warn "Found ${count} existing Loki deployment(s) in this account/region:" echo "" + local -a vpc_ids=() while IFS=$'\t' read -r vpc_id watermark method name; do echo -e " ${BOLD}${vpc_id}${NC} watermark=${watermark:-n/a} method=${method:-n/a} name=${name:-n/a}" + vpc_ids+=("$vpc_id") done <<< "$vpcs" echo "" - warn "Deploying another Loki will create a separate VPC and resources." - confirm_or_abort "Continue with a new deployment?" + + # Offer to reuse an existing VPC instead of creating a new one + local reuse_vpc=true + if [[ "$AUTO_YES" == true ]]; then + info "Auto mode: reusing first existing VPC" + else + if ! confirm "Reuse an existing VPC?" "default_yes"; then + reuse_vpc=false + fi + fi + + if [[ "$reuse_vpc" == true ]]; then + local chosen_vpc + if [[ ${#vpc_ids[@]} -eq 1 || "$AUTO_YES" == true ]]; then + chosen_vpc="${vpc_ids[0]}" + info "Using VPC: ${chosen_vpc}" + else + echo "" + echo " Select a VPC to reuse:" + local i + for i in "${!vpc_ids[@]}"; do + echo " $((i+1))) ${vpc_ids[$i]}" + done + echo "" + local vpc_choice + prompt "VPC number" vpc_choice "1" + vpc_choice="${vpc_choice//[^0-9]/}" + [[ -z "$vpc_choice" ]] && vpc_choice=1 + local vpc_idx=$(( vpc_choice - 1 )) + [[ $vpc_idx -lt 0 || $vpc_idx -ge ${#vpc_ids[@]} ]] && vpc_idx=0 + chosen_vpc="${vpc_ids[$vpc_idx]}" + info "Selected VPC: ${chosen_vpc}" + fi + + EXISTING_VPC_ID="$chosen_vpc" + + # Find the public subnet in the chosen VPC + local subnet_id + subnet_id=$(aws ec2 describe-subnets \ + --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=tag:Name,Values=*public*" \ + --query 'Subnets[0].SubnetId' --output text --region "$REGION" 2>/dev/null || echo "None") + if [[ "$subnet_id" == "None" || -z "$subnet_id" ]]; then + subnet_id=$(aws ec2 describe-subnets \ + --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=mapPublicIpOnLaunch,Values=true" \ + --query 'Subnets[0].SubnetId' --output text --region "$REGION" 2>/dev/null || echo "") + fi + + if [[ -n "$subnet_id" && "$subnet_id" != "None" ]]; then + EXISTING_SUBNET_ID="$subnet_id" + ok "Reusing VPC: ${EXISTING_VPC_ID} subnet: ${EXISTING_SUBNET_ID}" + else + warn "Could not find a public subnet in ${chosen_vpc} — creating new VPC instead" + EXISTING_VPC_ID="" + EXISTING_SUBNET_ID="" + fi + else + # User declined reuse — proceed with a new VPC + confirm_or_abort "Continue with a new deployment (new VPC)?" + fi else ok "No existing Loki deployments found" fi @@ -527,8 +586,8 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console, CFN CLI, Terraform # ============================================================================ # ⚠ KEEP THESE THREE ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName InstanceType ModelMode BedrockRegion LokiWatermark EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder) -PARAM_TF_NAMES=(environment_name pack_name instance_type model_mode bedrock_region loki_watermark enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder) +PARAM_CFN_NAMES=(EnvironmentName PackName InstanceType ModelMode BedrockRegion LokiWatermark EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId) +PARAM_TF_NAMES=(environment_name pack_name instance_type model_mode bedrock_region loki_watermark enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder existing_vpc_id existing_subnet_id) PARAM_VALUES=() # populated by build_deploy_params() # Populate PARAM_VALUES from user config (call after collect_config) @@ -545,6 +604,8 @@ build_deploy_params() { "$INSPECTOR" "$ACCESS_ANALYZER" "$CONFIG_RECORDER" + "${EXISTING_VPC_ID:-}" + "${EXISTING_SUBNET_ID:-}" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ @@ -591,6 +652,9 @@ show_summary() { echo -e " ${BOLD}│${NC} Instance: ${INSTANCE_TYPE}" echo -e " ${BOLD}│${NC} Region: ${DEPLOY_REGION}" echo -e " ${BOLD}│${NC} Watermark: ${LOKI_WATERMARK}" + if [[ -n "${EXISTING_VPC_ID:-}" ]]; then + echo -e " ${BOLD}│${NC} VPC: reuse ${EXISTING_VPC_ID} (existing)" + fi echo -e " ${BOLD}│${NC} SecurityHub: ${SECURITY_HUB} GuardDuty: ${GUARDDUTY}" echo -e " ${BOLD}│${NC} Inspector: ${INSPECTOR} Analyzer: ${ACCESS_ANALYZER}" echo -e " ${BOLD}│${NC} Config: ${CONFIG_RECORDER}" @@ -792,6 +856,10 @@ TF_LOCK_TABLE="" TF_WORKDIR="" # Set if Terraform work is moved to /tmp (CloudShell low-disk) PACK_NAME="openclaw" # Default pack; overridden by collect_config +# VPC reuse: set by check_existing_deployments(); empty = create new VPC +EXISTING_VPC_ID="" +EXISTING_SUBNET_ID="" + # ============================================================================ # Deploy: Terraform (option 4) # Auto-install Terraform if not present (works on CloudShell, AL2023, Ubuntu, macOS) @@ -1168,7 +1236,12 @@ main() { preflight_checks choose_deploy_method collect_config - check_vpc_quota # Run after collect_config so we use DEPLOY_REGION + # Skip VPC quota check when reusing an existing VPC + if [[ -z "${EXISTING_VPC_ID:-}" ]]; then + check_vpc_quota # Run after collect_config so we use DEPLOY_REGION + else + ok "Skipping VPC quota check (reusing existing VPC ${EXISTING_VPC_ID})" + fi build_deploy_params # Populate parameter arrays from user config show_summary From 02a1205223192373caaa4fc90eb7cfbccfb0e6a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 23:18:09 +0000 Subject: [PATCH 051/172] ci: bump version to 0.5.21 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index cb200cf..9a02a9b 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.20" +INSTALLER_VERSION="0.5.21" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From 1bacdd4142d0d9b144acd1a886f1f5cffe9f411c Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 23:40:54 +0000 Subject: [PATCH 052/172] fix(vpc-reuse): address 4 critical review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: move check_existing_deployments after collect_config, use DEPLOY_REGION (was using auto-detected REGION before user picks their deploy region) C2: add CFN Rules — ExistingSubnetId required when ExistingVpcId set + AllowedPattern validation on both VPC/subnet ID params C3: add Terraform validation + precondition for same constraint C4: use AWS::StackName in SG GroupName to avoid collision when multiple deployments share a VPC Also applied to SAM template (Rules + AllowedPattern + SG name). --- deploy/cloudformation/template.yaml | 16 ++++++++++++++-- deploy/sam/template.yaml | 14 +++++++++++++- deploy/terraform/main.tf | 13 ++++++++++++- deploy/terraform/variables.tf | 8 ++++++++ install.sh | 11 ++++++----- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index e9e3f42..c01df05 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -273,11 +273,23 @@ Parameters: Type: String Default: '' Description: "Reuse an existing Loki VPC instead of creating a new one. Leave empty to create a new VPC." + AllowedPattern: '^(vpc-[a-z0-9]+)?$' ExistingSubnetId: Type: String Default: '' Description: "Public subnet ID in the existing VPC. Required if ExistingVpcId is set." + AllowedPattern: '^(subnet-[a-z0-9]+)?$' + +# ============================================================================ +# RULES +# ============================================================================ +Rules: + SubnetRequiredWithVpc: + RuleCondition: !Not [!Equals [!Ref ExistingVpcId, '']] + Assertions: + - Assert: !Not [!Equals [!Ref ExistingSubnetId, '']] + AssertDescription: "ExistingSubnetId is required when ExistingVpcId is provided." # ============================================================================ # CONDITIONS @@ -394,8 +406,8 @@ Resources: InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: - GroupName: !Sub '${EnvironmentName}-sg' - GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance' + GroupName: !Sub '${AWS::StackName}-sg' + GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance (${AWS::StackName})' VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupIngress: - IpProtocol: tcp diff --git a/deploy/sam/template.yaml b/deploy/sam/template.yaml index 1651b63..c8bd610 100644 --- a/deploy/sam/template.yaml +++ b/deploy/sam/template.yaml @@ -274,11 +274,23 @@ Parameters: Type: String Default: '' Description: "Reuse an existing Loki VPC instead of creating a new one. Leave empty to create a new VPC." + AllowedPattern: '^(vpc-[a-z0-9]+)?$' ExistingSubnetId: Type: String Default: '' Description: "Public subnet ID in the existing VPC. Required if ExistingVpcId is set." + AllowedPattern: '^(subnet-[a-z0-9]+)?$' + +# ============================================================================ +# RULES +# ============================================================================ +Rules: + SubnetRequiredWithVpc: + RuleCondition: !Not [!Equals [!Ref ExistingVpcId, '']] + Assertions: + - Assert: !Not [!Equals [!Ref ExistingSubnetId, '']] + AssertDescription: "ExistingSubnetId is required when ExistingVpcId is provided." # ============================================================================ # CONDITIONS @@ -395,7 +407,7 @@ Resources: InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: - GroupName: !Sub '${EnvironmentName}-sg' + GroupName: !Sub '${AWS::StackName}-sg' GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance' VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] SecurityGroupIngress: diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 409a335..f18b296 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -21,6 +21,17 @@ locals { subnet_id = var.existing_subnet_id != "" ? var.existing_subnet_id : (length(aws_subnet.public) > 0 ? aws_subnet.public[0].id : "") } +# Validate: if existing_vpc_id is set, existing_subnet_id must also be set +resource "terraform_data" "vpc_subnet_validation" { + count = var.existing_vpc_id != "" && var.existing_subnet_id == "" ? 1 : 0 + lifecycle { + precondition { + condition = false + error_message = "existing_subnet_id is required when existing_vpc_id is set." + } + } +} + # ============================================================================ # VPC & Networking # ============================================================================ @@ -80,7 +91,7 @@ resource "aws_route_table_association" "public" { # Security Group # ============================================================================ resource "aws_security_group" "main" { - name = "${var.environment_name}-sg" + name = "${var.environment_name}-${var.pack_name}-sg" description = "Security group for ${var.environment_name} EC2 instance" vpc_id = local.vpc_id diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 9d88546..3196fe1 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -198,10 +198,18 @@ variable "existing_vpc_id" { type = string default = "" description = "Reuse an existing Loki VPC. Leave empty to create a new VPC." + validation { + condition = var.existing_vpc_id == "" || can(regex("^vpc-[a-z0-9]+$", var.existing_vpc_id)) + error_message = "existing_vpc_id must be empty or a valid VPC ID (vpc-xxx)." + } } variable "existing_subnet_id" { type = string default = "" description = "Public subnet ID in the existing VPC. Required if existing_vpc_id is set." + validation { + condition = var.existing_subnet_id == "" || can(regex("^subnet-[a-z0-9]+$", var.existing_subnet_id)) + error_message = "existing_subnet_id must be empty or a valid subnet ID (subnet-xxx)." + } } diff --git a/install.sh b/install.sh index 9a02a9b..a26c5fb 100755 --- a/install.sh +++ b/install.sh @@ -250,7 +250,6 @@ preflight_checks() { confirm_or_abort "Deploy to account ${ACCOUNT_ID} in ${REGION}?" "default_yes" check_permissions - check_existing_deployments } check_vpc_quota() { @@ -325,12 +324,13 @@ check_permissions() { } check_existing_deployments() { + local check_region="${DEPLOY_REGION:-$REGION}" echo "" - info "Checking for existing Loki deployments..." + info "Checking for existing Loki deployments in ${check_region}..." local vpcs vpcs=$(aws ec2 describe-vpcs \ --filters "Name=tag:loki:managed,Values=true" \ - --region "$REGION" \ + --region "$check_region" \ --query 'Vpcs[*].[VpcId, Tags[?Key==`loki:watermark`].Value|[0], Tags[?Key==`loki:deploy-method`].Value|[0], Tags[?Key==`Name`].Value|[0]]' \ --output text 2>/dev/null || echo "") @@ -384,11 +384,11 @@ check_existing_deployments() { local subnet_id subnet_id=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=tag:Name,Values=*public*" \ - --query 'Subnets[0].SubnetId' --output text --region "$REGION" 2>/dev/null || echo "None") + --query 'Subnets[0].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "None") if [[ "$subnet_id" == "None" || -z "$subnet_id" ]]; then subnet_id=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=mapPublicIpOnLaunch,Values=true" \ - --query 'Subnets[0].SubnetId' --output text --region "$REGION" 2>/dev/null || echo "") + --query 'Subnets[0].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "") fi if [[ -n "$subnet_id" && "$subnet_id" != "None" ]]; then @@ -1236,6 +1236,7 @@ main() { preflight_checks choose_deploy_method collect_config + check_existing_deployments # Must run AFTER collect_config so DEPLOY_REGION is set # Skip VPC quota check when reusing an existing VPC if [[ -z "${EXISTING_VPC_ID:-}" ]]; then check_vpc_quota # Run after collect_config so we use DEPLOY_REGION From 56ced5fed365ebf318d14d56e74532d3df8b8de2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 23:41:03 +0000 Subject: [PATCH 053/172] ci: bump version to 0.5.22 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index a26c5fb..ee29641 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.21" +INSTALLER_VERSION="0.5.22" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From ebaed4f3a3f6bc7f18ded9136da0522152e7d2ec Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Fri, 3 Apr 2026 23:53:16 +0000 Subject: [PATCH 054/172] fix: remove SAM deploy, fix review findings (C1, W3, W5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove deploy/sam/ entirely — only CFN and Terraform supported now - Remove SAM option from installer menu (renumber: CFN Console=1, CFN CLI=2, Terraform=3) - C1: add reverse CFN Rule + TF precondition (SubnetId without VpcId now blocked) - W3: move region prompt before VPC count query so default env name uses correct region - W5: subnet discovery now verifies IGW route (0.0.0.0/0 → igw-*) instead of trusting tag names or mapPublicIpOnLaunch alone --- deploy/cloudformation/template.yaml | 5 + deploy/sam/README.md | 57 -- deploy/sam/template.yaml | 1011 --------------------------- deploy/terraform/main.tf | 12 +- install.sh | 56 +- 5 files changed, 57 insertions(+), 1084 deletions(-) delete mode 100644 deploy/sam/README.md delete mode 100644 deploy/sam/template.yaml diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index c01df05..dd20d1d 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -290,6 +290,11 @@ Rules: Assertions: - Assert: !Not [!Equals [!Ref ExistingSubnetId, '']] AssertDescription: "ExistingSubnetId is required when ExistingVpcId is provided." + VpcRequiredWithSubnet: + RuleCondition: !Not [!Equals [!Ref ExistingSubnetId, '']] + Assertions: + - Assert: !Not [!Equals [!Ref ExistingVpcId, '']] + AssertDescription: "ExistingVpcId is required when ExistingSubnetId is provided." # ============================================================================ # CONDITIONS diff --git a/deploy/sam/README.md b/deploy/sam/README.md deleted file mode 100644 index abfe64e..0000000 --- a/deploy/sam/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# SAM Deployment - -Deploy Loki using the AWS Serverless Application Model (SAM) CLI. - -## Prerequisites - -- [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) installed - -## Quick Start - -```bash -sam deploy \ - --template-file template.yaml \ - --stack-name my-openclaw \ - --region us-east-1 \ - --parameter-overrides \ - EnvironmentName=my-openclaw \ - InstanceType=t4g.xlarge \ - ModelMode=bedrock \ - --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ - --no-confirm-changeset - # Security (all default true — set false for test deploys): - # ParameterKey=EnableSecurityHub,ParameterValue=false \ - # ParameterKey=EnableGuardDuty,ParameterValue=false \ - # ParameterKey=EnableInspector,ParameterValue=false \ - # ParameterKey=EnableAccessAnalyzer,ParameterValue=false \ - # ParameterKey=EnableConfigRecorder,ParameterValue=false \ - # ParameterKey=LokiWatermark,ParameterValue=my-team \ -``` - -Or use guided mode for interactive parameter input: - -```bash -sam deploy --guided --template-file template.yaml -``` - -## What's Different from CloudFormation? - -The SAM template uses `Transform: AWS::Serverless-2016-10-31` and `AWS::Serverless::Function` for the Lambda custom resources. This means: - -- SAM auto-generates IAM execution roles for Lambda functions (using `Policies` instead of separate `AWS::IAM::Role` resources) -- Requires `CAPABILITY_AUTO_EXPAND` in addition to `CAPABILITY_NAMED_IAM` -- Otherwise identical infrastructure and parameters - -## Outputs - -Same as the CloudFormation template — `InstanceId`, `PublicIp`, `SSMConnect`, `RoleArn`, `VpcId`. - -## Notes - -- No S3 bucket required — all Lambda code is inline (`InlineCode`) -- Stack creation takes ~8–10 minutes -- Use `sam delete --stack-name my-openclaw` to tear down - -## Next Steps - -See [Next Steps After Deployment](../README.md#next-steps-after-deployment) for bootstrap scripts setup. diff --git a/deploy/sam/template.yaml b/deploy/sam/template.yaml deleted file mode 100644 index c8bd610..0000000 --- a/deploy/sam/template.yaml +++ /dev/null @@ -1,1011 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: > - OpenClaw Instance - Deploys a fully configured OpenClaw AI assistant on EC2 - within its own VPC. Designed for StackSet deployment across AWS Organization accounts. - -Metadata: - AWS::CloudFormation::Interface: - ParameterGroups: - - Label: - default: "🔧 Basic Configuration" - Parameters: - - EnvironmentName - - PackName - - InstanceType - - DefaultModel - - Label: - default: "🧠 Model Access" - Parameters: - - ModelMode - - BedrockRegion - - LiteLLMBaseUrl - - LiteLLMApiKey - - LiteLLMModel - - ProviderApiKey - - RequestQuotaIncreases - - Label: - default: "🛡️ Security Services" - Parameters: - - EnableSecurityHub - - EnableGuardDuty - - EnableInspector - - EnableAccessAnalyzer - - EnableConfigRecorder - - Label: - default: "💾 Storage" - Parameters: - - RootVolumeSize - - DataVolumeSize - - Label: - default: "🌐 Networking" - Parameters: - - VpcCidr - - PublicSubnetCidr - - SSHAllowedCidr - - KeyPairName - - Label: - default: "⚙️ Advanced" - Parameters: - - OpenClawGatewayPort - - LokiWatermark - ParameterLabels: - EnvironmentName: - default: "Environment Name" - InstanceType: - default: "Instance Size" - DefaultModel: - default: "Default AI Model" - ModelMode: - default: "Model Access Mode" - BedrockRegion: - default: "Bedrock Region" - LiteLLMBaseUrl: - default: "LiteLLM Proxy URL" - LiteLLMApiKey: - default: "LiteLLM API Key" - LiteLLMModel: - default: "LiteLLM Model Name" - ProviderApiKey: - default: "Provider API Key (Anthropic etc.)" - RequestQuotaIncreases: - default: "Auto-Request Bedrock Quota Increases" - EnableSecurityHub: - default: "Enable AWS Security Hub" - EnableGuardDuty: - default: "Enable Amazon GuardDuty" - EnableInspector: - default: "Enable Amazon Inspector" - EnableAccessAnalyzer: - default: "Enable IAM Access Analyzer" - EnableConfigRecorder: - default: "Enable AWS Config Recorder" - RootVolumeSize: - default: "Root Volume Size (GB)" - DataVolumeSize: - default: "Data Volume Size (GB) — set 0 to skip" - VpcCidr: - default: "VPC CIDR Block" - PublicSubnetCidr: - default: "Public Subnet CIDR" - SSHAllowedCidr: - default: "SSH Access CIDR" - KeyPairName: - default: "SSH Key Pair (optional)" - OpenClawGatewayPort: - default: "Gateway Port" - PackName: - default: "Agent Pack" - LokiWatermark: - default: "Loki Watermark Tag" - -# ============================================================================ -# PARAMETERS -# ============================================================================ -Parameters: - PackName: - Type: String - Default: openclaw - AllowedValues: - - openclaw - - claude-code - - hermes - - pi - - ironclaw - Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." - - EnvironmentName: - Type: String - Default: openclaw - Description: "A short name for this deployment (e.g. 'my-openclaw'). Used as prefix for all AWS resources. Lowercase letters, numbers, and hyphens only." - AllowedPattern: '[a-z0-9-]+' - MaxLength: 24 - - InstanceType: - Type: String - Default: t4g.xlarge - Description: "EC2 instance size. t4g.medium works for light use. t4g.xlarge recommended for production. All options are ARM64 Graviton." - AllowedValues: - - t4g.medium - - t4g.large - - t4g.xlarge - - t4g.2xlarge - - m7g.medium - - m7g.large - - m7g.xlarge - - c7g.large - - c7g.xlarge - - VpcCidr: - Type: String - Default: '10.0.0.0/16' - Description: "CIDR block for the new VPC. Change only if it conflicts with existing VPCs in your account." - AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/\d{1,2}' - - PublicSubnetCidr: - Type: String - Default: '10.0.1.0/24' - Description: "CIDR for the public subnet. Must be within the VPC CIDR range." - AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/\d{1,2}' - - SSHAllowedCidr: - Type: String - Default: '127.0.0.1/32' - Description: "IP range allowed to SSH. Default 127.0.0.1/32 disables SSH entirely — use AWS SSM Session Manager instead (recommended). Set to your-ip/32 to enable SSH." - AllowedPattern: '(\d{1,3}\.){3}\d{1,3}/\d{1,2}' - - RootVolumeSize: - Type: Number - Default: 40 - MinValue: 20 - MaxValue: 200 - Description: "Root disk size in GB. 40GB is sufficient for most deployments." - - DataVolumeSize: - Type: Number - Default: 80 - MinValue: 0 - MaxValue: 500 - Description: "Separate data volume for OpenClaw state and workspaces. Set to 0 to skip (uses root volume instead). 80GB recommended for OpenClaw, 0 for Hermes." - - KeyPairName: - Type: String - Default: '' - Description: "EC2 key pair for SSH access. Leave blank to skip — SSM Session Manager is the recommended access method." - - OpenClawGatewayPort: - Type: Number - Default: 3001 - MinValue: 1024 - MaxValue: 65535 - Description: "Internal port for the OpenClaw gateway service. Change only if port 3001 conflicts with other services." - - BedrockRegion: - Type: String - Default: us-east-1 - Description: "AWS region for Bedrock API calls. us-east-1 has the widest model selection." - AllowedValues: - - us-east-1 - - us-west-2 - - eu-west-1 - - eu-central-1 - - eu-north-1 - - ap-northeast-1 - - ap-southeast-1 - - DefaultModel: - Type: String - Default: 'us.anthropic.claude-opus-4-6-v1' - Description: "The primary AI model. Claude Opus 4.6 is recommended for best performance. Used when ModelMode is 'bedrock'." - - ModelMode: - Type: String - Default: 'bedrock' - AllowedValues: - - litellm - - api-key - - bedrock - Description: "How OpenClaw connects to AI models. 'bedrock' uses AWS Bedrock (recommended, no extra keys needed). 'litellm' routes through a LiteLLM proxy. 'api-key' uses a provider API key directly." - - LiteLLMBaseUrl: - Type: String - Default: '' - Description: "URL of your LiteLLM proxy server. Only needed when Model Access Mode is 'litellm'. Leave empty otherwise." - - LiteLLMApiKey: - Type: String - Default: '' - NoEcho: true - Description: "API key for authenticating with the LiteLLM proxy. Only needed when Model Access Mode is 'litellm'." - - LiteLLMModel: - Type: String - Default: 'claude-opus-4-6' - Description: "Default model alias on your LiteLLM proxy (e.g. 'claude-opus-4-6'). Only used when Model Access Mode is 'litellm'." - - ProviderApiKey: - Type: String - Default: '' - NoEcho: true - Description: "Direct API key from your AI provider (e.g. Anthropic). Only needed when Model Access Mode is 'api-key'." - - RequestQuotaIncreases: - Type: String - Default: 'false' - AllowedValues: ['true', 'false'] - Description: "Automatically request higher Bedrock rate limits during deployment. Set to 'true' if you expect heavy usage." - - EnableSecurityHub: - Type: String - Default: 'true' - AllowedValues: ['true', 'false'] - Description: "AWS Security Hub aggregates security findings from multiple services into a single dashboard. Enables CIS Benchmarks and AWS Foundational Security Best Practices standards. (~$0.001 per finding/month)" - - EnableGuardDuty: - Type: String - Default: 'true' - AllowedValues: ['true', 'false'] - Description: "Amazon GuardDuty provides intelligent threat detection by analyzing CloudTrail, VPC Flow Logs, and DNS queries. Alerts on suspicious activity like cryptocurrency mining, data exfiltration, or unauthorized access. (~$4/million events)" - - EnableInspector: - Type: String - Default: 'true' - AllowedValues: ['true', 'false'] - Description: "Amazon Inspector automatically scans EC2 instances, container images, and Lambda functions for software vulnerabilities and unintended network exposure. (~$0.01-$1.25 per resource/month)" - - EnableAccessAnalyzer: - Type: String - Default: 'true' - AllowedValues: ['true', 'false'] - Description: "IAM Access Analyzer identifies resources shared with external entities and validates IAM policies. Helps ensure least-privilege access. (Free)" - - EnableConfigRecorder: - Type: String - Default: 'true' - AllowedValues: ['true', 'false'] - Description: "AWS Config records resource configuration changes and evaluates compliance against rules. Required by Security Hub for many checks. (~$0.003 per item recorded/month)" - - LokiWatermark: - Type: String - Default: 'loki-agent' - Description: "Custom identifier tag applied to all resources. Use to distinguish multiple Loki deployments or mark team ownership (e.g. 'team-alpha', 'dev-loki')." - - ExistingVpcId: - Type: String - Default: '' - Description: "Reuse an existing Loki VPC instead of creating a new one. Leave empty to create a new VPC." - AllowedPattern: '^(vpc-[a-z0-9]+)?$' - - ExistingSubnetId: - Type: String - Default: '' - Description: "Public subnet ID in the existing VPC. Required if ExistingVpcId is set." - AllowedPattern: '^(subnet-[a-z0-9]+)?$' - -# ============================================================================ -# RULES -# ============================================================================ -Rules: - SubnetRequiredWithVpc: - RuleCondition: !Not [!Equals [!Ref ExistingVpcId, '']] - Assertions: - - Assert: !Not [!Equals [!Ref ExistingSubnetId, '']] - AssertDescription: "ExistingSubnetId is required when ExistingVpcId is provided." - -# ============================================================================ -# CONDITIONS -# ============================================================================ -Conditions: - HasKeyPair: !Not [!Equals [!Ref KeyPairName, '']] - HasDataVolume: !Not [!Equals [!Ref DataVolumeSize, 0]] - IsLiteLLM: !Equals [!Ref ModelMode, 'litellm'] - IsApiKey: !Equals [!Ref ModelMode, 'api-key'] - IsBedrock: !Equals [!Ref ModelMode, 'bedrock'] - CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] - UseExistingVpc: !Not [!Equals [!Ref ExistingVpcId, '']] - -# ============================================================================ -# RESOURCES -# ============================================================================ -Resources: - - # -------------------------------------------------------------------------- - # VPC & Networking - # -------------------------------------------------------------------------- - VPC: - Type: AWS::EC2::VPC - Condition: CreateNewVpc - Properties: - CidrBlock: !Ref VpcCidr - EnableDnsSupport: true - EnableDnsHostnames: true - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-vpc' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - Key: loki:pack - Value: !Ref PackName - - InternetGateway: - Type: AWS::EC2::InternetGateway - Condition: CreateNewVpc - Properties: - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-igw' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - VPCGatewayAttachment: - Type: AWS::EC2::VPCGatewayAttachment - Condition: CreateNewVpc - Properties: - VpcId: !Ref VPC - InternetGatewayId: !Ref InternetGateway - - PublicSubnet: - Type: AWS::EC2::Subnet - Condition: CreateNewVpc - Properties: - VpcId: !Ref VPC - CidrBlock: !Ref PublicSubnetCidr - MapPublicIpOnLaunch: true - AvailabilityZone: !Select [0, !GetAZs ''] - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-public' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - PublicRouteTable: - Type: AWS::EC2::RouteTable - Condition: CreateNewVpc - Properties: - VpcId: !Ref VPC - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-public-routes' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - PublicRoute: - Type: AWS::EC2::Route - Condition: CreateNewVpc - DependsOn: VPCGatewayAttachment - Properties: - RouteTableId: !Ref PublicRouteTable - DestinationCidrBlock: '0.0.0.0/0' - GatewayId: !Ref InternetGateway - - PublicSubnetRouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Condition: CreateNewVpc - Properties: - SubnetId: !Ref PublicSubnet - RouteTableId: !Ref PublicRouteTable - - # -------------------------------------------------------------------------- - # Security Group - # -------------------------------------------------------------------------- - InstanceSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: !Sub '${AWS::StackName}-sg' - GroupDescription: !Sub 'Security group for ${EnvironmentName} EC2 instance' - VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 22 - ToPort: 22 - CidrIp: !Ref SSHAllowedCidr - Description: SSH access - SecurityGroupEgress: - - IpProtocol: '-1' - CidrIp: '0.0.0.0/0' - Description: All outbound traffic - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-sg' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - # -------------------------------------------------------------------------- - # IAM Role & Instance Profile - # -------------------------------------------------------------------------- - InstanceRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${EnvironmentName}-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: ec2.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - - arn:aws:iam::aws:policy/AdministratorAccess - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-role' - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - InstanceProfile: - Type: AWS::IAM::InstanceProfile - Properties: - InstanceProfileName: !Sub '${EnvironmentName}-profile' - Roles: - - !Ref InstanceRole - - # -------------------------------------------------------------------------- - # Bedrock Model Access (SAM Serverless Function + Custom Resource) - # -------------------------------------------------------------------------- - BedrockFormFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub '${EnvironmentName}-bedrock-form' - Runtime: python3.12 - Handler: index.handler - Timeout: 120 - Policies: - - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - bedrock:PutUseCaseForModelAccess - - bedrock:GetUseCaseForModelAccess - Resource: '*' - - Effect: Allow - Action: - - servicequotas:RequestServiceQuotaIncrease - - servicequotas:GetServiceQuota - - servicequotas:ListRequestedServiceQuotaChangeHistory - Resource: '*' - InlineCode: | - import json, time, urllib.request, boto3, traceback - - def send_response(event, context, status, reason='', data={}): - # CFN responses must be under 4096 bytes total - reason_str = (reason or f'See CW: {context.log_stream_name}')[:256] - phys_id = (context.log_stream_name or 'custom-resource')[-64:] - safe_data = {k: str(v)[:128] for k, v in (data or {}).items()} - body = json.dumps({ - 'Status': status, 'Reason': reason_str, - 'PhysicalResourceId': phys_id, - 'StackId': event['StackId'], - 'RequestId': event['RequestId'], - 'LogicalResourceId': event['LogicalResourceId'], - 'Data': safe_data if len(json.dumps(safe_data)) < 1024 else {} - }).encode() - req = urllib.request.Request(event['ResponseURL'], data=body, - headers={'Content-Type': 'application/json', 'Content-Length': len(body)}, - method='PUT') - urllib.request.urlopen(req) - - def request_quota_increases(): - """Request higher Bedrock quotas for Opus 4.6 cross-region inference.""" - sq = boto3.client('servicequotas', region_name='us-east-1') - quotas = [ - ('L-11DFF789', 1000, 'Cross-region RPM Opus 4.6'), - ('L-0AD9BBE8', 4000000, 'Cross-region TPM Opus 4.6'), - ] - for code, desired, name in quotas: - try: - current = sq.get_service_quota(ServiceCode='bedrock', QuotaCode=code) - current_val = current['Quota']['Value'] - if current_val >= desired: - print(f"[OK] {name}: already at {current_val}") - continue - sq.request_service_quota_increase( - ServiceCode='bedrock', QuotaCode=code, DesiredValue=desired - ) - print(f"[OK] {name}: requested {current_val} -> {desired}") - except Exception as e: - print(f"[WARN] {name} quota request failed: {e}") - - def handler(event, context): - print(f"[INFO] Event: {json.dumps(event)}") - if event['RequestType'] == 'Delete': - send_response(event, context, 'SUCCESS', 'Delete is a no-op') - return - - client = boto3.client('bedrock', region_name='us-east-1') - form_payload = json.dumps({ - "companyName": "My Company", - "companyWebsite": "https://example.com", - "intendedUsers": "0", - "industryOption": "Education", - "otherIndustryOption": "", - "useCases": "AI development" - }).encode() - - try: - # Check if already submitted - try: - existing = client.get_use_case_for_model_access() - print(f"[OK] Form already submitted: {existing.get('formData', '')[:50]}...") - if event.get('ResourceProperties', {}).get('RequestQuotas') == 'true': - request_quota_increases() - send_response(event, context, 'SUCCESS', 'Form already submitted', - {'FormStatus': 'ALREADY_SUBMITTED'}) - return - except client.exceptions.ResourceNotFoundException: - print("[INFO] Form not yet submitted, submitting now...") - - # Submit the form - print(f"[INFO] Submitting form ({len(form_payload)} bytes)") - client.put_use_case_for_model_access(formData=form_payload) - print("[OK] Form submitted successfully") - - # Verify it was accepted - time.sleep(2) - try: - verify = client.get_use_case_for_model_access() - print(f"[OK] Form verified: {verify.get('formData', '')[:50]}...") - except client.exceptions.ResourceNotFoundException: - print("[WARN] Form not found after submission - may need manual action") - - # Request Bedrock quota increases if enabled - if event.get('ResourceProperties', {}).get('RequestQuotas') == 'true': - request_quota_increases() - - send_response(event, context, 'SUCCESS', 'Form submitted', - {'FormStatus': 'SUBMITTED'}) - - except Exception as e: - tb = traceback.format_exc() - print(f"[FAIL] {e}\n{tb}") - # Always succeed so stack doesn't rollback - send_response(event, context, 'SUCCESS', - f'Form submission failed: {e} - manual action required', - {'FormStatus': 'FAILED', 'Error': str(e)}) - - BedrockFormCustomResource: - Type: Custom::BedrockForm - Properties: - ServiceToken: !GetAtt BedrockFormFunction.Arn - RequestQuotas: !Ref RequestQuotaIncreases - - # -------------------------------------------------------------------------- - # Security Services Enablement - # -------------------------------------------------------------------------- - SecurityEnablementFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub '${EnvironmentName}-security-enable' - Runtime: python3.12 - Handler: index.handler - Timeout: 120 - Policies: - - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - securityhub:EnableSecurityHub - - securityhub:DescribeHub - - securityhub:BatchEnableStandards - - guardduty:CreateDetector - - guardduty:ListDetectors - - inspector2:Enable - - inspector2:BatchGetAccountStatus - - access-analyzer:CreateAnalyzer - - access-analyzer:ListAnalyzers - - config:PutConfigurationRecorder - - config:PutDeliveryChannel - - config:StartConfigurationRecorder - - config:DescribeConfigurationRecorders - - config:DescribeDeliveryChannels - - s3:CreateBucket - - s3:PutBucketPolicy - - s3:GetBucketPolicy - - iam:CreateServiceLinkedRole - - iam:GetRole - Resource: '*' - InlineCode: | - import json, urllib.request, boto3, traceback - - def send_response(event, context, status, reason='', data={}): - # CFN responses must be under 4096 bytes total - reason_str = (reason or f'See CW: {context.log_stream_name}')[:256] - phys_id = (context.log_stream_name or 'custom-resource')[-64:] - safe_data = {k: str(v)[:128] for k, v in (data or {}).items()} - body = json.dumps({ - 'Status': status, 'Reason': reason_str, - 'PhysicalResourceId': phys_id, - 'StackId': event['StackId'], - 'RequestId': event['RequestId'], - 'LogicalResourceId': event['LogicalResourceId'], - 'Data': safe_data if len(json.dumps(safe_data)) < 1024 else {} - }).encode() - req = urllib.request.Request(event['ResponseURL'], data=body, - headers={'Content-Type': 'application/json', 'Content-Length': len(body)}, - method='PUT') - urllib.request.urlopen(req) - - def handler(event, context): - print(f"[INFO] Event: {json.dumps(event)}") - if event['RequestType'] == 'Delete': - send_response(event, context, 'SUCCESS', 'Delete is a no-op') - return - - props = event.get('ResourceProperties', {}) - enable_sh = props.get('EnableSecurityHub', 'true') == 'true' - enable_gd = props.get('EnableGuardDuty', 'true') == 'true' - enable_insp = props.get('EnableInspector', 'true') == 'true' - enable_aa = props.get('EnableAccessAnalyzer', 'true') == 'true' - enable_cfg = props.get('EnableConfigRecorder', 'true') == 'true' - - region = 'us-east-1' - account_id = context.invoked_function_arn.split(':')[4] - results = [] - - # 1. Security Hub - if enable_sh: - try: - sh = boto3.client('securityhub', region_name=region) - try: - sh.describe_hub() - print("[OK] Security Hub already enabled") - except: - sh.enable_security_hub(EnableDefaultStandards=True) - print("[OK] Security Hub enabled") - results.append('SecurityHub:OK') - except Exception as e: - print(f"[WARN] Security Hub: {e}") - results.append(f'SecurityHub:WARN') - else: - results.append('SecurityHub:SKIPPED') - - # 2. GuardDuty - if enable_gd: - try: - gd = boto3.client('guardduty', region_name=region) - detectors = gd.list_detectors()['DetectorIds'] - if detectors: - print(f"[OK] GuardDuty already enabled: {detectors[0]}") - else: - resp = gd.create_detector(Enable=True, FindingPublishingFrequency='FIFTEEN_MINUTES') - print(f"[OK] GuardDuty enabled: {resp['DetectorId']}") - results.append('GuardDuty:OK') - except Exception as e: - print(f"[WARN] GuardDuty: {e}") - results.append('GuardDuty:WARN') - else: - results.append('GuardDuty:SKIPPED') - - # 3. Inspector - if enable_insp: - try: - insp = boto3.client('inspector2', region_name=region) - insp.enable( - resourceTypes=['EC2', 'ECR', 'LAMBDA', 'LAMBDA_CODE'], - accountIds=[account_id] - ) - print("[OK] Inspector enabled (EC2, ECR, Lambda)") - results.append('Inspector:OK') - except Exception as e: - print(f"[WARN] Inspector: {e}") - results.append('Inspector:WARN') - else: - results.append('Inspector:SKIPPED') - - # 4. IAM Access Analyzer - if enable_aa: - try: - aa = boto3.client('accessanalyzer', region_name=region) - analyzers = aa.list_analyzers(type='ACCOUNT')['analyzers'] - if analyzers: - print(f"[OK] Access Analyzer already exists: {analyzers[0]['name']}") - else: - aa.create_analyzer( - analyzerName='account-analyzer', - type='ACCOUNT' - ) - print("[OK] Access Analyzer created") - results.append('AccessAnalyzer:OK') - except Exception as e: - print(f"[WARN] Access Analyzer: {e}") - results.append('AccessAnalyzer:WARN') - else: - results.append('AccessAnalyzer:SKIPPED') - - # 5. Config Recorder - if enable_cfg: - try: - cfg = boto3.client('config', region_name=region) - recorders = cfg.describe_configuration_recorders()['ConfigurationRecorders'] - if recorders: - print(f"[OK] Config recorder already exists: {recorders[0]['name']}") - else: - # Create S3 bucket for Config - s3 = boto3.client('s3', region_name=region) - bucket_name = f'config-bucket-{account_id}-{region}' - try: - s3.create_bucket(Bucket=bucket_name) - s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps({ - 'Version': '2012-10-17', - 'Statement': [{ - 'Sid': 'AWSConfigBucketPermissionsCheck', - 'Effect': 'Allow', - 'Principal': {'Service': 'config.amazonaws.com'}, - 'Action': 's3:GetBucketAcl', - 'Resource': f'arn:aws:s3:::{bucket_name}' - }, { - 'Sid': 'AWSConfigBucketDelivery', - 'Effect': 'Allow', - 'Principal': {'Service': 'config.amazonaws.com'}, - 'Action': 's3:PutObject', - 'Resource': f'arn:aws:s3:::{bucket_name}/*', - 'Condition': {'StringEquals': {'s3:x-amz-acl': 'bucket-owner-full-control'}} - }] - })) - except s3.exceptions.BucketAlreadyOwnedByYou: - pass - except Exception as be: - print(f"[WARN] Config bucket: {be}") - - try: - cfg.put_configuration_recorder(ConfigurationRecorder={ - 'name': 'default', - 'roleARN': f'arn:aws:iam::{account_id}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig', - 'recordingGroup': {'allSupported': True, 'includeGlobalResourceTypes': True} - }) - cfg.put_delivery_channel(DeliveryChannel={ - 'name': 'default', - 's3BucketName': bucket_name, - }) - cfg.start_configuration_recorder(ConfigurationRecorderName='default') - print("[OK] Config recorder started") - except Exception as ce: - print(f"[WARN] Config recorder: {ce}") - results.append('Config:OK') - except Exception as e: - print(f"[WARN] Config: {e}") - results.append('Config:WARN') - else: - results.append('Config:SKIPPED') - - send_response(event, context, 'SUCCESS', - f'Security services: {", ".join(results)}', - {'Results': ', '.join(results)}) - - SecurityEnablementCustomResource: - Type: Custom::SecurityEnablement - Properties: - ServiceToken: !GetAtt SecurityEnablementFunction.Arn - EnableSecurityHub: !Ref EnableSecurityHub - EnableGuardDuty: !Ref EnableGuardDuty - EnableInspector: !Ref EnableInspector - EnableAccessAnalyzer: !Ref EnableAccessAnalyzer - EnableConfigRecorder: !Ref EnableConfigRecorder - - # -------------------------------------------------------------------------- - # Admin Console User - # -------------------------------------------------------------------------- - AdminUser: - Type: AWS::IAM::User - Properties: - UserName: !Sub '${EnvironmentName}-admin' - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AdministratorAccess - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-admin' - - Key: ManagedBy - Value: StackSet - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - AdminAccessKey: - Type: AWS::IAM::AccessKey - Properties: - UserName: !Ref AdminUser - - AdminSetupFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub '${EnvironmentName}-admin-setup' - Runtime: python3.12 - Handler: index.handler - Timeout: 30 - InlineCode: | - import json, urllib.request - - def send_response(event, context, status, reason='', data={}): - # CFN responses must be under 4096 bytes total - reason_str = (reason or f'See CW: {context.log_stream_name}')[:256] - phys_id = (context.log_stream_name or 'custom-resource')[-64:] - safe_data = {k: str(v)[:128] for k, v in (data or {}).items()} - body = json.dumps({ - 'Status': status, 'Reason': reason_str, - 'PhysicalResourceId': phys_id, - 'StackId': event['StackId'], - 'RequestId': event['RequestId'], - 'LogicalResourceId': event['LogicalResourceId'], - 'Data': safe_data if len(json.dumps(safe_data)) < 1024 else {} - }).encode() - req = urllib.request.Request(event['ResponseURL'], data=body, - headers={'Content-Type': 'application/json', 'Content-Length': len(body)}, - method='PUT') - urllib.request.urlopen(req) - - def handler(event, context): - # No-op: admin user has API access keys only (no console login) - print(f"[INFO] Event type: {event['RequestType']}") - send_response(event, context, 'SUCCESS', 'No-op — admin user has API keys only') - - AdminSetupCustomResource: - Type: Custom::AdminSetup - DependsOn: - - AdminUser - Properties: - ServiceToken: !GetAtt AdminSetupFunction.Arn - AccountId: !Ref 'AWS::AccountId' - Region: !Ref 'AWS::Region' - AdminUsername: !Sub '${EnvironmentName}-admin' - - # -------------------------------------------------------------------------- - # EC2 Instance - # -------------------------------------------------------------------------- - Instance: - Type: AWS::EC2::Instance - DependsOn: - - BedrockFormCustomResource - CreationPolicy: - ResourceSignal: - Timeout: PT30M - Properties: - InstanceType: !Ref InstanceType - ImageId: !Sub '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}' - IamInstanceProfile: !Ref InstanceProfile - KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref 'AWS::NoValue'] - SubnetId: !If [CreateNewVpc, !Ref PublicSubnet, !Ref ExistingSubnetId] - SecurityGroupIds: - - !Ref InstanceSecurityGroup - EbsOptimized: true - BlockDeviceMappings: - - DeviceName: /dev/xvda - Ebs: - VolumeSize: !Ref RootVolumeSize - VolumeType: gp3 - DeleteOnTermination: true - Encrypted: true - - !If - - HasDataVolume - - DeviceName: /dev/sdb - Ebs: - VolumeSize: !Ref DataVolumeSize - VolumeType: gp3 - DeleteOnTermination: false - Encrypted: true - - !Ref AWS::NoValue - Tags: - - Key: Name - Value: !Sub '${EnvironmentName}-instance' - - Key: Application - Value: OpenClaw - - Key: loki:managed - Value: 'true' - - Key: loki:watermark - Value: !Ref LokiWatermark - - Key: loki:deploy-method - Value: sam - - Key: loki:version - Value: '1.0' - - Key: loki:pack - Value: !Ref PackName - UserData: - Fn::Base64: !Sub | - #!/bin/bash - set -euo pipefail - # Export all params as env vars for the bootstrap script - export ACCT_ID="${AWS::AccountId}" - export REGION="${AWS::Region}" - export STACK_NAME="${AWS::StackName}" - export DEFAULT_MODEL="${DefaultModel}" - export BEDROCK_REGION="${BedrockRegion}" - export GW_PORT="${OpenClawGatewayPort}" - export MODEL_MODE="${ModelMode}" - export LITELLM_BASE_URL="${LiteLLMBaseUrl}" - export LITELLM_API_KEY="${LiteLLMApiKey}" - export LITELLM_MODEL="${LiteLLMModel}" - export PROVIDER_API_KEY="${ProviderApiKey}" - export PACK_NAME="${PackName}" - # Publish failure to SSM and signal CFN on any error - trap ' - aws ssm put-parameter --name "/loki/setup-status" \ - --value "FAILED" --type String --overwrite --region "$REGION" 2>/dev/null || true - aws ssm put-parameter --name "/loki/setup-step" \ - --value "FAILED: userdata error at line $LINENO" \ - --type String --overwrite --region "$REGION" 2>/dev/null || true - touch /tmp/loki-bootstrap-done - /opt/aws/bin/cfn-signal -e 1 --stack "${STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true - ' ERR - # Ensure git is available (not present on all AMIs) - command -v git &>/dev/null || dnf install -y git 2>/dev/null || yum install -y git - # Clone repo with retry (GitHub blips shouldn't kill bootstrap) - _cloned=false - for _attempt in 1 2 3; do - git clone --depth 1 https://github.com/inceptionstack/loki-agent.git /tmp/loki-agent && _cloned=true && break - echo "git clone failed (attempt $_attempt), retrying in 10s..." && sleep 10 - done - if [[ "$_cloned" != "true" ]]; then - echo "FATAL: git clone failed after 3 attempts" >&2 - exit 1 - fi - bash /tmp/loki-agent/deploy/bootstrap.sh \ - --pack "$PACK_NAME" \ - --region "$BEDROCK_REGION" \ - --model "$DEFAULT_MODEL" \ - --gw-port "$GW_PORT" \ - --model-mode "$MODEL_MODE" \ - --litellm-base-url "$LITELLM_BASE_URL" \ - --litellm-api-key "$LITELLM_API_KEY" \ - --litellm-model "$LITELLM_MODEL" \ - --provider-api-key "$PROVIDER_API_KEY" -# ============================================================================ -# OUTPUTS -# ============================================================================ -Outputs: - InstanceId: - Description: EC2 Instance ID - Value: !Ref Instance - - PublicIp: - Description: Public IP address - Value: !GetAtt Instance.PublicIp - - PrivateIp: - Description: Private IP address - Value: !GetAtt Instance.PrivateIp - - VpcId: - Description: VPC ID - Value: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] - - SecurityGroupId: - Description: Security Group ID - Value: !Ref InstanceSecurityGroup - - RoleArn: - Description: IAM Role ARN - Value: !GetAtt InstanceRole.Arn - - SSMConnect: - Description: Connect via SSM Session Manager - Value: !Sub 'aws ssm start-session --target ${Instance} --region ${AWS::Region}' - - PackName: - Description: "Deployed agent pack" - Value: !Ref PackName diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index f18b296..36616a6 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -21,7 +21,7 @@ locals { subnet_id = var.existing_subnet_id != "" ? var.existing_subnet_id : (length(aws_subnet.public) > 0 ? aws_subnet.public[0].id : "") } -# Validate: if existing_vpc_id is set, existing_subnet_id must also be set +# Validate: if existing_vpc_id is set, existing_subnet_id must also be set (and vice versa) resource "terraform_data" "vpc_subnet_validation" { count = var.existing_vpc_id != "" && var.existing_subnet_id == "" ? 1 : 0 lifecycle { @@ -32,6 +32,16 @@ resource "terraform_data" "vpc_subnet_validation" { } } +resource "terraform_data" "subnet_vpc_validation" { + count = var.existing_subnet_id != "" && var.existing_vpc_id == "" ? 1 : 0 + lifecycle { + precondition { + condition = false + error_message = "existing_vpc_id is required when existing_subnet_id is set." + } + } +} + # ============================================================================ # VPC & Networking # ============================================================================ diff --git a/install.sh b/install.sh index ee29641..463c770 100755 --- a/install.sh +++ b/install.sh @@ -45,8 +45,7 @@ done # Deploy method constants DEPLOY_CFN_CONSOLE=1 DEPLOY_CFN_CLI=2 -DEPLOY_SAM=3 -DEPLOY_TERRAFORM=4 +DEPLOY_TERRAFORM=3 # Stamped at release; fall back to git info at runtime INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$(dirname "$0")" rev-parse --short HEAD 2>/dev/null || echo dev)}" INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$(dirname "$0")" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" @@ -380,16 +379,44 @@ check_existing_deployments() { EXISTING_VPC_ID="$chosen_vpc" - # Find the public subnet in the chosen VPC - local subnet_id - subnet_id=$(aws ec2 describe-subnets \ + # Find a public subnet in the chosen VPC (one with an internet gateway route) + local subnet_id="" + local candidate_subnets + # First try subnets tagged with "public" + candidate_subnets=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=tag:Name,Values=*public*" \ - --query 'Subnets[0].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "None") - if [[ "$subnet_id" == "None" || -z "$subnet_id" ]]; then - subnet_id=$(aws ec2 describe-subnets \ + --query 'Subnets[*].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "") + # Fallback: subnets with auto-assign public IP + if [[ -z "$candidate_subnets" || "$candidate_subnets" == "None" ]]; then + candidate_subnets=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=mapPublicIpOnLaunch,Values=true" \ - --query 'Subnets[0].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "") + --query 'Subnets[*].SubnetId' --output text --region "$check_region" 2>/dev/null || echo "") fi + # Verify at least one candidate has an IGW route (0.0.0.0/0 → igw-*) + for candidate in $candidate_subnets; do + [[ "$candidate" == "None" || -z "$candidate" ]] && continue + local rtb_id + rtb_id=$(aws ec2 describe-route-tables \ + --filters "Name=association.subnet-id,Values=${candidate}" \ + --query 'RouteTables[0].RouteTableId' --output text --region "$check_region" 2>/dev/null || echo "") + # Fall back to main route table if no explicit association + if [[ -z "$rtb_id" || "$rtb_id" == "None" ]]; then + rtb_id=$(aws ec2 describe-route-tables \ + --filters "Name=vpc-id,Values=${chosen_vpc}" "Name=association.main,Values=true" \ + --query 'RouteTables[0].RouteTableId' --output text --region "$check_region" 2>/dev/null || echo "") + fi + if [[ -n "$rtb_id" && "$rtb_id" != "None" ]]; then + local has_igw + has_igw=$(aws ec2 describe-route-tables \ + --route-table-ids "$rtb_id" \ + --query 'RouteTables[0].Routes[?DestinationCidrBlock==`0.0.0.0/0`].GatewayId' \ + --output text --region "$check_region" 2>/dev/null || echo "") + if [[ "$has_igw" == igw-* ]]; then + subnet_id="$candidate" + break + fi + fi + done if [[ -n "$subnet_id" && "$subnet_id" != "None" ]]; then EXISTING_SUBNET_ID="$subnet_id" @@ -424,8 +451,7 @@ choose_deploy_method() { echo "" echo " 1) CloudFormation Console -- opens browser wizard to review & launch" echo " 2) CloudFormation CLI -- deploy from terminal" - echo " 3) SAM -- for SAM CLI users" - echo -e " ${GREEN}4) Terraform${NC} -- for Terraform shops (auto-installs if needed)" + echo -e " ${GREEN}3) Terraform${NC} -- for Terraform shops (auto-installs if needed)" echo "" prompt "Deployment method" DEPLOY_METHOD "$DEPLOY_TERRAFORM" DEPLOY_METHOD=$(echo "$DEPLOY_METHOD" | tr -d '[:space:]') @@ -511,11 +537,14 @@ collect_config() { fi ok "Selected pack: ${PACK_NAME}" + prompt "AWS region" DEPLOY_REGION "$REGION" + # Count existing deployments to generate a smart default env name + # Must be after region prompt so we count in the right region local existing_count existing_count=$(aws ec2 describe-vpcs \ --filters "Name=tag:loki:managed,Values=true" \ - --region "$REGION" \ + --region "$DEPLOY_REGION" \ --query 'length(Vpcs)' --output text 2>/dev/null || echo "0") local default_env_name="${PACK_NAME}-$((existing_count + 1))" @@ -547,7 +576,6 @@ collect_config() { *) INSTANCE_TYPE="t4g.xlarge" ;; esac - prompt "AWS region" DEPLOY_REGION "$REGION" collect_security_config } @@ -1259,8 +1287,6 @@ main() { case "$DEPLOY_METHOD" in "$DEPLOY_CFN_CLI") info "Deploying with CloudFormation..." deploy_cfn_stack "deploy/cloudformation/template.yaml" "CAPABILITY_NAMED_IAM" ;; - "$DEPLOY_SAM") info "Deploying with SAM..." - deploy_cfn_stack "deploy/sam/template.yaml" "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ;; "$DEPLOY_TERRAFORM") info "Deploying with Terraform..." deploy_terraform ;; *) fail "Invalid choice: $DEPLOY_METHOD" ;; From b7d906c5b36f15d53fffdfb3a29a48f569b30abe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 23:53:25 +0000 Subject: [PATCH 055/172] ci: bump version to 0.5.23 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 463c770..1c9f21c 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.22" +INSTALLER_VERSION="0.5.23" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From 4de1a0258d7fc4285f0cfc7058ebaff5e53a8610 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:06:20 +0000 Subject: [PATCH 056/172] =?UTF-8?q?fix:=20address=20final=20review=20?= =?UTF-8?q?=E2=80=94=20W1=20(param=20groups),=20W2=20(dead=20condition),?= =?UTF-8?q?=20W3=20(verified=20non-issue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W1: add ExistingVpcId/ExistingSubnetId to CFN ParameterGroups (Networking) + ParameterLabels so console users see friendly names W2: remove dead UseExistingVpc condition (only CreateNewVpc is used) W3: verified non-issue — VPC resource has count=0 when reusing, TF won't tag it --- deploy/cloudformation/template.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index dd20d1d..bff88dc 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -41,6 +41,8 @@ Metadata: Parameters: - VpcCidr - PublicSubnetCidr + - ExistingVpcId + - ExistingSubnetId - SSHAllowedCidr - KeyPairName - Label: @@ -97,6 +99,10 @@ Metadata: default: "Agent Pack" LokiWatermark: default: "Loki Watermark Tag" + ExistingVpcId: + default: "Existing VPC ID (leave empty to create new)" + ExistingSubnetId: + default: "Existing Public Subnet ID" # ============================================================================ # PARAMETERS @@ -306,7 +312,6 @@ Conditions: IsApiKey: !Equals [!Ref ModelMode, 'api-key'] IsBedrock: !Equals [!Ref ModelMode, 'bedrock'] CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] - UseExistingVpc: !Not [!Equals [!Ref ExistingVpcId, '']] # ============================================================================ # RESOURCES From 865f48ec76570b7831d09f2789074e84c7419fda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:06:30 +0000 Subject: [PATCH 057/172] ci: bump version to 0.5.24 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1c9f21c..f8c6a35 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.23" +INSTALLER_VERSION="0.5.24" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From 5ffdf213effb0a642311e308997729cfa62949b8 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:09:13 +0000 Subject: [PATCH 058/172] fix: remove SAM references from test-templates.sh --- deploy/test-templates.sh | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/deploy/test-templates.sh b/deploy/test-templates.sh index 98ff5af..bc3b7d7 100644 --- a/deploy/test-templates.sh +++ b/deploy/test-templates.sh @@ -6,7 +6,6 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CFN_TEMPLATE="$REPO_ROOT/deploy/cloudformation/template.yaml" -SAM_TEMPLATE="$REPO_ROOT/deploy/sam/template.yaml" TF_VARS="$REPO_ROOT/deploy/terraform/variables.tf" TF_MAIN="$REPO_ROOT/deploy/terraform/main.tf" TF_OUTPUTS="$REPO_ROOT/deploy/terraform/outputs.tf" @@ -53,19 +52,6 @@ check_contains "$CFN_TEMPLATE" "Deployed agent pack" "CFN: PackName in Outputs" echo "" -# ── SAM ───────────────────────────────────────────────────────────────────── -echo -e "${BOLD}SAM (deploy/sam/template.yaml)${NC}" -check_contains "$SAM_TEMPLATE" "PackName:" "SAM: PackName parameter defined" -check_contains "$SAM_TEMPLATE" "hermes" "SAM: PackName AllowedValues includes hermes" -check_contains "$SAM_TEMPLATE" "- PackName" "SAM: PackName in Metadata ParameterGroups" -check_contains "$SAM_TEMPLATE" 'loki:pack' "SAM: VPC has loki:pack tag" -check_contains "$SAM_TEMPLATE" 'git clone --depth 1' "SAM: UserData uses git clone" -check_contains "$SAM_TEMPLATE" 'deploy/bootstrap.sh' "SAM: UserData calls bootstrap.sh" -check_contains "$SAM_TEMPLATE" '--pack' "SAM: UserData passes --pack flag" -check_contains "$SAM_TEMPLATE" 'Deployed agent pack' "SAM: PackName in Outputs" - -echo "" - # ── Terraform variables.tf ────────────────────────────────────────────────── echo -e "${BOLD}Terraform (deploy/terraform/variables.tf)${NC}" check_contains "$TF_VARS" 'variable "pack_name"' "TF: pack_name variable defined" From 85e0276811223c2281ecaa3d577941740af3fe89 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:09:21 +0000 Subject: [PATCH 059/172] ci: bump version to 0.5.25 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index f8c6a35..7e0ca26 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.24" +INSTALLER_VERSION="0.5.25" # --yes / -y: accept all defaults, minimal prompts AUTO_YES=false From 2f57f5261fadb8fb5c7f10ffbb1e4fe216238b4f Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:13:35 +0000 Subject: [PATCH 060/172] feat: --pack and -- flags to pre-select agent pack Examples: bash install.sh --yes --claude-code bash install.sh --pack hermes bash install.sh --openclaw --yes Supports: --openclaw, --claude-code, --hermes, --pi, --ironclaw Or generic: --pack (validated against registry.json) Unknown pack names fall back to interactive selection. --- README.md | 14 +++++++++++++- install.sh | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96ff7a1..0f8b69e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,18 @@ > curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --yes > ``` > +> **Express install with a specific pack:** +> ```sh +> # Deploy Claude Code +> bash /tmp/loki-install.sh --yes --claude-code +> +> # Deploy OpenClaw (default) +> bash /tmp/loki-install.sh --yes --openclaw +> +> # Or use --pack +> bash /tmp/loki-install.sh --yes --pack hermes +> ``` +> > Requires: AWS CLI configured, admin access on a dedicated AWS account. Without `--yes`, the script walks you through everything interactively. > > ⚠️ **We highly recommend deploying Loki in a brand-new, dedicated AWS account.** Loki has admin-level access and LLMs can make mistakes — a clean account limits the blast radius. Start with prototyping work as you learn and get acquainted with its capabilities. Like any powerful tool, it carries risks; isolating it in its own account is the simplest way to manage them. @@ -39,7 +51,7 @@ Run the install command from the TL;DR above. The installer verifies AWS permissions, lets you select your **agent pack**, instance size, and deployment method (CloudFormation / SAM / Terraform), then deploys automatically. -Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. +Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--claude-code`, `--hermes`, `--pi`, or `--ironclaw` (or `--pack `) to pre-select a pack. **Agent packs available:** | Pack | Description | Instance | Data Volume | diff --git a/install.sh b/install.sh index 7e0ca26..b143a96 100755 --- a/install.sh +++ b/install.sh @@ -2,6 +2,7 @@ # Loki Agent — One-Shot Installer # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh # Flags: --yes / -y Accept all defaults (non-interactive deploy) +# --pack or -- Pre-select agent pack (e.g. --claude-code, --openclaw) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -35,10 +36,25 @@ SSM_DOC_NAME="Loki-Session" INSTALLER_VERSION="0.5.25" # --yes / -y: accept all defaults, minimal prompts +# --pack or --: pre-select agent pack AUTO_YES=false +PRESELECT_PACK="" for arg in "$@"; do case "$arg" in --yes|-y) AUTO_YES=true ;; + --pack) ;; # value handled below + --openclaw) PRESELECT_PACK="openclaw" ;; + --claude-code) PRESELECT_PACK="claude-code" ;; + --hermes) PRESELECT_PACK="hermes" ;; + --pi) PRESELECT_PACK="pi" ;; + --ironclaw) PRESELECT_PACK="ironclaw" ;; + esac +done +# Handle --pack (two-arg form) +while [[ $# -gt 0 ]]; do + case "$1" in + --pack) [[ $# -gt 1 ]] && PRESELECT_PACK="$2" && shift 2 || shift ;; + *) shift ;; esac done @@ -218,7 +234,12 @@ show_banner() { echo -e "${BOLD}╚══════════════════════════════════════════════╝${NC}" if [[ "$AUTO_YES" == true ]]; then echo "" - info "Running in auto mode (--yes) — using defaults, minimal prompts" + local auto_msg="Running in auto mode (--yes) — using defaults, minimal prompts" + [[ -n "${PRESELECT_PACK}" ]] && auto_msg+=", pack: ${PRESELECT_PACK}" + info "$auto_msg" + elif [[ -n "${PRESELECT_PACK}" ]]; then + echo "" + info "Pack pre-selected: ${PRESELECT_PACK}" fi echo "" } @@ -511,6 +532,27 @@ collect_config() { ' "$registry" 2>/dev/null \ || echo "openclaw|OpenClaw -- stateful AI agent with persistent gateway|false") + # If pack was pre-selected via --pack or --, find and use it + if [[ -n "${PRESELECT_PACK}" ]]; then + local found=false + for i in "${!pack_names[@]}"; do + if [[ "${pack_names[$i]}" == "${PRESELECT_PACK}" ]]; then + PACK_NAME="${pack_names[$i]}" + found=true + if [[ "${pack_experimental[$i]}" == "true" ]]; then + warn "${PACK_NAME} is experimental — expect rough edges" + fi + ok "Pack pre-selected: ${PACK_NAME}" + break + fi + done + if [[ "$found" != true ]]; then + warn "Unknown pack '${PRESELECT_PACK}' — falling back to interactive selection" + PRESELECT_PACK="" + fi + fi + + if [[ -z "${PRESELECT_PACK}" ]]; then echo " Agent to deploy:" local i for i in "${!pack_names[@]}"; do @@ -536,6 +578,7 @@ collect_config() { warn "${PACK_NAME} is experimental — expect rough edges" fi ok "Selected pack: ${PACK_NAME}" + fi # end of interactive pack selection prompt "AWS region" DEPLOY_REGION "$REGION" From 776454c8b0f583f2470f1bd73f76154cb72fe0d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:13:44 +0000 Subject: [PATCH 061/172] ci: bump version to 0.5.26 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b143a96..08d09a1 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.25" +INSTALLER_VERSION="0.5.26" # --yes / -y: accept all defaults, minimal prompts # --pack or --: pre-select agent pack From ea4f611d0729cfb05795101b6189353d7dd2bdc2 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:22:15 +0000 Subject: [PATCH 062/172] =?UTF-8?q?fix:=20simplify=20pack=20selection=20?= =?UTF-8?q?=E2=80=94=20--pack=20only,=20fail=20on=20unknown/empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove shorthand flags (--claude-code, --openclaw, etc.) — only --pack - --pack without a value: immediate error with usage hint - --pack with unknown name: hard fail with list of available packs - Single-pass arg parsing (no more two-loop fragility) - Updated README examples --- README.md | 8 ++++---- install.sh | 40 +++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0f8b69e..ff04e97 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ > **Express install with a specific pack:** > ```sh > # Deploy Claude Code -> bash /tmp/loki-install.sh --yes --claude-code +> bash /tmp/loki-install.sh --yes --pack claude-code > > # Deploy OpenClaw (default) -> bash /tmp/loki-install.sh --yes --openclaw +> bash /tmp/loki-install.sh --yes --pack openclaw > -> # Or use --pack +> # Deploy Hermes > bash /tmp/loki-install.sh --yes --pack hermes > ``` > @@ -51,7 +51,7 @@ Run the install command from the TL;DR above. The installer verifies AWS permissions, lets you select your **agent pack**, instance size, and deployment method (CloudFormation / SAM / Terraform), then deploys automatically. -Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--claude-code`, `--hermes`, `--pi`, or `--ironclaw` (or `--pack `) to pre-select a pack. +Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--pack ` to pre-select a pack (e.g. `--pack claude-code`). **Agent packs available:** | Pack | Description | Instance | Data Volume | diff --git a/install.sh b/install.sh index 08d09a1..fb8064f 100755 --- a/install.sh +++ b/install.sh @@ -2,7 +2,7 @@ # Loki Agent — One-Shot Installer # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh # Flags: --yes / -y Accept all defaults (non-interactive deploy) -# --pack or -- Pre-select agent pack (e.g. --claude-code, --openclaw) +# --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -36,24 +36,18 @@ SSM_DOC_NAME="Loki-Session" INSTALLER_VERSION="0.5.26" # --yes / -y: accept all defaults, minimal prompts -# --pack or --: pre-select agent pack +# --pack : pre-select agent pack AUTO_YES=false PRESELECT_PACK="" -for arg in "$@"; do - case "$arg" in - --yes|-y) AUTO_YES=true ;; - --pack) ;; # value handled below - --openclaw) PRESELECT_PACK="openclaw" ;; - --claude-code) PRESELECT_PACK="claude-code" ;; - --hermes) PRESELECT_PACK="hermes" ;; - --pi) PRESELECT_PACK="pi" ;; - --ironclaw) PRESELECT_PACK="ironclaw" ;; - esac -done -# Handle --pack (two-arg form) while [[ $# -gt 0 ]]; do case "$1" in - --pack) [[ $# -gt 1 ]] && PRESELECT_PACK="$2" && shift 2 || shift ;; + --yes|-y) AUTO_YES=true; shift ;; + --pack) + if [[ $# -lt 2 || "$2" == --* ]]; then + echo -e "\033[0;31m✗\033[0m --pack requires a pack name (e.g. --pack openclaw, --pack claude-code)" >&2 + exit 1 + fi + PRESELECT_PACK="$2"; shift 2 ;; *) shift ;; esac done @@ -237,9 +231,6 @@ show_banner() { local auto_msg="Running in auto mode (--yes) — using defaults, minimal prompts" [[ -n "${PRESELECT_PACK}" ]] && auto_msg+=", pack: ${PRESELECT_PACK}" info "$auto_msg" - elif [[ -n "${PRESELECT_PACK}" ]]; then - echo "" - info "Pack pre-selected: ${PRESELECT_PACK}" fi echo "" } @@ -532,7 +523,7 @@ collect_config() { ' "$registry" 2>/dev/null \ || echo "openclaw|OpenClaw -- stateful AI agent with persistent gateway|false") - # If pack was pre-selected via --pack or --, find and use it + # If pack was pre-selected via --pack, find and validate it if [[ -n "${PRESELECT_PACK}" ]]; then local found=false for i in "${!pack_names[@]}"; do @@ -547,8 +538,15 @@ collect_config() { fi done if [[ "$found" != true ]]; then - warn "Unknown pack '${PRESELECT_PACK}' — falling back to interactive selection" - PRESELECT_PACK="" + echo "" + echo -e " ${RED}✗ Unknown pack: '${PRESELECT_PACK}'${NC}" + echo "" + echo " Available packs:" + for i in "${!pack_names[@]}"; do + echo " - ${pack_names[$i]}" + done + echo "" + fail "Pack '${PRESELECT_PACK}' not found. Use --pack with one of the packs listed above." fi fi From d1d424fb27da819b6df3f00eded6f653f82e5ead Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:22:23 +0000 Subject: [PATCH 063/172] ci: bump version to 0.5.27 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index fb8064f..1de6108 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.26" +INSTALLER_VERSION="0.5.27" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From b57a30c5a64fe13979844c979d57d5141d85cb9e Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:27:21 +0000 Subject: [PATCH 064/172] fix: escape ${STACK_NAME} in CFN UserData !Sub block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was causing 'Unresolved resource dependencies [STACK_NAME]' — CFN !Sub interprets ${VAR} as a resource ref. Use ${!VAR} to pass literal shell var. --- deploy/cloudformation/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index bff88dc..521827e 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1017,7 +1017,7 @@ Resources: --value "FAILED: userdata error at line $LINENO" \ --type String --overwrite --region "$REGION" 2>/dev/null || true touch /tmp/loki-bootstrap-done - /opt/aws/bin/cfn-signal -e 1 --stack "${STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true + /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true ' ERR # Ensure git is available (not present on all AMIs) command -v git &>/dev/null || dnf install -y git 2>/dev/null || yum install -y git From 8a309652cebde10e9f4adee111d7c2b6105dc714 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:27:29 +0000 Subject: [PATCH 065/172] ci: bump version to 0.5.28 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1de6108..b8ab03d 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.27" +INSTALLER_VERSION="0.5.28" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From b10f96566482833babb8705c3997e23467241167 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:28:41 +0000 Subject: [PATCH 066/172] fix: terraform precondition must reference vars (not bare false) TF requires precondition expressions to reference config objects. Changed 'condition = false' to 'condition = var.existing_subnet_id != ""' which evaluates the same way (count already ensures only created when invalid). --- deploy/terraform/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 36616a6..35df1f3 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -26,7 +26,7 @@ resource "terraform_data" "vpc_subnet_validation" { count = var.existing_vpc_id != "" && var.existing_subnet_id == "" ? 1 : 0 lifecycle { precondition { - condition = false + condition = var.existing_subnet_id != "" error_message = "existing_subnet_id is required when existing_vpc_id is set." } } @@ -36,7 +36,7 @@ resource "terraform_data" "subnet_vpc_validation" { count = var.existing_subnet_id != "" && var.existing_vpc_id == "" ? 1 : 0 lifecycle { precondition { - condition = false + condition = var.existing_vpc_id != "" error_message = "existing_vpc_id is required when existing_subnet_id is set." } } From 2211bcef0012424e25d79f84c4cb6a822bcaddf1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:28:55 +0000 Subject: [PATCH 067/172] ci: bump version to 0.5.29 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b8ab03d..4c05627 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.28" +INSTALLER_VERSION="0.5.29" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 3d0883a185a24d4f9445f0e29c5e3eca5ea7048c Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 00:44:10 +0000 Subject: [PATCH 068/172] =?UTF-8?q?fix:=20add=20cfn-signal=20success=20aft?= =?UTF-8?q?er=20bootstrap=20(was=20missing=20=E2=80=94=20stack=20hung=20fo?= =?UTF-8?q?r=2030min)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ERR trap only signals failure (-e 1). On successful bootstrap, nothing signaled CFN, so CreationPolicy waited until PT30M timeout. Now signals -e 0 after bootstrap completes. --- deploy/cloudformation/template.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 521827e..9773933 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1041,6 +1041,8 @@ Resources: --litellm-api-key "$LITELLM_API_KEY" \ --litellm-model "$LITELLM_MODEL" \ --provider-api-key "$PROVIDER_API_KEY" + # Signal CFN success + /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" # ============================================================================ # OUTPUTS # ============================================================================ From a3233b6f33d3030576f2cb1e24a5fb7fe87bc81e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 00:44:25 +0000 Subject: [PATCH 069/172] ci: bump version to 0.5.30 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 4c05627..58d046c 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.29" +INSTALLER_VERSION="0.5.30" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 9b876ac51cd02f838f4a2e9e6bc89e3cac21cce5 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Sat, 4 Apr 2026 01:03:35 +0000 Subject: [PATCH 070/172] fix: install cfn-bootstrap before signaling + add || true fallback AL2023 doesn't ship cfn-signal by default. Install aws-cfn-bootstrap via pip3 if missing, and add || true so set -e doesn't trigger ERR trap if signal still fails. --- deploy/cloudformation/template.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 9773933..cc43505 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1041,8 +1041,11 @@ Resources: --litellm-api-key "$LITELLM_API_KEY" \ --litellm-model "$LITELLM_MODEL" \ --provider-api-key "$PROVIDER_API_KEY" - # Signal CFN success - /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" + # Signal CFN success (install cfn-bootstrap if needed — not on AL2023 by default) + if [[ ! -x /opt/aws/bin/cfn-signal ]]; then + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 2>/dev/null || true + fi + /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" 2>/dev/null || true # ============================================================================ # OUTPUTS # ============================================================================ From 99b2af66c6c44dfb5ab1dc04136e56d717e5ebee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 01:03:44 +0000 Subject: [PATCH 071/172] ci: bump version to 0.5.31 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 58d046c..541e668 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.30" +INSTALLER_VERSION="0.5.31" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From e6fe3270dcbbde876758e60741ba0864b72a1f0b Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:27:47 +0000 Subject: [PATCH 072/172] =?UTF-8?q?fix:=20cfn-signal=20on=20AL2023=20?= =?UTF-8?q?=E2=80=94=20install=20cfn-bootstrap=20+=20AWS=20CLI=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AL2023 doesn't ship aws-cfn-bootstrap, so /opt/aws/bin/cfn-signal doesn't exist. pip3 is also missing, so the inline pip3 fallback was silently failing. Fix (3 layers of defense): 1. Install python3-pip + aws-cfn-bootstrap in System Dependencies phase of bootstrap.sh (covers all packs) 2. Every cfn-signal call now checks -x /opt/aws/bin/cfn-signal first 3. If cfn-signal binary still missing, fall back to 'aws cloudformation signal-resource' (AWS CLI is always available) Applies to: bootstrap.sh (all packs), template.yaml UserData (ERR trap + success signal) --- deploy/bootstrap.sh | 38 +++++++++++++++++++++++++---- deploy/cloudformation/template.yaml | 21 ++++++++++++++-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index f3e6f0c..a58bd88 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -29,7 +29,17 @@ trap ' --type String --overwrite --region "${REGION:-us-east-1}" >/dev/null 2>&1 || true touch /tmp/loki-bootstrap-done if [[ -n "${STACK_NAME:-}" ]]; then - /opt/aws/bin/cfn-signal -e 1 --stack "${STACK_NAME}" --resource Instance --region "${REGION:-us-east-1}" 2>/dev/null || true + if [[ -x /opt/aws/bin/cfn-signal ]]; then + /opt/aws/bin/cfn-signal -e 1 --stack "${STACK_NAME}" --resource Instance --region "${REGION:-us-east-1}" 2>/dev/null || true + else + _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + aws cloudformation signal-resource \ + --stack-name "${STACK_NAME}" \ + --logical-resource-id Instance \ + --unique-id "$_IID" \ + --status FAILURE \ + --region "${REGION:-us-east-1}" 2>/dev/null || true + fi fi ' ERR @@ -340,9 +350,17 @@ ok "System updated" # ---- Dependencies ---- step "System Dependencies" -dnf install -y git jq htop tmux gnupg2-minimal libatomic gettext +dnf install -y git jq htop tmux gnupg2-minimal libatomic gettext python3-pip ok "Packages installed" +# Install aws-cfn-bootstrap for cfn-signal (not pre-installed on AL2023) +if [[ ! -x /opt/aws/bin/cfn-signal ]]; then + info "Installing aws-cfn-bootstrap (provides cfn-signal)..." + pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 2>/dev/null || \ + python3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 2>/dev/null || true + [[ -x /opt/aws/bin/cfn-signal ]] && ok "cfn-bootstrap installed (cfn-signal ready)" || warn "cfn-bootstrap install failed — stack may timeout waiting for signal" +fi + # ---- Mount data volume ---- DATA_VOL_GB="$(registry_get_data_vol "${PACK_NAME}")" step "Data Volume (pack requests ${DATA_VOL_GB}GB)" @@ -623,11 +641,21 @@ ok "Pack '${PACK_NAME}' bootstrap complete at $(date -u)" # ---- cfn-signal ---- if [[ -n "${STACK_NAME}" ]]; then step "CloudFormation Signal" - if aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region "${REGION}" &>/dev/null; then + INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + if [[ -x /opt/aws/bin/cfn-signal ]]; then /opt/aws/bin/cfn-signal -e 0 --stack "${STACK_NAME}" --resource Instance --region "${REGION}" \ && ok "cfn-signal sent (stack=${STACK_NAME})" \ - || fail "cfn-signal failed" + || fail "cfn-signal binary failed" else - info "Stack '${STACK_NAME}' not found in region ${REGION} — skipping cfn-signal" + # Fallback: signal via AWS CLI when cfn-bootstrap is not installed + info "cfn-signal binary not found — signalling via AWS CLI" + aws cloudformation signal-resource \ + --stack-name "${STACK_NAME}" \ + --logical-resource-id Instance \ + --unique-id "${INSTANCE_ID}" \ + --status SUCCESS \ + --region "${REGION}" \ + && ok "cfn-signal sent via CLI (stack=${STACK_NAME})" \ + || fail "cfn-signal via CLI failed" fi fi diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index cc43505..bd25058 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1017,7 +1017,12 @@ Resources: --value "FAILED: userdata error at line $LINENO" \ --type String --overwrite --region "$REGION" 2>/dev/null || true touch /tmp/loki-bootstrap-done - /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true + if [[ -x /opt/aws/bin/cfn-signal ]]; then + /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true + else + _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + aws cloudformation signal-resource --stack-name "${!STACK_NAME}" --logical-resource-id Instance --unique-id "$_IID" --status FAILURE --region "$REGION" 2>/dev/null || true + fi ' ERR # Ensure git is available (not present on all AMIs) command -v git &>/dev/null || dnf install -y git 2>/dev/null || yum install -y git @@ -1043,9 +1048,21 @@ Resources: --provider-api-key "$PROVIDER_API_KEY" # Signal CFN success (install cfn-bootstrap if needed — not on AL2023 by default) if [[ ! -x /opt/aws/bin/cfn-signal ]]; then + dnf install -y python3-pip 2>/dev/null || true pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 2>/dev/null || true fi - /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" 2>/dev/null || true + if [[ -x /opt/aws/bin/cfn-signal ]]; then + /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" 2>/dev/null || true + else + # Fallback: signal via AWS CLI + _INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + aws cloudformation signal-resource \ + --stack-name "$STACK_NAME" \ + --logical-resource-id Instance \ + --unique-id "$_INSTANCE_ID" \ + --status SUCCESS \ + --region "$REGION" 2>/dev/null || true + fi # ============================================================================ # OUTPUTS # ============================================================================ From 1e2778c33da8f4def97114ecb40c41999a6b7284 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:27:58 +0000 Subject: [PATCH 073/172] ci: bump version to 0.5.32 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 541e668..756fd4b 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.31" +INSTALLER_VERSION="0.5.32" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From aec63d0cdcffb830203c0aa7854b69d9d355d811 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:30:51 +0000 Subject: [PATCH 074/172] review: IMDSv2 support, consistent variable naming, defense-in-depth comment Code review follow-up: - Add get_instance_id() helper with IMDSv2 token + v1 fallback - Use IMDSv2-safe IMDS calls in template.yaml ERR trap + success signal - Standardize variable naming to _INSTANCE_ID across all signal paths - Add clarifying comment on double cfn-signal (bootstrap.sh + UserData) - Use get_instance_id in bootstrap step log --- deploy/bootstrap.sh | 22 +++++++++++++++++----- deploy/cloudformation/template.yaml | 19 ++++++++++++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index a58bd88..93b398d 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -32,11 +32,11 @@ trap ' if [[ -x /opt/aws/bin/cfn-signal ]]; then /opt/aws/bin/cfn-signal -e 1 --stack "${STACK_NAME}" --resource Instance --region "${REGION:-us-east-1}" 2>/dev/null || true else - _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + _INSTANCE_ID=$(get_instance_id) aws cloudformation signal-resource \ --stack-name "${STACK_NAME}" \ --logical-resource-id Instance \ - --unique-id "$_IID" \ + --unique-id "$_INSTANCE_ID" \ --status FAILURE \ --region "${REGION:-us-east-1}" 2>/dev/null || true fi @@ -44,6 +44,18 @@ trap ' ' ERR # ── Helpers ─────────────────────────────────────────────────────────────────── + +# IMDSv2-safe instance ID fetch (falls back to IMDSv1 if token request fails) +get_instance_id() { + local token + 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 "$token" ]]; then + curl -sf -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown + else + curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown + fi +} + STEP_COUNTER_FILE="/tmp/loki-step-counter" STEP_TOTAL_FILE="/tmp/loki-step-total" echo "0" > "$STEP_COUNTER_FILE" @@ -227,7 +239,7 @@ REGISTRY="${PACKS_DIR}/registry.yaml" step "Bootstrap Dispatcher" info "Pack: ${PACK_NAME} | Region: ${REGION}${STACK_NAME:+ | Stack: $STACK_NAME}" info "Repo: ${REPO_DIR}" -info "Instance: $(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown)" +info "Instance: $(get_instance_id)" # ── Validate pack exists in registry ───────────────────────────────────────── if [[ ! -f "$REGISTRY" ]]; then @@ -641,7 +653,7 @@ ok "Pack '${PACK_NAME}' bootstrap complete at $(date -u)" # ---- cfn-signal ---- if [[ -n "${STACK_NAME}" ]]; then step "CloudFormation Signal" - INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + _INSTANCE_ID=$(get_instance_id) if [[ -x /opt/aws/bin/cfn-signal ]]; then /opt/aws/bin/cfn-signal -e 0 --stack "${STACK_NAME}" --resource Instance --region "${REGION}" \ && ok "cfn-signal sent (stack=${STACK_NAME})" \ @@ -652,7 +664,7 @@ if [[ -n "${STACK_NAME}" ]]; then aws cloudformation signal-resource \ --stack-name "${STACK_NAME}" \ --logical-resource-id Instance \ - --unique-id "${INSTANCE_ID}" \ + --unique-id "${_INSTANCE_ID}" \ --status SUCCESS \ --region "${REGION}" \ && ok "cfn-signal sent via CLI (stack=${STACK_NAME})" \ diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index bd25058..47febcc 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1020,7 +1020,12 @@ Resources: if [[ -x /opt/aws/bin/cfn-signal ]]; then /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true else - _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + _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 "$_IMDS_TOKEN" ]]; then + _IID=$(curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + else + _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + fi aws cloudformation signal-resource --stack-name "${!STACK_NAME}" --logical-resource-id Instance --unique-id "$_IID" --status FAILURE --region "$REGION" 2>/dev/null || true fi ' ERR @@ -1046,7 +1051,10 @@ Resources: --litellm-api-key "$LITELLM_API_KEY" \ --litellm-model "$LITELLM_MODEL" \ --provider-api-key "$PROVIDER_API_KEY" - # Signal CFN success (install cfn-bootstrap if needed — not on AL2023 by default) + # Signal CFN success — defense-in-depth: bootstrap.sh also signals, + # but this catches edge cases where bootstrap.sh signal fails silently. + # CFN ignores duplicate signals, so double-fire is safe. + # Install cfn-bootstrap if needed — not on AL2023 by default. if [[ ! -x /opt/aws/bin/cfn-signal ]]; then dnf install -y python3-pip 2>/dev/null || true pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz 2>/dev/null || true @@ -1055,7 +1063,12 @@ Resources: /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource Instance --region "$REGION" 2>/dev/null || true else # Fallback: signal via AWS CLI - _INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + _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 "$_IMDS_TOKEN" ]]; then + _INSTANCE_ID=$(curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + else + _INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + fi aws cloudformation signal-resource \ --stack-name "$STACK_NAME" \ --logical-resource-id Instance \ From 3c82f34951579dc25fc077a8e3e621238ef127f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:31:08 +0000 Subject: [PATCH 075/172] ci: bump version to 0.5.33 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 756fd4b..c668328 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.32" +INSTALLER_VERSION="0.5.33" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 98ddc7514dbf20bbeef9597e9529f287f1590d73 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:34:02 +0000 Subject: [PATCH 076/172] security: enforce IMDSv2-only, remove all IMDSv1 fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CFN template: add MetadataOptions HttpTokens=required, HopLimit=2 - Terraform main.tf: add metadata_options http_tokens=required - bootstrap.sh: get_instance_id() now returns 'unknown' if token fetch fails instead of falling back to IMDSv1 - template.yaml: inline IMDS calls stripped of v1 fallback branches All instances now reject bare IMDSv1 GET requests. Every metadata call uses the PUT token→GET pattern required by IMDSv2. --- deploy/bootstrap.sh | 4 ++-- deploy/cloudformation/template.yaml | 16 ++++++---------- deploy/terraform/main.tf | 6 ++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 93b398d..914e1e6 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -45,14 +45,14 @@ trap ' # ── Helpers ─────────────────────────────────────────────────────────────────── -# IMDSv2-safe instance ID fetch (falls back to IMDSv1 if token request fails) +# IMDSv2-only instance ID fetch (instance enforces HttpTokens=required) get_instance_id() { local token 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 "$token" ]]; then curl -sf -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown else - curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown + echo unknown fi } diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 47febcc..aedf281 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -960,6 +960,10 @@ Resources: SubnetId: !If [CreateNewVpc, !Ref PublicSubnet, !Ref ExistingSubnetId] SecurityGroupIds: - !Ref InstanceSecurityGroup + MetadataOptions: + HttpTokens: required + HttpPutResponseHopLimit: 2 + HttpEndpoint: enabled EbsOptimized: true BlockDeviceMappings: - DeviceName: /dev/xvda @@ -1021,11 +1025,7 @@ Resources: /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true else _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 "$_IMDS_TOKEN" ]]; then - _IID=$(curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) - else - _IID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) - fi + _IID=$([[ -n "$_IMDS_TOKEN" ]] && curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) aws cloudformation signal-resource --stack-name "${!STACK_NAME}" --logical-resource-id Instance --unique-id "$_IID" --status FAILURE --region "$REGION" 2>/dev/null || true fi ' ERR @@ -1064,11 +1064,7 @@ Resources: else # Fallback: signal via AWS CLI _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 "$_IMDS_TOKEN" ]]; then - _INSTANCE_ID=$(curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) - else - _INSTANCE_ID=$(curl -sf http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) - fi + _INSTANCE_ID=$([[ -n "$_IMDS_TOKEN" ]] && curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) aws cloudformation signal-resource \ --stack-name "$STACK_NAME" \ --logical-resource-id Instance \ diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 35df1f3..686f9fb 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -637,6 +637,12 @@ resource "aws_instance" "main" { vpc_security_group_ids = [aws_security_group.main.id] ebs_optimized = true + metadata_options { + http_tokens = "required" + http_put_response_hop_limit = 2 + http_endpoint = "enabled" + } + root_block_device { volume_size = var.root_volume_size volume_type = "gp3" From 744f2f46d397ac08bb17bc63540f47947bc08f2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:34:19 +0000 Subject: [PATCH 077/172] ci: bump version to 0.5.34 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index c668328..52e16ec 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.33" +INSTALLER_VERSION="0.5.34" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 908fb4137e369b318a9eba5235dbe7f69100dcf7 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:40:12 +0000 Subject: [PATCH 078/172] =?UTF-8?q?review:=20fix=20remaining=20=5FIID=20?= =?UTF-8?q?=E2=86=92=20=5FINSTANCE=5FID=20in=20template=20ERR=20trap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final variable naming inconsistency caught in code review. All signal paths now consistently use _INSTANCE_ID. --- deploy/cloudformation/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index aedf281..a4fcb4c 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1025,8 +1025,8 @@ Resources: /opt/aws/bin/cfn-signal -e 1 --stack "${!STACK_NAME}" --resource Instance --region "$REGION" 2>/dev/null || true else _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) - _IID=$([[ -n "$_IMDS_TOKEN" ]] && curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) - aws cloudformation signal-resource --stack-name "${!STACK_NAME}" --logical-resource-id Instance --unique-id "$_IID" --status FAILURE --region "$REGION" 2>/dev/null || true + _INSTANCE_ID=$([[ -n "$_IMDS_TOKEN" ]] && curl -sf -H "X-aws-ec2-metadata-token: $_IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo unknown) + aws cloudformation signal-resource --stack-name "${!STACK_NAME}" --logical-resource-id Instance --unique-id "$_INSTANCE_ID" --status FAILURE --region "$REGION" 2>/dev/null || true fi ' ERR # Ensure git is available (not present on all AMIs) From f5b5676a6d2bd25cbb97185fa2a80398c7364e6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:40:26 +0000 Subject: [PATCH 079/172] ci: bump version to 0.5.35 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 52e16ec..cda0421 100755 --- a/install.sh +++ b/install.sh @@ -33,7 +33,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.34" +INSTALLER_VERSION="0.5.35" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From e9b7f9801ad889d13abcf1b86cc0ebdff238d728 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:44:36 +0000 Subject: [PATCH 080/172] feat: add --method flag to install.sh Pre-select deploy method from CLI: --method cfn-console CloudFormation Console wizard --method cfn CloudFormation CLI --method terraform Terraform (alias: tf) Examples: install.sh --yes --pack hermes --method cfn install.sh --yes --pack openclaw --method terraform install.sh -y --pack claude-code --method cfn-console Also accepts 'console' and 'cloudformation' as aliases. --- install.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/install.sh b/install.sh index cda0421..1147801 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,7 @@ # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh # Flags: --yes / -y Accept all defaults (non-interactive deploy) # --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) +# --method Pre-select deploy method: cfn-console, cfn, terraform (or tf) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -37,8 +38,10 @@ INSTALLER_VERSION="0.5.35" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack +# --method : pre-select deploy method (cfn-console, cfn, terraform/tf) AUTO_YES=false PRESELECT_PACK="" +PRESELECT_METHOD="" while [[ $# -gt 0 ]]; do case "$1" in --yes|-y) AUTO_YES=true; shift ;; @@ -48,6 +51,12 @@ while [[ $# -gt 0 ]]; do exit 1 fi PRESELECT_PACK="$2"; shift 2 ;; + --method) + if [[ $# -lt 2 || "$2" == --* ]]; then + echo -e "\033[0;31m✗\033[0m --method requires a value (cfn-console, cfn, terraform, tf)" >&2 + exit 1 + fi + PRESELECT_METHOD="$2"; shift 2 ;; *) shift ;; esac done @@ -230,6 +239,7 @@ show_banner() { echo "" local auto_msg="Running in auto mode (--yes) — using defaults, minimal prompts" [[ -n "${PRESELECT_PACK}" ]] && auto_msg+=", pack: ${PRESELECT_PACK}" + [[ -n "${PRESELECT_METHOD}" ]] && auto_msg+=", method: ${PRESELECT_METHOD}" info "$auto_msg" fi echo "" @@ -458,6 +468,32 @@ terraform_version_string() { } choose_deploy_method() { + # If method was pre-selected via --method, validate and set it + if [[ -n "${PRESELECT_METHOD}" ]]; then + case "${PRESELECT_METHOD}" in + cfn-console|console) DEPLOY_METHOD="$DEPLOY_CFN_CONSOLE" ;; + cfn|cloudformation) DEPLOY_METHOD="$DEPLOY_CFN_CLI" ;; + terraform|tf) DEPLOY_METHOD="$DEPLOY_TERRAFORM" ;; + *) + echo "" + echo -e " ${RED}✗ Unknown deploy method: '${PRESELECT_METHOD}'${NC}" + echo "" + echo " Valid methods:" + echo " cfn-console — CloudFormation Console wizard" + echo " cfn — CloudFormation CLI" + echo " terraform — Terraform (or 'tf')" + echo "" + fail "Use --method with one of the methods listed above." + ;; + esac + local method_name + case "$DEPLOY_METHOD" in + "$DEPLOY_CFN_CONSOLE") method_name="CloudFormation Console" ;; + "$DEPLOY_CFN_CLI") method_name="CloudFormation CLI" ;; + "$DEPLOY_TERRAFORM") method_name="Terraform" ;; + esac + ok "Deploy method pre-selected: ${method_name}" + else echo "" echo " Deployment methods:" echo "" @@ -467,6 +503,7 @@ choose_deploy_method() { echo "" prompt "Deployment method" DEPLOY_METHOD "$DEPLOY_TERRAFORM" DEPLOY_METHOD=$(echo "$DEPLOY_METHOD" | tr -d '[:space:]') + fi # If Terraform selected and not installed, handle it now — before config questions. # This avoids the user filling out all config only to be blocked at deploy time. From 0c57b58e20267e3af6ca493721c05ffec8189370 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:44:52 +0000 Subject: [PATCH 081/172] ci: bump version to 0.5.36 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1147801..e3367c8 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.35" +INSTALLER_VERSION="0.5.36" # --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From e139b28997ed7f7aaaa1af6808a84864bf08df1d Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:49:53 +0000 Subject: [PATCH 082/172] refactor: rename --yes to --non-interactive, drop cfn-console from --method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Primary flag is now --non-interactive (standard convention used by npm, terraform, apt, helm). --yes and -y kept as aliases. - cfn-console removed from --method — it's interactive-only (opens browser wizard), not valid for scripted deploys. - Valid --method values: cfn, cloudformation, terraform, tf - Banner updated to say 'non-interactive mode' --- install.sh | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/install.sh b/install.sh index e3367c8..998ac39 100755 --- a/install.sh +++ b/install.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # Loki Agent — One-Shot Installer # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh -# Flags: --yes / -y Accept all defaults (non-interactive deploy) -# --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) -# --method Pre-select deploy method: cfn-console, cfn, terraform (or tf) +# Flags: --non-interactive / -y Accept all defaults, minimal prompts +# --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) +# --method Pre-select deploy method: cfn, terraform (or tf) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -36,15 +36,15 @@ TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/ma SSM_DOC_NAME="Loki-Session" INSTALLER_VERSION="0.5.36" -# --yes / -y: accept all defaults, minimal prompts +# --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack -# --method : pre-select deploy method (cfn-console, cfn, terraform/tf) +# --method : pre-select deploy method (cfn, terraform/tf) AUTO_YES=false PRESELECT_PACK="" PRESELECT_METHOD="" while [[ $# -gt 0 ]]; do case "$1" in - --yes|-y) AUTO_YES=true; shift ;; + --non-interactive|--yes|-y) AUTO_YES=true; shift ;; --pack) if [[ $# -lt 2 || "$2" == --* ]]; then echo -e "\033[0;31m✗\033[0m --pack requires a pack name (e.g. --pack openclaw, --pack claude-code)" >&2 @@ -53,7 +53,7 @@ while [[ $# -gt 0 ]]; do PRESELECT_PACK="$2"; shift 2 ;; --method) if [[ $# -lt 2 || "$2" == --* ]]; then - echo -e "\033[0;31m✗\033[0m --method requires a value (cfn-console, cfn, terraform, tf)" >&2 + echo -e "\033[0;31m✗\033[0m --method requires a value (cfn, terraform, tf)" >&2 exit 1 fi PRESELECT_METHOD="$2"; shift 2 ;; @@ -237,7 +237,7 @@ show_banner() { echo -e "${BOLD}╚══════════════════════════════════════════════╝${NC}" if [[ "$AUTO_YES" == true ]]; then echo "" - local auto_msg="Running in auto mode (--yes) — using defaults, minimal prompts" + local auto_msg="Running in non-interactive mode — using defaults, minimal prompts" [[ -n "${PRESELECT_PACK}" ]] && auto_msg+=", pack: ${PRESELECT_PACK}" [[ -n "${PRESELECT_METHOD}" ]] && auto_msg+=", method: ${PRESELECT_METHOD}" info "$auto_msg" @@ -471,7 +471,6 @@ choose_deploy_method() { # If method was pre-selected via --method, validate and set it if [[ -n "${PRESELECT_METHOD}" ]]; then case "${PRESELECT_METHOD}" in - cfn-console|console) DEPLOY_METHOD="$DEPLOY_CFN_CONSOLE" ;; cfn|cloudformation) DEPLOY_METHOD="$DEPLOY_CFN_CLI" ;; terraform|tf) DEPLOY_METHOD="$DEPLOY_TERRAFORM" ;; *) @@ -479,16 +478,14 @@ choose_deploy_method() { echo -e " ${RED}✗ Unknown deploy method: '${PRESELECT_METHOD}'${NC}" echo "" echo " Valid methods:" - echo " cfn-console — CloudFormation Console wizard" echo " cfn — CloudFormation CLI" echo " terraform — Terraform (or 'tf')" echo "" - fail "Use --method with one of the methods listed above." + fail "Use --method with one of the methods listed above." ;; esac local method_name case "$DEPLOY_METHOD" in - "$DEPLOY_CFN_CONSOLE") method_name="CloudFormation Console" ;; "$DEPLOY_CFN_CLI") method_name="CloudFormation CLI" ;; "$DEPLOY_TERRAFORM") method_name="Terraform" ;; esac From fa5f13d9cea44f38e4350d1963a9b4ebb060c66e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 07:50:03 +0000 Subject: [PATCH 083/172] ci: bump version to 0.5.37 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 998ac39..08aa345 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.36" +INSTALLER_VERSION="0.5.37" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 40b9e5b198326d3af8b6e36c206dae9b329d45d6 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 07:59:41 +0000 Subject: [PATCH 084/172] docs: update README with --non-interactive and --method flags - Replace --yes with --non-interactive in all examples - Add --method cfn/terraform examples - Add CLI flags reference table - Show combined usage: --non-interactive --pack hermes --method cfn --- README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ff04e97..836515c 100644 --- a/README.md +++ b/README.md @@ -12,24 +12,31 @@ > curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh > ``` > -> **Express install (accept all defaults, zero prompts):** +> **Non-interactive install (accept all defaults, zero prompts):** > ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --yes +> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive > ``` > -> **Express install with a specific pack:** +> **Non-interactive with a specific pack and deploy method:** > ```sh -> # Deploy Claude Code -> bash /tmp/loki-install.sh --yes --pack claude-code +> # Deploy Claude Code via CloudFormation +> bash /tmp/loki-install.sh --non-interactive --pack claude-code --method cfn > -> # Deploy OpenClaw (default) -> bash /tmp/loki-install.sh --yes --pack openclaw +> # Deploy OpenClaw via Terraform (default) +> bash /tmp/loki-install.sh --non-interactive --pack openclaw --method terraform > -> # Deploy Hermes -> bash /tmp/loki-install.sh --yes --pack hermes +> # Deploy Hermes via CloudFormation +> bash /tmp/loki-install.sh --non-interactive --pack hermes --method cfn > ``` > -> Requires: AWS CLI configured, admin access on a dedicated AWS account. Without `--yes`, the script walks you through everything interactively. +> Requires: AWS CLI configured, admin access on a dedicated AWS account. Without `--non-interactive`, the script walks you through everything interactively. +> +> **CLI flags:** +> | Flag | Description | +> |------|-------------| +> | `--non-interactive` | Accept all defaults, skip prompts (aliases: `--yes`, `-y`) | +> | `--pack ` | Pre-select agent pack (`openclaw`, `claude-code`, `hermes`, `pi`, `ironclaw`) | +> | `--method ` | Pre-select deploy method (`cfn`, `terraform` / `tf`) | > > ⚠️ **We highly recommend deploying Loki in a brand-new, dedicated AWS account.** Loki has admin-level access and LLMs can make mistakes — a clean account limits the blast radius. Start with prototyping work as you learn and get acquainted with its capabilities. Like any powerful tool, it carries risks; isolating it in its own account is the simplest way to manage them. > @@ -51,7 +58,7 @@ Run the install command from the TL;DR above. The installer verifies AWS permissions, lets you select your **agent pack**, instance size, and deployment method (CloudFormation / SAM / Terraform), then deploys automatically. -Use `--yes` (or `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--pack ` to pre-select a pack (e.g. `--pack claude-code`). +Use `--non-interactive` (or `--yes` / `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--pack ` to pre-select a pack and `--method ` to pre-select the deploy method. **Agent packs available:** | Pack | Description | Instance | Data Volume | From 1a628d1d86bb4be324cea068087a6e3bd75ec4c7 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 08:00:17 +0000 Subject: [PATCH 085/172] docs: restructure TL;DR with OpenClaw and Claude Code one-liners TL;DR now leads with the two main packs as copy-paste one-liners: - OpenClaw (recommended) with --non-interactive - Claude Code with --non-interactive --method cfn Plus interactive mode and additional examples. --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 836515c..1e450bb 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,25 @@ > **TL;DR — deploy Loki:** > -> **macOS / Linux / WSL / CloudShell:** +> **Interactive (walks you through everything):** > ```sh > curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh > ``` > -> **Non-interactive install (accept all defaults, zero prompts):** +> **Non-interactive — OpenClaw (recommended, full AI agent with 24/7 gateway):** > ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive +> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive --pack openclaw > ``` > -> **Non-interactive with a specific pack and deploy method:** +> **Non-interactive — Claude Code (Anthropic's coding agent):** > ```sh -> # Deploy Claude Code via CloudFormation -> bash /tmp/loki-install.sh --non-interactive --pack claude-code --method cfn +> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive --pack claude-code --method cfn +> ``` > -> # Deploy OpenClaw via Terraform (default) +> **More examples:** +> ```sh +> # Pick a deploy method: cfn (CloudFormation) or terraform > bash /tmp/loki-install.sh --non-interactive --pack openclaw --method terraform -> -> # Deploy Hermes via CloudFormation > bash /tmp/loki-install.sh --non-interactive --pack hermes --method cfn > ``` > From 65413affe482d3dc4cc7747c47763e0fbb127a6e Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 10:47:36 +0000 Subject: [PATCH 086/172] =?UTF-8?q?feat:=20permission=20profiles=20?= =?UTF-8?q?=E2=80=94=20builder,=20account=5Fassistant,=20personal=5Fassist?= =?UTF-8?q?ant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiles control IAM permissions and instance defaults, orthogonal to packs. Any pack × any profile combination is valid. Profiles: - builder: AdministratorAccess, t4g.xlarge default (current behavior) - account_assistant: ReadOnlyAccess + Bedrock invoke + deny (secrets, S3 objects, lambda:GetFunction), t4g.medium default - personal_assistant: Bedrock + SSM + STS only, t4g.medium default Profile is REQUIRED — no default. --non-interactive without --profile fails. Changes: - install.sh: --profile flag, choose_profile(), risk-colored menu, instance defaults per profile, PARAM array plumbing - CFN template: ProfileName param, 6 conditions, conditional IAM policies, AdminUser gated on IsBuilder, SecurityEnablement gated on RunSecurityServices, loki:profile tag, UserData passthrough - Terraform: profile_name variable with validation, conditional IAM resources (count pattern), policy files, userdata passthrough - bootstrap.sh: --profile arg, .profile marker, pack config JSON, profile-aware security skip, Bedrock check for all profiles - Policy files: deny, bedrock, bootstrap_operations, personal_assistant - 101 tests (test-profiles.sh), all passing - CFN template validates Review fixes applied: - Removed ssm:GetParameter from deny (breaks SSM Agent) - Added TF security_enablement_invoke count condition - Added DRY check test (profiles/ vs deploy/terraform/policies/) - Added SSM Agent safety test --- PROFILE-PLAN.md | 519 ++++++++++++++ deploy/bootstrap.sh | 32 +- deploy/cloudformation/template.yaml | 156 +++- deploy/terraform/main.tf | 66 +- deploy/terraform/outputs.tf | 5 + .../policies/account_assistant_bedrock.json | 18 + .../policies/account_assistant_deny.json | 32 + .../policies/bootstrap_operations.json | 18 + .../policies/personal_assistant.json | 51 ++ deploy/terraform/userdata.sh.tpl | 2 + deploy/terraform/variables.tf | 10 + install.sh | 116 ++- profiles/account_assistant_bedrock.json | 18 + profiles/account_assistant_deny.json | 32 + profiles/bootstrap_operations.json | 18 + profiles/builder.json | 3 + profiles/personal_assistant.json | 51 ++ profiles/registry.yaml | 39 + tests/test-profiles.sh | 665 ++++++++++++++++++ 19 files changed, 1830 insertions(+), 21 deletions(-) create mode 100644 PROFILE-PLAN.md create mode 100644 deploy/terraform/policies/account_assistant_bedrock.json create mode 100644 deploy/terraform/policies/account_assistant_deny.json create mode 100644 deploy/terraform/policies/bootstrap_operations.json create mode 100644 deploy/terraform/policies/personal_assistant.json create mode 100644 profiles/account_assistant_bedrock.json create mode 100644 profiles/account_assistant_deny.json create mode 100644 profiles/bootstrap_operations.json create mode 100644 profiles/builder.json create mode 100644 profiles/personal_assistant.json create mode 100644 profiles/registry.yaml create mode 100644 tests/test-profiles.sh diff --git a/PROFILE-PLAN.md b/PROFILE-PLAN.md new file mode 100644 index 0000000..9bd410a --- /dev/null +++ b/PROFILE-PLAN.md @@ -0,0 +1,519 @@ +# PROFILE-PLAN.md — Permission Profiles for Loki Agent + +## Overview + +Profiles control the **IAM permissions** and **instance sizing defaults** for a Loki deployment. They are orthogonal to packs — any pack can be combined with any profile. + +``` +Pack = WHAT agent runtime gets installed (openclaw, claude-code, hermes, pi, ironclaw) +Profile = WHAT the agent is ALLOWED TO DO on AWS (builder, account_assistant, personal_assistant) +``` + +## Profiles + +### `builder` — Full-stack AWS builder +The current behavior. Agent can create, modify, and delete any AWS resource. + +| Attribute | Value | +|-----------|-------| +| **IAM** | `AdministratorAccess` (managed policy) | +| **Instance default** | `t4g.xlarge` | +| **Use case** | Build apps, deploy infrastructure, manage pipelines, fix production issues | +| **Risk** | High — can break anything in the account | + +### `account_assistant` — Read-only AWS advisor +Can see everything, change nothing. Useful for cost analysis, architecture review, debugging, compliance checks. + +| Attribute | Value | +|-----------|-------| +| **IAM** | `ReadOnlyAccess` (managed policy) + explicit Deny on secrets/S3 object reads (see below) | +| **Instance default** | `t4g.medium` | +| **Use case** | Budget advice, architecture review, debugging help, security posture review | +| **Risk** | Low — cannot modify any resources, cannot read secret values or S3 data | + +**Deny policy (inline, attached to the role):** +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenySecretValues", + "Effect": "Deny", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:GetResourcePolicy", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath" + ], + "Resource": "*" + }, + { + "Sid": "DenyS3ObjectAccess", + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectAcl" + ], + "Resource": "*" + }, + { + "Sid": "DenyLambdaCodeAccess", + "Effect": "Deny", + "Action": [ + "lambda:GetFunction" + ], + "Resource": "*" + } + ] +} +``` + +> **Note:** The agent can still `s3:ListBucket`, `s3:GetBucketLocation`, etc. — it can see what buckets and objects exist, just can't read their contents. Same for secrets — can list them, can't read values. `lambda:GetFunction` is denied because it returns a presigned URL to download function code + env vars that may contain secrets. `lambda:ListFunctions` and `lambda:GetFunctionConfiguration` remain available. +> +> **Intentionally allowed:** `ssm:GetParameterHistory`, `codecommit:GetFile/GetBlob`, `ecr:BatchGetImage`, `logs:GetLogEvents` — these are useful for debugging and architecture review, which is the profile's purpose. + +### `personal_assistant` — Non-AWS personal helper +No AWS access at all (except Bedrock for inference). For daily productivity, writing, research, scheduling — not AWS work. + +| Attribute | Value | +|-----------|-------| +| **IAM** | Bedrock invoke only + SSM (for connectivity) | +| **Instance default** | `t4g.medium` | +| **Use case** | Personal assistant, writing, research, coding help (non-AWS) | +| **Risk** | Minimal — cannot interact with any AWS service except Bedrock | + +**IAM policy:** +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess" + ], + "Resource": "*" + }, + { + "Sid": "SSMConnectivity", + "Effect": "Allow", + "Action": [ + "ssm:UpdateInstanceInformation", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply" + ], + "Resource": "*" + }, + { + "Sid": "BedrockDiscovery", + "Effect": "Allow", + "Action": [ + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" + }, + { + "Sid": "Identity", + "Effect": "Allow", + "Action": [ + "sts:GetCallerIdentity" + ], + "Resource": "*" + } + ] +} +``` + +> **Note:** Agent still has full local shell access on the EC2 instance — can install packages, run code, manage files, etc. It just can't call AWS APIs (except Bedrock + SSM). +> +> **All profiles also get bootstrap permissions** (scoped, for deployment only): +> ```json +> { +> "Sid": "BootstrapOperations", +> "Effect": "Allow", +> "Action": [ +> "ssm:PutParameter", +> "ssm:DeleteParameter", +> "cloudformation:SignalResource" +> ], +> "Resource": [ +> "arn:aws:ssm:*:*:parameter/loki/*", +> "arn:aws:cloudformation:*:*:stack/*" +> ] +> } +> ``` +> These are needed by bootstrap.sh for status publishing and cfn-signal. Scoped to `/loki/*` SSM parameters so the agent can't write arbitrary params post-bootstrap. + +## Bedrockify Dependency + +**All profiles need Bedrockify installed.** Bedrockify (OpenAI-compatible proxy for Bedrock) is a base dependency that most packs rely on. Profile controls IAM permissions, not what software gets installed. The pack system handles dependencies via `registry.yaml` — if a pack lists `bedrockify` as a dep, it gets installed regardless of profile. + +`account_assistant` uses `ReadOnlyAccess` which does NOT include Bedrock invoke permissions. Since the agent still needs inference, add a Bedrock statement alongside the managed policy: + +```json +{ + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess", + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" +} +``` + +## Implementation Plan + +### Phase 1: Profile Registry + +Create `profiles/` directory with one YAML manifest per profile: + +``` +profiles/ + registry.yaml # Profile metadata + defaults + builder.yaml # IAM policy document + account_assistant.yaml # IAM policy document + deny policy + personal_assistant.yaml # IAM policy document +``` + +**`profiles/registry.yaml`:** +```yaml +profiles: + builder: + description: "Full-stack AWS builder — can create, modify, and delete any AWS resource" + instance_type: t4g.xlarge + iam_mode: managed # Use AWS managed policy + managed_policies: + - arn:aws:iam::aws:policy/AdministratorAccess + deny_policies: [] + security_services: true # Enable security services by default + + account_assistant: + description: "Read-only AWS advisor — can see everything, change nothing" + instance_type: t4g.medium + iam_mode: managed + managed_policies: + - arn:aws:iam::aws:policy/ReadOnlyAccess + inline_policies: + - profiles/account_assistant_bedrock.json + deny_policies: + - profiles/account_assistant_deny.json + bootstrap_policies: + - profiles/bootstrap_operations.json + security_services: true + + personal_assistant: + description: "Personal helper — Bedrock inference only, no AWS access" + instance_type: t4g.medium + iam_mode: inline # Custom inline policy + inline_policies: + - profiles/personal_assistant.json + bootstrap_policies: + - profiles/bootstrap_operations.json + deny_policies: [] + security_services: false # No point enabling if agent can't read them +``` + +### Phase 2: CloudFormation Template Changes + +**New parameter:** +```yaml +Parameters: + ProfileName: + Type: String + AllowedValues: + - builder + - account_assistant + - personal_assistant + Description: "Permission profile. 'builder' = full admin. 'account_assistant' = read-only. 'personal_assistant' = Bedrock only." + # No Default — must be explicitly specified +``` + +**Conditional IAM role:** +```yaml +Conditions: + IsBuilder: !Equals [!Ref ProfileName, 'builder'] + IsNotBuilder: !Not [!Condition IsBuilder] + IsAccountAssistant: !Equals [!Ref ProfileName, 'account_assistant'] + IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] + NeedsAdminUser: !Condition IsBuilder + +Resources: + InstanceRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${EnvironmentName}-role' + AssumeRolePolicyDocument: ... + ManagedPolicyArns: + - 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'] + + # Bootstrap operations for non-builder profiles (SSM status + cfn-signal) + BootstrapOperationsPolicy: + Type: AWS::IAM::Policy + Condition: IsNotBuilder # Builder has AdministratorAccess, doesn't need this + Properties: + PolicyName: !Sub '${EnvironmentName}-bootstrap-ops' + Roles: [!Ref InstanceRole] + PolicyDocument: + # (bootstrap_operations.json — scoped ssm:PutParameter + cfn:SignalResource) + + # Bedrock inference for account_assistant (ReadOnlyAccess doesn't include invoke) + AccountAssistantBedrockPolicy: + Type: AWS::IAM::Policy + Condition: IsAccountAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-bedrock-inference' + Roles: [!Ref InstanceRole] + PolicyDocument: + # (account_assistant_bedrock.json — Bedrock invoke + discovery) + + # Deny policy for account_assistant (secrets + S3 objects + lambda code) + AccountAssistantDenyPolicy: + Type: AWS::IAM::Policy + Condition: IsAccountAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-deny-secrets-s3' + Roles: [!Ref InstanceRole] + PolicyDocument: + # (deny policy JSON from above) + + # Inline policy for personal_assistant (Bedrock + SSM only) + PersonalAssistantPolicy: + Type: AWS::IAM::Policy + Condition: IsPersonalAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-bedrock-only' + Roles: [!Ref InstanceRole] + PolicyDocument: + # (Bedrock + SSM + STS policy JSON from above) + + # Admin user — only for builder profile + AdminUser: + Type: AWS::IAM::User + Condition: NeedsAdminUser + ... +``` + +**Instance type default from profile:** + +The instance type parameter keeps its current allowed values, but the install.sh changes the *default* based on profile. In the template itself, `InstanceType` remains user-overridable. + +**Security services conditional on profile:** + +Skip security service Lambda invocation for `personal_assistant` (agent can't read findings anyway). Bedrock Form Lambda runs for ALL profiles since every profile needs inference. + +### Phase 3: Terraform Changes + +Mirror the CFN changes in `deploy/terraform/`: + +```hcl +variable "profile_name" { + type = string + description = "Permission profile" + validation { + condition = contains(["builder", "account_assistant", "personal_assistant"], var.profile_name) + error_message = "Must be one of: builder, account_assistant, personal_assistant" + } + # No default — must be specified +} + +# Conditional managed policy attachments +resource "aws_iam_role_policy_attachment" "admin" { + count = var.profile_name == "builder" ? 1 : 0 + role = aws_iam_role.instance.name + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" +} + +resource "aws_iam_role_policy_attachment" "readonly" { + count = var.profile_name == "account_assistant" ? 1 : 0 + role = aws_iam_role.instance.name + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" +} + +# Deny policy for account_assistant +resource "aws_iam_role_policy" "account_assistant_deny" { + count = var.profile_name == "account_assistant" ? 1 : 0 + name = "${var.environment_name}-deny-secrets-s3" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/account_assistant_deny.json") +} + +# Inline policy for personal_assistant +resource "aws_iam_role_policy" "personal_assistant" { + count = var.profile_name == "personal_assistant" ? 1 : 0 + name = "${var.environment_name}-bedrock-only" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/personal_assistant.json") +} +``` + +### Phase 4: Installer (`install.sh`) Changes + +**New `--profile` flag:** +```bash +--profile) + if [[ $# -lt 2 || "$2" == --* ]]; then + echo "✗ --profile requires a value (builder, account_assistant, personal_assistant)" >&2 + exit 1 + fi + PRESELECT_PROFILE="$2"; shift 2 ;; +``` + +**Profile selection — REQUIRED, no default:** + +New function `choose_profile()` called early in `collect_config`: + +```bash +choose_profile() { + if [[ -n "${PRESELECT_PROFILE}" ]]; then + # validate against registry + ... + ok "Profile pre-selected: ${PROFILE_NAME}" + return + fi + + echo "" + echo " Permission profiles (REQUIRED — choose one):" + echo "" + echo " 1) builder — Full admin access. Can create, modify, delete any AWS resource." + echo " Best for: building apps, deploying infra, managing pipelines." + echo "" + echo " 2) account_assistant — Read-only. Can see everything, change nothing." + echo " Best for: cost analysis, architecture review, debugging help." + echo "" + echo " 3) personal_assistant — Bedrock only. No AWS access." + echo " Best for: writing, research, coding help, daily tasks." + echo "" + + if [[ "$AUTO_YES" == true ]]; then + # Non-interactive mode WITHOUT --profile → error, don't guess + fail "Profile is required. Use --profile " + fi + + prompt "Select profile" PROFILE_CHOICE "" + # ... validate and set PROFILE_NAME +} +``` + +> **Key design decision:** `--non-interactive` without `--profile` is an **error**. We don't pick a default because the wrong profile has security implications. Interactive mode shows the menu and waits. + +**Instance size default adjusts per profile:** + +```bash +# In collect_config(), after profile is selected: +case "$PROFILE_NAME" in + builder) default_size_choice="3" ;; # t4g.xlarge + account_assistant) default_size_choice="1" ;; # t4g.medium + personal_assistant) default_size_choice="1" ;; # t4g.medium +esac +``` + +**Parameter plumbing:** + +Add `ProfileName` / `profile_name` to: +- `PARAM_CFN_NAMES` / `PARAM_TF_NAMES` / `PARAM_VALUES` arrays +- `format_console_params()`, `format_cfn_cli_params()`, `format_tf_vars()` +- Deploy summary display + +### Phase 5: Bootstrap (`deploy/bootstrap.sh`) Changes + +Bootstrap needs a new `--profile` argument and profile-aware branching: + +1. **Add `--profile` to arg parsing** in bootstrap.sh (alongside `--pack`, `--region`, etc.) + +2. **Thread `--profile` through UserData in BOTH deploy methods:** + - **CFN template UserData:** add `export PROFILE_NAME="${ProfileName}"` and `--profile "$PROFILE_NAME"` to the bootstrap.sh call + - **Terraform `userdata.sh.tpl`:** add `profile_name` template var and `--profile "$PROFILE_NAME"` to the bootstrap.sh call + +3. **Write profile to a marker file** so the agent knows its own profile: +```bash +echo "${PROFILE_NAME}" > /home/ec2-user/.openclaw/workspace/.profile +``` + +4. **Conditionally adjust brain files** based on profile. E.g., `personal_assistant` gets a different `SOUL.md` that says "you have no AWS access" and `TOOLS.md` that omits AWS tooling. + +5. **Skip security service checks** for `personal_assistant` profile (agent can't read findings). Bedrock connectivity check runs for ALL profiles since all need inference. + +### Phase 6: Registry Integration + +Add `ProfileName` to: +- `packs/registry.json` — for the installer to pass through +- CFN template `AllowedValues` +- Terraform `validation` block +- Loki watermark tags (so `uninstall.sh` can show profile info) + +### File Changes Summary + +| File | Change | +|------|--------| +| `profiles/registry.yaml` | **NEW** — profile metadata | +| `profiles/account_assistant_deny.json` | **NEW** — deny policy (secrets, S3, lambda code) | +| `profiles/account_assistant_bedrock.json` | **NEW** — Bedrock invoke for account_assistant | +| `profiles/personal_assistant.json` | **NEW** — Bedrock-only policy | +| `profiles/bootstrap_operations.json` | **NEW** — scoped SSM + cfn-signal for bootstrap | +| `install.sh` | Add `--profile` flag, `choose_profile()`, require profile, instance defaults | +| `deploy/cloudformation/template.yaml` | Add `ProfileName` param, conditional IAM, conditional security services, AdminUser gated on IsBuilder | +| `deploy/terraform/main.tf` | Add `profile_name` var, conditional IAM resources | +| `deploy/terraform/variables.tf` | Add `profile_name` variable | +| `deploy/terraform/policies/` | **NEW** — JSON policy files | +| `deploy/terraform/userdata.sh.tpl` | Pass `--profile` to bootstrap.sh | +| `deploy/bootstrap.sh` | Add `--profile` arg, write `.profile` marker, profile-aware branching (skip security checks for personal_assistant) | +| `uninstall.sh` | Display profile tag in deployment list | +| `README.md` | Document profiles, update TL;DR examples, add profile table | + +### Implementation Order + +1. **Create profile policy files** (JSON) — can validate independently +2. **CFN template** — add parameter + conditionals, test with `aws cloudformation validate-template` +3. **Terraform** — add variable + conditionals, test with `terraform validate` +4. **install.sh** — add `--profile` flag + selection + plumbing +5. **bootstrap.sh** — profile marker + conditional brain +6. **README.md** — document everything +7. **Test matrix:** 3 profiles × 2 methods (cfn, terraform) = 6 deploys + +### Example Usage After Implementation + +```sh +# Full builder (current behavior, explicit) +bash install.sh --non-interactive --pack openclaw --method cfn --profile builder + +# Read-only assistant for cost/architecture review +bash install.sh --non-interactive --pack openclaw --method terraform --profile account_assistant + +# Personal assistant (Bedrock only, no AWS) +bash install.sh --non-interactive --pack claude-code --method cfn --profile personal_assistant + +# Interactive — profile menu shown, must pick one +bash install.sh +``` + +### Open Questions — RESOLVED + +1. **Should `account_assistant` be allowed to read CloudWatch logs/metrics?** **YES** — ReadOnlyAccess includes it, keep it. Logs/metrics are essential for debugging and cost analysis. + +2. **Should `personal_assistant` have `sts:GetCallerIdentity`?** **YES** — add it to the personal_assistant policy. Agent needs to know what account it's in for basic context. + +3. **Profile upgrade/downgrade path?** **Redeploy for now.** Stack update would work technically (IAM changes don't replace instances), but redeploy is simpler and safer for v1. Can add upgrade-in-place later. + +4. **Should profile be a tag on the instance?** **YES** — tag as `loki:profile` on the instance and VPC. Useful for `uninstall.sh` visibility and agent self-identification. diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 914e1e6..2dc6f52 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -110,6 +110,7 @@ EOF # ── Arg parsing ─────────────────────────────────────────────────────────────── PACK_NAME="" +PROFILE_NAME="${PROFILE_NAME:-}" REGION="${REGION:-us-east-1}" STACK_NAME="${STACK_NAME:-}" # Pack-specific args (written to JSON config) @@ -134,6 +135,11 @@ while [[ $# -gt 0 ]]; do PACK_NAME="$2" shift 2 ;; + --profile) + [[ $# -gt 1 ]] || { echo "ERROR: --profile requires a value" >&2; exit 1; } + PROFILE_NAME="$2" + shift 2 + ;; --region) [[ $# -gt 1 ]] || { echo "ERROR: --region requires a value" >&2; exit 1; } REGION="$2" @@ -209,6 +215,7 @@ fi PACK_CONFIG="/tmp/loki-pack-config.json" jq -n \ --arg pack "$PACK_NAME" \ + --arg profile "$PROFILE_NAME" \ --arg region "$REGION" \ --arg model "$MODEL" \ --arg gw_port "$GW_PORT" \ @@ -219,7 +226,7 @@ jq -n \ --arg litellm_key "$LITELLM_KEY" \ --arg litellm_model "$LITELLM_MODEL" \ --arg provider_key "$PROVIDER_KEY" \ - '{pack:$pack, region:$region, model:$model, gw_port:$gw_port, + '{pack:$pack, profile:$profile, region:$region, model:$model, gw_port:$gw_port, model_mode:$model_mode, bedrockify_port:$bedrockify_port, hermes_model:$hermes_model, litellm_url:$litellm_url, litellm_key:$litellm_key, litellm_model:$litellm_model, @@ -237,7 +244,7 @@ PACKS_DIR="${REPO_DIR}/packs" REGISTRY="${PACKS_DIR}/registry.yaml" step "Bootstrap Dispatcher" -info "Pack: ${PACK_NAME} | Region: ${REGION}${STACK_NAME:+ | Stack: $STACK_NAME}" +info "Pack: ${PACK_NAME} | Profile: ${PROFILE_NAME:-unset} | Region: ${REGION}${STACK_NAME:+ | Stack: $STACK_NAME}" info "Repo: ${REPO_DIR}" info "Instance: $(get_instance_id)" @@ -491,6 +498,18 @@ step "Enable Linger" loginctl enable-linger ec2-user ok "Linger enabled for ec2-user" +# ---- Write profile marker file ---- +step "Profile Marker" +sudo -u ec2-user bash << PROFILE_EOF +set -euo pipefail +ok() { echo "[OK] \$(date -u '+%H:%M:%S') \$1"; } +info() { echo "[INFO] \$(date -u '+%H:%M:%S') \$1"; } +mkdir -p "\${HOME}/.openclaw/workspace" +echo "${PROFILE_NAME:-}" > "\${HOME}/.openclaw/workspace/.profile" +chmod 644 "\${HOME}/.openclaw/workspace/.profile" +ok "Profile marker written: ${PROFILE_NAME:-unset} -> \${HOME}/.openclaw/workspace/.profile" +PROFILE_EOF + # ── Phase 2: PACKS ──────────────────────────────────────────────────────────── step "Phase 2: Pack Dispatch" @@ -627,7 +646,7 @@ LOKIPROFILE chmod 644 /etc/profile.d/loki.sh ok "Shell profile installed (/etc/profile.d/loki.sh)" -# ---- Bedrock model access check ---- +# ---- Bedrock model access check (runs for ALL profiles — all need inference) ---- step "Bedrock Model Access Check" sudo -u ec2-user bash << 'BEDROCK_EOF' set -euo pipefail @@ -641,6 +660,13 @@ else fi BEDROCK_EOF +# ---- Security services check (skip for personal_assistant — no AWS read access) ---- +if [[ "${PROFILE_NAME:-}" == "personal_assistant" ]]; then + info "Security checks: skipped for personal_assistant profile (agent cannot read findings)" +else + info "Security checks: applicable for profile '${PROFILE_NAME:-builder}'" +fi + # ---- Complete ---- # ---- Clean up config file (contains secrets) ---- rm -f "${PACK_CONFIG}" diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index a4fcb4c..c58a197 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -11,6 +11,7 @@ Metadata: Parameters: - EnvironmentName - PackName + - ProfileName - InstanceType - DefaultModel - Label: @@ -53,6 +54,8 @@ Metadata: ParameterLabels: EnvironmentName: default: "Environment Name" + ProfileName: + default: "Permission Profile" InstanceType: default: "Instance Size" DefaultModel: @@ -119,6 +122,14 @@ Parameters: - ironclaw Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." + ProfileName: + Type: String + AllowedValues: + - builder + - account_assistant + - personal_assistant + Description: "Permission profile. 'builder' = full admin (AdministratorAccess). 'account_assistant' = read-only (ReadOnlyAccess). 'personal_assistant' = Bedrock inference only, no AWS access." + EnvironmentName: Type: String Default: openclaw @@ -312,6 +323,12 @@ Conditions: IsApiKey: !Equals [!Ref ModelMode, 'api-key'] IsBedrock: !Equals [!Ref ModelMode, 'bedrock'] CreateNewVpc: !Equals [!Ref ExistingVpcId, ''] + IsBuilder: !Equals [!Ref ProfileName, 'builder'] + IsNotBuilder: !Not [!Condition IsBuilder] + IsAccountAssistant: !Equals [!Ref ProfileName, 'account_assistant'] + IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] + NeedsAdminUser: !Condition IsBuilder + RunSecurityServices: !Not [!Condition IsPersonalAssistant] # ============================================================================ # RESOURCES @@ -455,7 +472,8 @@ Resources: Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - - arn:aws:iam::aws:policy/AdministratorAccess + - !If [IsBuilder, 'arn:aws:iam::aws:policy/AdministratorAccess', !Ref 'AWS::NoValue'] + - !If [IsAccountAssistant, 'arn:aws:iam::aws:policy/ReadOnlyAccess', !Ref 'AWS::NoValue'] Tags: - Key: Name Value: !Sub '${EnvironmentName}-role' @@ -466,6 +484,126 @@ Resources: - Key: loki:deploy-method Value: cloudformation + # Bootstrap operations for non-builder profiles (SSM status + cfn-signal) + # Builder has AdministratorAccess which already includes these permissions. + BootstrapOperationsPolicy: + Type: AWS::IAM::Policy + Condition: IsNotBuilder + Properties: + PolicyName: !Sub '${EnvironmentName}-bootstrap-ops' + Roles: + - !Ref InstanceRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: BootstrapOperations + Effect: Allow + Action: + - ssm:PutParameter + - ssm:DeleteParameter + - cloudformation:SignalResource + Resource: + - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/loki/*' + - !Sub 'arn:aws:cloudformation:*:${AWS::AccountId}:stack/*' + + # Bedrock inference for account_assistant (ReadOnlyAccess does not include invoke) + AccountAssistantBedrockPolicy: + Type: AWS::IAM::Policy + Condition: IsAccountAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-bedrock-inference' + Roles: + - !Ref InstanceRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: BedrockInference + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:GetUseCaseForModelAccess + - bedrock:ListFoundationModels + - bedrock:GetFoundationModel + - bedrock:ListInferenceProfiles + Resource: '*' + + # Deny policy for account_assistant — secrets, S3 objects, Lambda code + AccountAssistantDenyPolicy: + Type: AWS::IAM::Policy + Condition: IsAccountAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-deny-secrets-s3' + Roles: + - !Ref InstanceRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: DenySecretValues + Effect: Deny + Action: + - secretsmanager:GetSecretValue + - secretsmanager:GetResourcePolicy + Resource: '*' + - Sid: DenyS3ObjectAccess + Effect: Deny + Action: + - s3:GetObject + - s3:GetObjectVersion + - s3:GetObjectAcl + Resource: '*' + - Sid: DenyLambdaCodeAccess + Effect: Deny + Action: + - lambda:GetFunction + Resource: '*' + + # Inline policy for personal_assistant (Bedrock + SSM connectivity + STS only) + PersonalAssistantPolicy: + Type: AWS::IAM::Policy + Condition: IsPersonalAssistant + Properties: + PolicyName: !Sub '${EnvironmentName}-bedrock-only' + Roles: + - !Ref InstanceRole + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: BedrockInference + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + - bedrock:GetUseCaseForModelAccess + Resource: '*' + - Sid: SSMConnectivity + Effect: Allow + Action: + - ssm:UpdateInstanceInformation + - ssmmessages:CreateControlChannel + - ssmmessages:CreateDataChannel + - ssmmessages:OpenControlChannel + - ssmmessages:OpenDataChannel + - ec2messages:AcknowledgeMessage + - ec2messages:DeleteMessage + - ec2messages:FailMessage + - ec2messages:GetEndpoint + - ec2messages:GetMessages + - ec2messages:SendReply + Resource: '*' + - Sid: BedrockDiscovery + Effect: Allow + Action: + - bedrock:ListFoundationModels + - bedrock:GetFoundationModel + - bedrock:ListInferenceProfiles + Resource: '*' + - Sid: Identity + Effect: Allow + Action: + - sts:GetCallerIdentity + Resource: '*' + InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: @@ -844,6 +982,7 @@ Resources: SecurityEnablementCustomResource: Type: Custom::SecurityEnablement + Condition: RunSecurityServices DependsOn: SecurityEnablementRole Properties: ServiceToken: !GetAtt SecurityEnablementFunction.Arn @@ -854,10 +993,11 @@ Resources: EnableConfigRecorder: !Ref EnableConfigRecorder # -------------------------------------------------------------------------- - # Admin Console User + # Admin Console User — only for builder profile # -------------------------------------------------------------------------- AdminUser: Type: AWS::IAM::User + Condition: NeedsAdminUser Properties: UserName: !Sub '${EnvironmentName}-admin' ManagedPolicyArns: @@ -876,11 +1016,13 @@ Resources: AdminAccessKey: Type: AWS::IAM::AccessKey + Condition: NeedsAdminUser Properties: UserName: !Ref AdminUser AdminSetupRole: Type: AWS::IAM::Role + Condition: NeedsAdminUser Properties: RoleName: !Sub '${EnvironmentName}-admin-pw-role' AssumeRolePolicyDocument: @@ -895,6 +1037,7 @@ Resources: AdminSetupFunction: Type: AWS::Lambda::Function + Condition: NeedsAdminUser Properties: FunctionName: !Sub '${EnvironmentName}-admin-setup' Runtime: python3.12 @@ -930,6 +1073,7 @@ Resources: AdminSetupCustomResource: Type: Custom::AdminSetup + Condition: NeedsAdminUser DependsOn: - AdminUser - AdminSetupRole @@ -996,6 +1140,8 @@ Resources: Value: '1.0' - Key: loki:pack Value: !Ref PackName + - Key: loki:profile + Value: !Ref ProfileName UserData: Fn::Base64: !Sub | #!/bin/bash @@ -1013,6 +1159,7 @@ Resources: export LITELLM_MODEL="${LiteLLMModel}" export PROVIDER_API_KEY="${ProviderApiKey}" export PACK_NAME="${PackName}" + export PROFILE_NAME="${ProfileName}" # Publish failure to SSM and signal CFN on any error trap ' aws ssm put-parameter --name "/loki/setup-status" \ @@ -1043,6 +1190,7 @@ Resources: fi bash /tmp/loki-agent/deploy/bootstrap.sh \ --pack "$PACK_NAME" \ + --profile "$PROFILE_NAME" \ --region "$BEDROCK_REGION" \ --model "$DEFAULT_MODEL" \ --gw-port "$GW_PORT" \ @@ -1108,3 +1256,7 @@ Outputs: Description: "Deployed agent pack" Value: !Ref PackName + ProfileName: + Description: "Deployed permission profile" + Value: !Ref ProfileName + diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 686f9fb..40993d7 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -16,6 +16,7 @@ locals { "loki:deploy-method" = "terraform" "loki:version" = "1.0" "loki:pack" = var.pack_name + "loki:profile" = var.profile_name } vpc_id = var.existing_vpc_id != "" ? var.existing_vpc_id : (length(aws_vpc.main) > 0 ? aws_vpc.main[0].id : "") subnet_id = var.existing_subnet_id != "" ? var.existing_subnet_id : (length(aws_subnet.public) > 0 ? aws_subnet.public[0].id : "") @@ -151,11 +152,53 @@ resource "aws_iam_role_policy_attachment" "instance_ssm" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } +# builder: AdministratorAccess managed policy resource "aws_iam_role_policy_attachment" "instance_admin" { + count = var.profile_name == "builder" ? 1 : 0 role = aws_iam_role.instance.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" } +# account_assistant: ReadOnlyAccess managed policy +resource "aws_iam_role_policy_attachment" "instance_readonly" { + count = var.profile_name == "account_assistant" ? 1 : 0 + role = aws_iam_role.instance.name + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" +} + +# account_assistant: Bedrock inference (ReadOnlyAccess doesn't include invoke) +resource "aws_iam_role_policy" "account_assistant_bedrock" { + count = var.profile_name == "account_assistant" ? 1 : 0 + name = "${var.environment_name}-bedrock-inference" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/account_assistant_bedrock.json") +} + +# account_assistant: Deny secrets, S3 objects, Lambda code +resource "aws_iam_role_policy" "account_assistant_deny" { + count = var.profile_name == "account_assistant" ? 1 : 0 + name = "${var.environment_name}-deny-secrets-s3" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/account_assistant_deny.json") +} + +# personal_assistant: Bedrock + SSM connectivity only +resource "aws_iam_role_policy" "personal_assistant" { + count = var.profile_name == "personal_assistant" ? 1 : 0 + name = "${var.environment_name}-bedrock-only" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/personal_assistant.json") +} + +# non-builder: scoped bootstrap operations (SSM status + cfn-signal) +# builder has AdministratorAccess which already covers these +resource "aws_iam_role_policy" "bootstrap_operations" { + count = var.profile_name != "builder" ? 1 : 0 + name = "${var.environment_name}-bootstrap-ops" + role = aws_iam_role.instance.id + policy = file("${path.module}/policies/bootstrap_operations.json") +} + resource "aws_iam_instance_profile" "main" { name = "${var.environment_name}-profile" role = aws_iam_role.instance.name @@ -527,6 +570,7 @@ resource "aws_lambda_function" "security_enablement" { } resource "null_resource" "security_enablement_invoke" { + count = var.profile_name != "personal_assistant" ? 1 : 0 depends_on = [aws_lambda_function.security_enablement] provisioner "local-exec" { @@ -542,10 +586,11 @@ resource "null_resource" "security_enablement_invoke" { } # ============================================================================ -# Admin Console User +# Admin Console User — only for builder profile # ============================================================================ resource "aws_iam_user" "admin" { - name = "${var.environment_name}-admin" + count = var.profile_name == "builder" ? 1 : 0 + name = "${var.environment_name}-admin" tags = merge(local.loki_tags, { Name = "${var.environment_name}-admin" @@ -554,17 +599,20 @@ resource "aws_iam_user" "admin" { } resource "aws_iam_user_policy_attachment" "admin" { - user = aws_iam_user.admin.name + count = var.profile_name == "builder" ? 1 : 0 + user = aws_iam_user.admin[0].name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" } resource "aws_iam_access_key" "admin" { - user = aws_iam_user.admin.name + count = var.profile_name == "builder" ? 1 : 0 + user = aws_iam_user.admin[0].name } # Admin password Lambda resource "aws_iam_role" "admin_setup_lambda" { - name = "${var.environment_name}-admin-pw-role" + count = var.profile_name == "builder" ? 1 : 0 + name = "${var.environment_name}-admin-pw-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -577,7 +625,8 @@ resource "aws_iam_role" "admin_setup_lambda" { } resource "aws_iam_role_policy_attachment" "admin_setup_basic" { - role = aws_iam_role.admin_setup_lambda.name + count = var.profile_name == "builder" ? 1 : 0 + role = aws_iam_role.admin_setup_lambda[0].name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } @@ -599,8 +648,9 @@ def handler(event, context): } resource "aws_lambda_function" "admin_setup" { + count = var.profile_name == "builder" ? 1 : 0 function_name = "${var.environment_name}-admin-setup" - role = aws_iam_role.admin_setup_lambda.arn + role = aws_iam_role.admin_setup_lambda[0].arn handler = "index.handler" runtime = "python3.12" timeout = 30 @@ -611,6 +661,7 @@ resource "aws_lambda_function" "admin_setup" { } resource "null_resource" "admin_setup_invoke" { + count = var.profile_name == "builder" ? 1 : 0 depends_on = [aws_lambda_function.admin_setup, aws_iam_user.admin] provisioner "local-exec" { @@ -655,6 +706,7 @@ resource "aws_instance" "main" { region = data.aws_region.current.name environment_name = var.environment_name pack_name = var.pack_name + profile_name = var.profile_name default_model = var.default_model bedrock_region = var.bedrock_region gw_port = var.openclaw_gateway_port diff --git a/deploy/terraform/outputs.tf b/deploy/terraform/outputs.tf index c3220dd..5c6292a 100644 --- a/deploy/terraform/outputs.tf +++ b/deploy/terraform/outputs.tf @@ -37,3 +37,8 @@ output "pack_name" { description = "Deployed agent pack" value = var.pack_name } + +output "profile_name" { + description = "Deployed permission profile" + value = var.profile_name +} diff --git a/deploy/terraform/policies/account_assistant_bedrock.json b/deploy/terraform/policies/account_assistant_bedrock.json new file mode 100644 index 0000000..123f7ce --- /dev/null +++ b/deploy/terraform/policies/account_assistant_bedrock.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess", + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" + } + ] +} diff --git a/deploy/terraform/policies/account_assistant_deny.json b/deploy/terraform/policies/account_assistant_deny.json new file mode 100644 index 0000000..a8e0580 --- /dev/null +++ b/deploy/terraform/policies/account_assistant_deny.json @@ -0,0 +1,32 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenySecretValues", + "Effect": "Deny", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:GetResourcePolicy" + ], + "Resource": "*" + }, + { + "Sid": "DenyS3ObjectAccess", + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectAcl" + ], + "Resource": "*" + }, + { + "Sid": "DenyLambdaCodeAccess", + "Effect": "Deny", + "Action": [ + "lambda:GetFunction" + ], + "Resource": "*" + } + ] +} diff --git a/deploy/terraform/policies/bootstrap_operations.json b/deploy/terraform/policies/bootstrap_operations.json new file mode 100644 index 0000000..0a8e3aa --- /dev/null +++ b/deploy/terraform/policies/bootstrap_operations.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BootstrapOperations", + "Effect": "Allow", + "Action": [ + "ssm:PutParameter", + "ssm:DeleteParameter", + "cloudformation:SignalResource" + ], + "Resource": [ + "arn:aws:ssm:*:*:parameter/loki/*", + "arn:aws:cloudformation:*:*:stack/*" + ] + } + ] +} diff --git a/deploy/terraform/policies/personal_assistant.json b/deploy/terraform/policies/personal_assistant.json new file mode 100644 index 0000000..fa4b071 --- /dev/null +++ b/deploy/terraform/policies/personal_assistant.json @@ -0,0 +1,51 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess" + ], + "Resource": "*" + }, + { + "Sid": "SSMConnectivity", + "Effect": "Allow", + "Action": [ + "ssm:UpdateInstanceInformation", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply" + ], + "Resource": "*" + }, + { + "Sid": "BedrockDiscovery", + "Effect": "Allow", + "Action": [ + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" + }, + { + "Sid": "Identity", + "Effect": "Allow", + "Action": [ + "sts:GetCallerIdentity" + ], + "Resource": "*" + } + ] +} diff --git a/deploy/terraform/userdata.sh.tpl b/deploy/terraform/userdata.sh.tpl index 507f39e..47467e6 100644 --- a/deploy/terraform/userdata.sh.tpl +++ b/deploy/terraform/userdata.sh.tpl @@ -11,6 +11,7 @@ export LITELLM_API_KEY="${litellm_api_key}" export LITELLM_MODEL="${litellm_model}" export PROVIDER_API_KEY="${provider_api_key}" export PACK_NAME="${pack_name}" +export PROFILE_NAME="${profile_name}" # Publish failure to SSM on any error so the installer can detect it trap ' @@ -36,6 +37,7 @@ if [[ "$_cloned" != "true" ]]; then fi bash /tmp/loki-agent/deploy/bootstrap.sh \ --pack "$PACK_NAME" \ + --profile "$PROFILE_NAME" \ --region "$BEDROCK_REGION" \ --model "$DEFAULT_MODEL" \ --gw-port "$GW_PORT" \ diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 3196fe1..5dec783 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -4,6 +4,16 @@ variable "aws_region" { description = "AWS region for infrastructure deployment. Defaults to us-east-1." } +variable "profile_name" { + type = string + description = "Permission profile. 'builder' = full admin. 'account_assistant' = read-only. 'personal_assistant' = Bedrock only." + # No default — must be explicitly specified + validation { + condition = contains(["builder", "account_assistant", "personal_assistant"], var.profile_name) + error_message = "profile_name must be one of: builder, account_assistant, personal_assistant." + } +} + variable "pack_name" { description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, or ironclaw)" type = string diff --git a/install.sh b/install.sh index 08aa345..5dbd8e1 100755 --- a/install.sh +++ b/install.sh @@ -39,9 +39,11 @@ INSTALLER_VERSION="0.5.37" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack # --method : pre-select deploy method (cfn, terraform/tf) +# --profile

: pre-select permission profile (builder, account_assistant, personal_assistant) AUTO_YES=false PRESELECT_PACK="" PRESELECT_METHOD="" +PRESELECT_PROFILE="" while [[ $# -gt 0 ]]; do case "$1" in --non-interactive|--yes|-y) AUTO_YES=true; shift ;; @@ -57,6 +59,12 @@ while [[ $# -gt 0 ]]; do exit 1 fi PRESELECT_METHOD="$2"; shift 2 ;; + --profile) + if [[ $# -lt 2 || "$2" == --* ]]; then + echo -e "\033[0;31m✗\033[0m --profile requires a value (builder, account_assistant, personal_assistant)" >&2 + exit 1 + fi + PRESELECT_PROFILE="$2"; shift 2 ;; *) shift ;; esac done @@ -240,6 +248,7 @@ show_banner() { local auto_msg="Running in non-interactive mode — using defaults, minimal prompts" [[ -n "${PRESELECT_PACK}" ]] && auto_msg+=", pack: ${PRESELECT_PACK}" [[ -n "${PRESELECT_METHOD}" ]] && auto_msg+=", method: ${PRESELECT_METHOD}" + [[ -n "${PRESELECT_PROFILE}" ]] && auto_msg+=", profile: ${PRESELECT_PROFILE}" info "$auto_msg" fi echo "" @@ -527,6 +536,81 @@ choose_deploy_method() { fi } +# ============================================================================ +# Profile selection — REQUIRED, no default +# ============================================================================ +PROFILE_NAME="" # set by choose_profile() + +choose_profile() { + local valid_profiles=("builder" "account_assistant" "personal_assistant") + + # Validate a profile name against allowed values + _is_valid_profile() { + local p="$1" + for vp in "${valid_profiles[@]}"; do [[ "$p" == "$vp" ]] && return 0; done + return 1 + } + + if [[ -n "${PRESELECT_PROFILE}" ]]; then + if ! _is_valid_profile "${PRESELECT_PROFILE}"; then + echo "" + echo -e " ${RED}✗ Unknown profile: '${PRESELECT_PROFILE}'${NC}" + echo "" + echo " Valid profiles:" + echo " builder — Full AWS admin access" + echo " account_assistant — Read-only AWS access" + echo " personal_assistant — Bedrock only, no AWS" + echo "" + fail "Use --profile with one of the profiles listed above." + fi + PROFILE_NAME="${PRESELECT_PROFILE}" + ok "Profile pre-selected: ${PROFILE_NAME}" + return + fi + + # Non-interactive without --profile: fail — no silent default + if [[ "$AUTO_YES" == true ]]; then + fail "Profile is required in non-interactive mode. Use --profile " + fi + + # Interactive: show menu and prompt + echo "" + echo " Permission profiles (REQUIRED — choose one):" + echo "" + echo -e " 1) ${RED}builder${NC} — Full AWS admin access." + echo " Can create, modify, and delete any AWS resource." + echo " Best for: building apps, deploying infra, managing pipelines." + echo "" + echo -e " 2) ${YELLOW}account_assistant${NC} — Read-only AWS access." + echo " Can see everything, change nothing." + echo " Best for: cost analysis, architecture review, debugging help." + echo "" + echo -e " 3) ${GREEN}personal_assistant${NC} — Bedrock only. No AWS access." + echo " Best for: writing, research, coding help, daily tasks." + echo "" + + local profile_choice + prompt "Select profile" profile_choice "" + + case "$profile_choice" in + 1|builder) PROFILE_NAME="builder" ;; + 2|account_assistant) PROFILE_NAME="account_assistant" ;; + 3|personal_assistant) PROFILE_NAME="personal_assistant" ;; + "") + fail "Profile is required. Enter 1, 2, or 3 (or a profile name)." + ;; + *) + if _is_valid_profile "$profile_choice"; then + PROFILE_NAME="$profile_choice" + else + fail "Invalid profile '${profile_choice}'. Choose: builder, account_assistant, or personal_assistant." + fi + ;; + esac + + ok "Profile selected: ${PROFILE_NAME}" +} + collect_config() { echo "" info "Configuration" @@ -612,6 +696,9 @@ collect_config() { ok "Selected pack: ${PACK_NAME}" fi # end of interactive pack selection + # ---- Profile selection (REQUIRED) ---------------------------------------- + choose_profile + prompt "AWS region" DEPLOY_REGION "$REGION" # Count existing deployments to generate a smart default env name @@ -628,14 +715,23 @@ collect_config() { ENV_NAME=$(echo "$ENV_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-') prompt "Loki watermark (tag to identify this deployment)" LOKI_WATERMARK "$ENV_NAME" - # Adjust instance size default based on pack registry + # Adjust instance size default: profile takes precedence, pack registry as fallback local default_size_choice="3" # default → t4g.xlarge - local pack_instance_type - pack_instance_type=$([ -n "$registry" ] && jq -r --arg p "$PACK_NAME" '.packs[$p].instance_type // "t4g.xlarge"' "$registry" 2>/dev/null || echo "t4g.xlarge") - case "$pack_instance_type" in - t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; - t4g.large) default_size_choice="2" ;; - *) default_size_choice="3" ;; + # Profile-driven defaults (profile wins over pack registry) + case "${PROFILE_NAME:-}" in + builder) default_size_choice="3" ;; # t4g.xlarge + account_assistant) default_size_choice="1" ;; # t4g.medium + personal_assistant) default_size_choice="1" ;; # t4g.medium + *) + # Fallback: pack registry instance_type + local pack_instance_type + pack_instance_type=$([ -n "$registry" ] && jq -r --arg p "$PACK_NAME" '.packs[$p].instance_type // "t4g.xlarge"' "$registry" 2>/dev/null || echo "t4g.xlarge") + case "$pack_instance_type" in + t4g.medium) default_size_choice="1"; info "${PACK_NAME} is lightweight — defaulting to t4g.medium" ;; + t4g.large) default_size_choice="2" ;; + *) default_size_choice="3" ;; + esac + ;; esac echo "" echo " Instance sizes:" @@ -689,8 +785,8 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console, CFN CLI, Terraform # ============================================================================ # ⚠ KEEP THESE THREE ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName InstanceType ModelMode BedrockRegion LokiWatermark EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId) -PARAM_TF_NAMES=(environment_name pack_name instance_type model_mode bedrock_region loki_watermark enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder existing_vpc_id existing_subnet_id) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType ModelMode BedrockRegion LokiWatermark EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId) +PARAM_TF_NAMES=(environment_name pack_name profile_name instance_type model_mode bedrock_region loki_watermark enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder existing_vpc_id existing_subnet_id) PARAM_VALUES=() # populated by build_deploy_params() # Populate PARAM_VALUES from user config (call after collect_config) @@ -698,6 +794,7 @@ build_deploy_params() { PARAM_VALUES=( "$ENV_NAME" "$PACK_NAME" + "$PROFILE_NAME" "$INSTANCE_TYPE" "bedrock" "$DEPLOY_REGION" @@ -752,6 +849,7 @@ show_summary() { echo -e " ${BOLD}╭─────────────── Deploy Summary ───────────────╮${NC}" echo -e " ${BOLD}│${NC} Environment: ${ENV_NAME}" echo -e " ${BOLD}│${NC} Pack: ${PACK_NAME}" + echo -e " ${BOLD}│${NC} Profile: ${PROFILE_NAME}" echo -e " ${BOLD}│${NC} Instance: ${INSTANCE_TYPE}" echo -e " ${BOLD}│${NC} Region: ${DEPLOY_REGION}" echo -e " ${BOLD}│${NC} Watermark: ${LOKI_WATERMARK}" diff --git a/profiles/account_assistant_bedrock.json b/profiles/account_assistant_bedrock.json new file mode 100644 index 0000000..123f7ce --- /dev/null +++ b/profiles/account_assistant_bedrock.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess", + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" + } + ] +} diff --git a/profiles/account_assistant_deny.json b/profiles/account_assistant_deny.json new file mode 100644 index 0000000..a8e0580 --- /dev/null +++ b/profiles/account_assistant_deny.json @@ -0,0 +1,32 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenySecretValues", + "Effect": "Deny", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:GetResourcePolicy" + ], + "Resource": "*" + }, + { + "Sid": "DenyS3ObjectAccess", + "Effect": "Deny", + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectAcl" + ], + "Resource": "*" + }, + { + "Sid": "DenyLambdaCodeAccess", + "Effect": "Deny", + "Action": [ + "lambda:GetFunction" + ], + "Resource": "*" + } + ] +} diff --git a/profiles/bootstrap_operations.json b/profiles/bootstrap_operations.json new file mode 100644 index 0000000..0a8e3aa --- /dev/null +++ b/profiles/bootstrap_operations.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BootstrapOperations", + "Effect": "Allow", + "Action": [ + "ssm:PutParameter", + "ssm:DeleteParameter", + "cloudformation:SignalResource" + ], + "Resource": [ + "arn:aws:ssm:*:*:parameter/loki/*", + "arn:aws:cloudformation:*:*:stack/*" + ] + } + ] +} diff --git a/profiles/builder.json b/profiles/builder.json new file mode 100644 index 0000000..3f1df3e --- /dev/null +++ b/profiles/builder.json @@ -0,0 +1,3 @@ +{ + "_comment": "builder profile uses AWS Managed Policy arn:aws:iam::aws:policy/AdministratorAccess — no inline policy needed" +} diff --git a/profiles/personal_assistant.json b/profiles/personal_assistant.json new file mode 100644 index 0000000..fa4b071 --- /dev/null +++ b/profiles/personal_assistant.json @@ -0,0 +1,51 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "BedrockInference", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream", + "bedrock:GetUseCaseForModelAccess" + ], + "Resource": "*" + }, + { + "Sid": "SSMConnectivity", + "Effect": "Allow", + "Action": [ + "ssm:UpdateInstanceInformation", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply" + ], + "Resource": "*" + }, + { + "Sid": "BedrockDiscovery", + "Effect": "Allow", + "Action": [ + "bedrock:ListFoundationModels", + "bedrock:GetFoundationModel", + "bedrock:ListInferenceProfiles" + ], + "Resource": "*" + }, + { + "Sid": "Identity", + "Effect": "Allow", + "Action": [ + "sts:GetCallerIdentity" + ], + "Resource": "*" + } + ] +} diff --git a/profiles/registry.yaml b/profiles/registry.yaml new file mode 100644 index 0000000..44f5db4 --- /dev/null +++ b/profiles/registry.yaml @@ -0,0 +1,39 @@ +version: 1 + +profiles: + builder: + description: "Full-stack AWS builder — can create, modify, and delete any AWS resource" + instance_type: t4g.xlarge + iam_mode: managed + managed_policies: + - arn:aws:iam::aws:policy/AdministratorAccess + inline_policies: [] + deny_policies: [] + bootstrap_policies: [] + security_services: true + + account_assistant: + description: "Read-only AWS advisor — can see everything, change nothing" + instance_type: t4g.medium + iam_mode: managed + managed_policies: + - arn:aws:iam::aws:policy/ReadOnlyAccess + inline_policies: + - profiles/account_assistant_bedrock.json + deny_policies: + - profiles/account_assistant_deny.json + bootstrap_policies: + - profiles/bootstrap_operations.json + security_services: true + + personal_assistant: + description: "Personal helper — Bedrock inference only, no AWS access" + instance_type: t4g.medium + iam_mode: inline + managed_policies: [] + inline_policies: + - profiles/personal_assistant.json + deny_policies: [] + bootstrap_policies: + - profiles/bootstrap_operations.json + security_services: false diff --git a/tests/test-profiles.sh b/tests/test-profiles.sh new file mode 100644 index 0000000..57eb20f --- /dev/null +++ b/tests/test-profiles.sh @@ -0,0 +1,665 @@ +#!/usr/bin/env bash +# tests/test-profiles.sh — Profile selection and IAM policy tests +# +# Tests: +# 1. --profile flag parsing (valid, invalid, missing value) +# 2. --non-interactive without --profile = error +# 3. --non-interactive with --profile = works (no profile-related error) +# 4. Profile registry YAML parsing (structure, all 3 profiles present) +# 5. All 3 profiles resolve correctly (instance type, IAM mode) +# 6. Policy JSON files (valid JSON, expected keys) +# 7. Instance size defaults per profile +# +# Run: bash tests/test-profiles.sh + +set -uo pipefail # no -e: test assertions may expect non-zero exits + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PROFILES_DIR="${SCRIPT_DIR}/profiles" +INSTALL_SH="${SCRIPT_DIR}/install.sh" + +PASS=0; FAIL=0 + +pass() { printf " \033[0;32m✓\033[0m %s\n" "$1"; PASS=$((PASS + 1)); } +fail_test() { printf " \033[0;31m✗\033[0m %s\n" "$1"; FAIL=$((FAIL + 1)); } +header() { printf "\n\033[1m%s\033[0m\n" "$1"; } + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +# Run install.sh with given args; capture output and exit code +run_installer() { + local args=("$@") + output=$(bash "$INSTALL_SH" "${args[@]}" 2>&1 || true) + exit_code=$? +} + +# Parse --profile flag in isolation (mirrors what install.sh does) +parse_profile_flag() { + local preselect="" + local args=("$@") + local i=0 + while [[ $i -lt ${#args[@]} ]]; do + if [[ "${args[$i]}" == "--profile" ]]; then + local next_i=$((i + 1)) + if [[ $next_i -ge ${#args[@]} ]] || [[ "${args[$next_i]}" == --* ]]; then + echo "ERROR: --profile requires a value" >&2 + return 1 + fi + preselect="${args[$next_i]}" + i=$((i + 2)) + else + i=$((i + 1)) + fi + done + echo "$preselect" + return 0 +} + +# Validate a profile name +validate_profile() { + local p="$1" + case "$p" in + builder|account_assistant|personal_assistant) return 0 ;; + *) return 1 ;; + esac +} + +# Parse profiles/registry.yaml with a given key +# Usage: registry_profile_field +registry_profile_field() { + local profile="$1" field="$2" + awk " + /^ ${profile}:/{found=1; next} + found && /^ [a-z]/{exit} + found && /^ ${field}:/{gsub(/^ ${field}: /, \"\"); print; exit} + " "${PROFILES_DIR}/registry.yaml" +} + +registry_profile_list_field() { + local profile="$1" field="$2" + awk " + /^ ${profile}:/{found=1; in_field=0; next} + found && /^ [a-z]/{exit} + found && /^ ${field}:/{in_field=1; next} + found && in_field && /^ - /{gsub(/^ - /, \"\"); print; next} + found && in_field && !/^ /{in_field=0} + " "${PROFILES_DIR}/registry.yaml" +} + +# ── Section 1: --profile flag parsing ──────────────────────────────────────── +header "Test: --profile flag parsing" + +# Missing value (no argument after --profile) +if ! parse_profile_flag --profile 2>/dev/null; then + pass "--profile with no value: returns error" +else + fail_test "--profile with no value: should return error" +fi + +# Missing value (another flag follows immediately) +if ! parse_profile_flag --profile --non-interactive 2>/dev/null; then + pass "--profile followed by flag: returns error" +else + fail_test "--profile followed by flag: should return error" +fi + +# Valid values +for profile in builder account_assistant personal_assistant; do + result=$(parse_profile_flag --profile "$profile" 2>/dev/null || true) + if [[ "$result" == "$profile" ]]; then + pass "--profile $profile: parsed correctly" + else + fail_test "--profile $profile: expected '$profile', got '${result:-empty}'" + fi +done + +# install.sh itself: --profile with no value should exit non-zero and show error +output=$(bash "$INSTALL_SH" --profile 2>&1 || true) +if echo "$output" | grep -q "requires a value"; then + pass "install.sh --profile (no value): exits with 'requires a value' message" +else + fail_test "install.sh --profile (no value): expected 'requires a value', got: ${output:0:200}" +fi + +# ── Section 2: Profile name validation ─────────────────────────────────────── +header "Test: Profile name validation" + +for valid in builder account_assistant personal_assistant; do + if validate_profile "$valid"; then + pass "validate_profile: '$valid' is valid" + else + fail_test "validate_profile: '$valid' should be valid" + fi +done + +for invalid in "" "admin" "superuser" "Builder" "BUILDER" "read_only"; do + if ! validate_profile "$invalid"; then + pass "validate_profile: '${invalid:-empty}' is correctly rejected" + else + fail_test "validate_profile: '${invalid:-empty}' should be rejected" + fi +done + +# ── Section 3: --non-interactive without --profile = error ─────────────────── +header "Test: --non-interactive without --profile" + +# When AUTO_YES=true and PRESELECT_PROFILE is empty, choose_profile must fail. +# Test this by sourcing the choose_profile function with mocked dependencies. +_test_choose_profile_autofail() { + # Inline the choose_profile logic for testing + local AUTO_YES="$1" + local PRESELECT_PROFILE="$2" + local PROFILE_NAME="" + + # Minimal implementations of install.sh helpers used by choose_profile + local _fail_called=false + fail_fn() { _fail_called=true; return 1; } + ok_fn() { :; } + + if [[ -n "$PRESELECT_PROFILE" ]]; then + if validate_profile "$PRESELECT_PROFILE"; then + PROFILE_NAME="$PRESELECT_PROFILE" + echo "PROFILE_NAME=$PROFILE_NAME" + return 0 + else + fail_fn "Invalid profile: $PRESELECT_PROFILE" + return 1 + fi + fi + + if [[ "$AUTO_YES" == true ]]; then + fail_fn "Profile is required in non-interactive mode" + return 1 + fi + + # Interactive: would prompt (not tested here) + return 0 +} + +# --non-interactive without --profile → should fail +if ! _test_choose_profile_autofail true "" 2>/dev/null; then + pass "--non-interactive without --profile: choose_profile fails" +else + fail_test "--non-interactive without --profile: should fail but succeeded" +fi + +# --non-interactive with valid --profile → should succeed +for profile in builder account_assistant personal_assistant; do + result=$(_test_choose_profile_autofail true "$profile" 2>/dev/null || true) + if echo "$result" | grep -q "PROFILE_NAME=$profile"; then + pass "--non-interactive with --profile $profile: choose_profile succeeds" + else + fail_test "--non-interactive with --profile $profile: expected success, got: $result" + fi +done + +# --non-interactive with invalid --profile → should fail +if ! _test_choose_profile_autofail true "superuser" 2>/dev/null; then + pass "--non-interactive with invalid profile: choose_profile fails" +else + fail_test "--non-interactive with invalid profile: should fail" +fi + +# ── Section 4: Profile registry YAML structure ─────────────────────────────── +header "Test: profiles/registry.yaml structure" + +if [[ -f "${PROFILES_DIR}/registry.yaml" ]]; then + pass "profiles/registry.yaml exists" +else + fail_test "profiles/registry.yaml: file not found" +fi + +# Check all 3 profiles present +for profile in builder account_assistant personal_assistant; do + if grep -q "^ ${profile}:" "${PROFILES_DIR}/registry.yaml" 2>/dev/null; then + pass "registry.yaml: '$profile' profile present" + else + fail_test "registry.yaml: '$profile' profile MISSING" + fi +done + +# Check required fields for each profile +for profile in builder account_assistant personal_assistant; do + for field in description instance_type iam_mode; do + val=$(registry_profile_field "$profile" "$field" 2>/dev/null || true) + if [[ -n "$val" ]]; then + pass "registry.yaml: $profile.$field = '$val'" + else + fail_test "registry.yaml: $profile.$field is missing or empty" + fi + done +done + +# Check managed_policies for builder +builder_policies=$(registry_profile_list_field builder managed_policies 2>/dev/null || true) +if echo "$builder_policies" | grep -q "AdministratorAccess"; then + pass "registry.yaml: builder has AdministratorAccess" +else + fail_test "registry.yaml: builder should have AdministratorAccess" +fi + +# Check managed_policies for account_assistant +aa_policies=$(registry_profile_list_field account_assistant managed_policies 2>/dev/null || true) +if echo "$aa_policies" | grep -q "ReadOnlyAccess"; then + pass "registry.yaml: account_assistant has ReadOnlyAccess" +else + fail_test "registry.yaml: account_assistant should have ReadOnlyAccess" +fi + +# Check security_services flag +builder_sec=$(registry_profile_field builder security_services 2>/dev/null || true) +if [[ "$builder_sec" == "true" ]]; then + pass "registry.yaml: builder has security_services=true" +else + fail_test "registry.yaml: builder should have security_services=true (got: '$builder_sec')" +fi + +pa_sec=$(registry_profile_field personal_assistant security_services 2>/dev/null || true) +if [[ "$pa_sec" == "false" ]]; then + pass "registry.yaml: personal_assistant has security_services=false" +else + fail_test "registry.yaml: personal_assistant should have security_services=false (got: '$pa_sec')" +fi + +# ── Section 5: All 3 profiles resolve correctly ─────────────────────────────── +header "Test: Profiles resolve correctly (instance types, IAM mode)" + +# builder → t4g.xlarge, managed (AdministratorAccess) +builder_itype=$(registry_profile_field builder instance_type 2>/dev/null || true) +if [[ "$builder_itype" == "t4g.xlarge" ]]; then + pass "builder: instance_type = t4g.xlarge" +else + fail_test "builder: instance_type should be t4g.xlarge, got '$builder_itype'" +fi + +builder_iam=$(registry_profile_field builder iam_mode 2>/dev/null || true) +if [[ "$builder_iam" == "managed" ]]; then + pass "builder: iam_mode = managed" +else + fail_test "builder: iam_mode should be managed, got '$builder_iam'" +fi + +# account_assistant → t4g.medium, managed (ReadOnlyAccess) +aa_itype=$(registry_profile_field account_assistant instance_type 2>/dev/null || true) +if [[ "$aa_itype" == "t4g.medium" ]]; then + pass "account_assistant: instance_type = t4g.medium" +else + fail_test "account_assistant: instance_type should be t4g.medium, got '$aa_itype'" +fi + +aa_iam=$(registry_profile_field account_assistant iam_mode 2>/dev/null || true) +if [[ "$aa_iam" == "managed" ]]; then + pass "account_assistant: iam_mode = managed" +else + fail_test "account_assistant: iam_mode should be managed, got '$aa_iam'" +fi + +# personal_assistant → t4g.medium, inline +pa_itype=$(registry_profile_field personal_assistant instance_type 2>/dev/null || true) +if [[ "$pa_itype" == "t4g.medium" ]]; then + pass "personal_assistant: instance_type = t4g.medium" +else + fail_test "personal_assistant: instance_type should be t4g.medium, got '$pa_itype'" +fi + +pa_iam=$(registry_profile_field personal_assistant iam_mode 2>/dev/null || true) +if [[ "$pa_iam" == "inline" ]]; then + pass "personal_assistant: iam_mode = inline" +else + fail_test "personal_assistant: iam_mode should be inline, got '$pa_iam'" +fi + +# ── Section 6: Instance size defaults per profile ──────────────────────────── +header "Test: Instance size defaults per profile" + +_profile_to_size_choice() { + local profile="$1" + case "$profile" in + builder) echo "3" ;; # t4g.xlarge + account_assistant) echo "1" ;; # t4g.medium + personal_assistant) echo "1" ;; # t4g.medium + *) echo "" ;; + esac +} + +_size_choice_to_instance() { + local choice="$1" + case "$choice" in + 1) echo "t4g.medium" ;; + 2) echo "t4g.large" ;; + 3) echo "t4g.xlarge" ;; + *) echo "t4g.xlarge" ;; + esac +} + +for profile in builder account_assistant personal_assistant; do + choice=$(_profile_to_size_choice "$profile") + itype=$(_size_choice_to_instance "$choice") + expected_itype=$(registry_profile_field "$profile" instance_type 2>/dev/null || true) + + if [[ "$itype" == "$expected_itype" ]]; then + pass "Instance default for $profile: $itype (matches registry)" + else + fail_test "Instance default for $profile: got $itype, registry says $expected_itype" + fi +done + +# ── Section 7: Policy JSON files ───────────────────────────────────────────── +header "Test: Policy JSON files" + +POLICY_FILES=( + "account_assistant_deny.json" + "account_assistant_bedrock.json" + "personal_assistant.json" + "bootstrap_operations.json" +) + +for pf in "${POLICY_FILES[@]}"; do + fp="${PROFILES_DIR}/${pf}" + if [[ -f "$fp" ]]; then + pass "Policy file exists: $pf" + else + fail_test "Policy file MISSING: $pf" + continue + fi + + # Validate JSON + if jq empty "$fp" 2>/dev/null; then + pass "Policy file valid JSON: $pf" + else + fail_test "Policy file invalid JSON: $pf" + continue + fi + + # Check it has Version and Statement + if jq -e '.Version' "$fp" >/dev/null 2>&1; then + pass "$pf has Version field" + else + fail_test "$pf missing Version field" + fi + + if jq -e '.Statement | length > 0' "$fp" >/dev/null 2>&1; then + pass "$pf has non-empty Statement" + else + fail_test "$pf has empty or missing Statement" + fi +done + +# builder.json is empty (comment placeholder for AdministratorAccess) +if [[ -f "${PROFILES_DIR}/builder.json" ]]; then + pass "profiles/builder.json exists (empty placeholder)" +else + fail_test "profiles/builder.json MISSING" +fi + +# ── Section 8: account_assistant_deny.json content ─────────────────────────── +header "Test: account_assistant_deny.json content" + +DENY_FILE="${PROFILES_DIR}/account_assistant_deny.json" +if [[ -f "$DENY_FILE" ]]; then + # Should deny secret values + if jq -e '.Statement[] | select(.Sid == "DenySecretValues")' "$DENY_FILE" >/dev/null 2>&1; then + pass "deny policy: has DenySecretValues statement" + else + fail_test "deny policy: missing DenySecretValues statement" + fi + + # Should deny S3 object access + if jq -e '.Statement[] | select(.Sid == "DenyS3ObjectAccess")' "$DENY_FILE" >/dev/null 2>&1; then + pass "deny policy: has DenyS3ObjectAccess statement" + else + fail_test "deny policy: missing DenyS3ObjectAccess statement" + fi + + # Should deny Lambda code access + if jq -e '.Statement[] | select(.Sid == "DenyLambdaCodeAccess")' "$DENY_FILE" >/dev/null 2>&1; then + pass "deny policy: has DenyLambdaCodeAccess statement" + else + fail_test "deny policy: missing DenyLambdaCodeAccess statement" + fi + + # All effects must be Deny + all_deny=$(jq -e '[.Statement[].Effect] | all(. == "Deny")' "$DENY_FILE" 2>/dev/null || echo "false") + if [[ "$all_deny" == "true" ]]; then + pass "deny policy: all statements have Effect=Deny" + else + fail_test "deny policy: not all statements have Effect=Deny" + fi +fi + +# ── Section 9: personal_assistant.json content ─────────────────────────────── +header "Test: personal_assistant.json content" + +PA_FILE="${PROFILES_DIR}/personal_assistant.json" +if [[ -f "$PA_FILE" ]]; then + # Must include Bedrock inference + if jq -e '.Statement[] | select(.Sid == "BedrockInference")' "$PA_FILE" >/dev/null 2>&1; then + pass "personal_assistant policy: has BedrockInference" + else + fail_test "personal_assistant policy: missing BedrockInference" + fi + + # Must include SSM connectivity + if jq -e '.Statement[] | select(.Sid == "SSMConnectivity")' "$PA_FILE" >/dev/null 2>&1; then + pass "personal_assistant policy: has SSMConnectivity" + else + fail_test "personal_assistant policy: missing SSMConnectivity" + fi + + # Must include Identity (sts:GetCallerIdentity) + if jq -e '.Statement[] | .Action | if type == "array" then .[] else . end | select(. == "sts:GetCallerIdentity")' "$PA_FILE" >/dev/null 2>&1; then + pass "personal_assistant policy: has sts:GetCallerIdentity" + else + fail_test "personal_assistant policy: missing sts:GetCallerIdentity" + fi + + # Must NOT allow arbitrary AWS actions (sanity check: no AdministratorAccess action) + admin_action=$(jq -e '.Statement[] | .Action | if type == "array" then .[] else . end | select(. == "*")' "$PA_FILE" 2>/dev/null || echo "") + if [[ -z "$admin_action" ]]; then + pass "personal_assistant policy: no wildcard (*) actions" + else + fail_test "personal_assistant policy: should not have wildcard (*) actions" + fi +fi + +# ── Section 10: bootstrap_operations.json content ──────────────────────────── +header "Test: bootstrap_operations.json content" + +BOOTSTRAP_FILE="${PROFILES_DIR}/bootstrap_operations.json" +if [[ -f "$BOOTSTRAP_FILE" ]]; then + # Must include ssm:PutParameter + if jq -e '.Statement[] | .Action | if type == "array" then .[] else . end | select(. == "ssm:PutParameter")' "$BOOTSTRAP_FILE" >/dev/null 2>&1; then + pass "bootstrap policy: has ssm:PutParameter" + else + fail_test "bootstrap policy: missing ssm:PutParameter" + fi + + # Must include cloudformation:SignalResource + if jq -e '.Statement[] | .Action | if type == "array" then .[] else . end | select(. == "cloudformation:SignalResource")' "$BOOTSTRAP_FILE" >/dev/null 2>&1; then + pass "bootstrap policy: has cloudformation:SignalResource" + else + fail_test "bootstrap policy: missing cloudformation:SignalResource" + fi + + # Must be scoped (Resource must not be "*") + all_wildcard=$(jq -e '[.Statement[].Resource] | all(. == "*")' "$BOOTSTRAP_FILE" 2>/dev/null || echo "false") + if [[ "$all_wildcard" != "true" ]]; then + pass "bootstrap policy: resources are scoped (not all wildcard)" + else + fail_test "bootstrap policy: resources should be scoped, not all '*'" + fi +fi + +# ── Section 11: install.sh has ProfileName in param arrays ─────────────────── +header "Test: install.sh ProfileName parameter plumbing" + +if grep -q "ProfileName" "$INSTALL_SH" 2>/dev/null; then + pass "install.sh: ProfileName found in source" +else + fail_test "install.sh: ProfileName missing from source (not yet implemented)" +fi + +if grep -q "profile_name" "$INSTALL_SH" 2>/dev/null; then + pass "install.sh: profile_name found in source (TF param)" +else + fail_test "install.sh: profile_name missing from source (TF param)" +fi + +if grep -q "PRESELECT_PROFILE" "$INSTALL_SH" 2>/dev/null; then + pass "install.sh: PRESELECT_PROFILE variable found" +else + fail_test "install.sh: PRESELECT_PROFILE variable missing" +fi + +if grep -q "choose_profile" "$INSTALL_SH" 2>/dev/null; then + pass "install.sh: choose_profile() function found" +else + fail_test "install.sh: choose_profile() function missing" +fi + +# ── Section 12: bootstrap.sh has --profile ─────────────────────────────────── +header "Test: bootstrap.sh --profile support" + +BOOTSTRAP_SH="${SCRIPT_DIR}/deploy/bootstrap.sh" + +if grep -q "\-\-profile" "$BOOTSTRAP_SH" 2>/dev/null; then + pass "bootstrap.sh: --profile arg found" +else + fail_test "bootstrap.sh: --profile arg missing" +fi + +if grep -q "PROFILE_NAME" "$BOOTSTRAP_SH" 2>/dev/null; then + pass "bootstrap.sh: PROFILE_NAME variable found" +else + fail_test "bootstrap.sh: PROFILE_NAME variable missing" +fi + +if grep -q '\.profile' "$BOOTSTRAP_SH" 2>/dev/null; then + pass "bootstrap.sh: .profile marker file referenced" +else + fail_test "bootstrap.sh: .profile marker file not referenced" +fi + +if grep -q "personal_assistant" "$BOOTSTRAP_SH" 2>/dev/null; then + pass "bootstrap.sh: personal_assistant profile-specific handling found" +else + fail_test "bootstrap.sh: personal_assistant profile handling missing" +fi + +# ── Section 13: CFN template has ProfileName ────────────────────────────────── +header "Test: CFN template ProfileName parameter" + +CFN_TEMPLATE="${SCRIPT_DIR}/deploy/cloudformation/template.yaml" + +if grep -q "ProfileName" "$CFN_TEMPLATE" 2>/dev/null; then + pass "CFN template: ProfileName parameter found" +else + fail_test "CFN template: ProfileName parameter missing" +fi + +if grep -q "IsBuilder" "$CFN_TEMPLATE" 2>/dev/null; then + pass "CFN template: IsBuilder condition found" +else + fail_test "CFN template: IsBuilder condition missing" +fi + +if grep -q "IsPersonalAssistant" "$CFN_TEMPLATE" 2>/dev/null; then + pass "CFN template: IsPersonalAssistant condition found" +else + fail_test "CFN template: IsPersonalAssistant condition missing" +fi + +if grep -q "NeedsAdminUser" "$CFN_TEMPLATE" 2>/dev/null; then + pass "CFN template: NeedsAdminUser condition found" +else + fail_test "CFN template: NeedsAdminUser condition missing" +fi + +if grep -q "loki:profile" "$CFN_TEMPLATE" 2>/dev/null; then + pass "CFN template: loki:profile instance tag found" +else + fail_test "CFN template: loki:profile instance tag missing" +fi + +# ── Section 14: Terraform has profile_name ──────────────────────────────────── +header "Test: Terraform profile_name variable" + +TF_VARS="${SCRIPT_DIR}/deploy/terraform/variables.tf" +TF_MAIN="${SCRIPT_DIR}/deploy/terraform/main.tf" + +if grep -q "profile_name" "$TF_VARS" 2>/dev/null; then + pass "Terraform variables.tf: profile_name variable found" +else + fail_test "Terraform variables.tf: profile_name variable missing" +fi + +if grep -q "contains.*builder.*account_assistant.*personal_assistant" "$TF_VARS" 2>/dev/null; then + pass "Terraform variables.tf: profile_name has validation" +else + fail_test "Terraform variables.tf: profile_name missing validation" +fi + +if grep -q "loki:profile" "$TF_MAIN" 2>/dev/null; then + pass "Terraform main.tf: loki:profile tag found" +else + fail_test "Terraform main.tf: loki:profile tag missing" +fi + +# Check TF policy files +for pf in account_assistant_deny.json account_assistant_bedrock.json personal_assistant.json bootstrap_operations.json; do + if [[ -f "${SCRIPT_DIR}/deploy/terraform/policies/${pf}" ]]; then + pass "TF policies/$pf exists" + else + fail_test "TF policies/$pf: file missing" + fi +done + +# ── bash -n syntax checks on all modified shell scripts ─────────────────────── +header "Test: bash -n syntax checks" + +for script in "$INSTALL_SH" "${SCRIPT_DIR}/deploy/bootstrap.sh"; do + if bash -n "$script" 2>/dev/null; then + pass "bash -n $(basename $script): no syntax errors" + else + err=$(bash -n "$script" 2>&1) + fail_test "bash -n $(basename $script): SYNTAX ERROR: $err" + fi +done + +# ── Results ─────────────────────────────────────────────────────────────────── +printf "\033[1mTest: Policy file DRY check (profiles/ vs deploy/terraform/policies/)\033[0m\n" +for pfile in account_assistant_deny.json account_assistant_bedrock.json personal_assistant.json bootstrap_operations.json; do + if [[ -f "${PROFILES_DIR}/${pfile}" ]] && [[ -f "${SCRIPT_DIR}/deploy/terraform/policies/${pfile}" ]]; then + if diff -q "${PROFILES_DIR}/${pfile}" "${SCRIPT_DIR}/deploy/terraform/policies/${pfile}" >/dev/null 2>&1; then + pass "DRY: ${pfile} identical in profiles/ and deploy/terraform/policies/" + else + fail "DRY DRIFT: ${pfile} differs between profiles/ and deploy/terraform/policies/" + fi + else + fail "DRY: ${pfile} missing in one location" + fi +done + +printf "\033[1mTest: Deny policy does NOT block SSM Agent\033[0m\n" +# ssm:GetParameter must NOT be in the deny — it breaks SSM Session Manager +if grep -q '"ssm:GetParameter"' "${PROFILES_DIR}/account_assistant_deny.json" 2>/dev/null; then + fail "account_assistant_deny.json contains ssm:GetParameter — this breaks SSM Agent!" +else + pass "account_assistant_deny.json does not deny ssm:GetParameter (SSM Agent safe)" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Profile Tests" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +printf " \033[0;32mPassed:\033[0m %d\n" "${PASS}" +printf " \033[0;31mFailed:\033[0m %d\n" "${FAIL}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +if [[ "${FAIL}" -gt 0 ]]; then + printf "\033[0;31m✗ %d test(s) failed\033[0m\n\n" "${FAIL}" + exit 1 +else + printf "\033[0;32m✓ All profile tests passed\033[0m\n\n" + exit 0 +fi From 7863060e35f75412e072e5329945601a564f7d6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 10:47:50 +0000 Subject: [PATCH 087/172] ci: bump version to 0.5.38 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 5dbd8e1..7590982 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.37" +INSTALLER_VERSION="0.5.38" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From b1a9c4765e54ff36500c5b41879a887ffb086309 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 10:52:42 +0000 Subject: [PATCH 088/172] fix: use timestamp suffix in default env name to prevent stack collisions Replace sequential counter (pack-1, pack-2) with last 3 digits of unix timestamp (pack-947, pack-948). Prevents parallel deploys from colliding on the same stack name. User can still override via the env name prompt. --- install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 7590982..0e7f79f 100755 --- a/install.sh +++ b/install.sh @@ -708,7 +708,8 @@ collect_config() { --filters "Name=tag:loki:managed,Values=true" \ --region "$DEPLOY_REGION" \ --query 'length(Vpcs)' --output text 2>/dev/null || echo "0") - local default_env_name="${PACK_NAME}-$((existing_count + 1))" + local ts_suffix; ts_suffix=$(date +%s | tail -c 4) + local default_env_name="${PACK_NAME}-${ts_suffix}" echo "" prompt "Environment name (lowercase, resource prefix)" ENV_NAME "$default_env_name" From 2f6e089b36fa568a26dadccb64e7ae9798727e27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 10:52:53 +0000 Subject: [PATCH 089/172] ci: bump version to 0.5.39 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 0e7f79f..17ed7df 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.38" +INSTALLER_VERSION="0.5.39" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 4ef0bf8b23ad23684646bc400baf5d3e454f0ceb Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 10:58:28 +0000 Subject: [PATCH 090/172] fix: env name includes count + timestamp suffix (e.g. openclaw-3-847) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 17ed7df..428a4a2 100755 --- a/install.sh +++ b/install.sh @@ -709,7 +709,7 @@ collect_config() { --region "$DEPLOY_REGION" \ --query 'length(Vpcs)' --output text 2>/dev/null || echo "0") local ts_suffix; ts_suffix=$(date +%s | tail -c 4) - local default_env_name="${PACK_NAME}-${ts_suffix}" + local default_env_name="${PACK_NAME}-$((existing_count + 1))-${ts_suffix}" echo "" prompt "Environment name (lowercase, resource prefix)" ENV_NAME "$default_env_name" From a46eae949cd7c197f40ed22636d9d9190b92d530 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 10:58:39 +0000 Subject: [PATCH 091/172] ci: bump version to 0.5.40 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 428a4a2..d81573d 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.39" +INSTALLER_VERSION="0.5.40" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 49751cc65770c341a7afa15be3370da4dc815725 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 11:26:36 +0000 Subject: [PATCH 092/172] docs: simplify TL;DR, add profiles to Getting Started - TL;DR: clean curl|bash one-liner + 3 profile examples - Getting Started: CLI flags table, profile table with risk emojis - Removed verbose warnings from TL;DR (kept one-liner warning) - Removed pipe-to-file pattern in favor of curl|bash --- README.md | 66 ++++++++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 1e450bb..0dc4ab1 100644 --- a/README.md +++ b/README.md @@ -7,48 +7,29 @@ > **TL;DR — deploy Loki:** > -> **Interactive (walks you through everything):** > ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh +> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh | bash > ``` > -> **Non-interactive — OpenClaw (recommended, full AI agent with 24/7 gateway):** -> ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive --pack openclaw -> ``` -> -> **Non-interactive — Claude Code (Anthropic's coding agent):** -> ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh --non-interactive --pack claude-code --method cfn -> ``` +> The installer walks you through pack, profile, and deploy method interactively. > -> **More examples:** +> **One-liner examples (non-interactive):** > ```sh -> # Pick a deploy method: cfn (CloudFormation) or terraform -> bash /tmp/loki-install.sh --non-interactive --pack openclaw --method terraform -> bash /tmp/loki-install.sh --non-interactive --pack hermes --method cfn -> ``` -> -> Requires: AWS CLI configured, admin access on a dedicated AWS account. Without `--non-interactive`, the script walks you through everything interactively. -> -> **CLI flags:** -> | Flag | Description | -> |------|-------------| -> | `--non-interactive` | Accept all defaults, skip prompts (aliases: `--yes`, `-y`) | -> | `--pack ` | Pre-select agent pack (`openclaw`, `claude-code`, `hermes`, `pi`, `ironclaw`) | -> | `--method ` | Pre-select deploy method (`cfn`, `terraform` / `tf`) | +> # Full builder agent (can create/modify/delete AWS resources) +> curl -sfL .../install.sh | bash -s -- --non-interactive --pack openclaw --profile builder > -> ⚠️ **We highly recommend deploying Loki in a brand-new, dedicated AWS account.** Loki has admin-level access and LLMs can make mistakes — a clean account limits the blast radius. Start with prototyping work as you learn and get acquainted with its capabilities. Like any powerful tool, it carries risks; isolating it in its own account is the simplest way to manage them. +> # Read-only advisor (can see everything, change nothing) +> curl -sfL .../install.sh | bash -s -- --non-interactive --pack openclaw --profile account_assistant > -> ⚠️ **This is an experiment, not a security product.** Loki can enable AWS security services and flag findings, but it does not replace professional security review, compliance auditing, or threat modeling. An LLM with admin access can cause damage — treat it accordingly. +> # Personal assistant (Bedrock only, no AWS access) +> curl -sfL .../install.sh | bash -s -- --non-interactive --pack claude-code --profile personal_assistant +> ``` > -> After deploying, run the [Essential Bootstraps](https://github.com/inceptionstack/loki-agent/tree/main/bootstraps/essential) — see [Getting Started](#getting-started) below. +> Requires: AWS CLI + admin access on a **dedicated sandbox account**. > -> **To remove a Loki deployment:** -> ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/uninstall.sh -o /tmp/loki-uninstall.sh && bash /tmp/loki-uninstall.sh -> ``` +> ⚠️ Deploy in a clean account — LLMs make mistakes, a sandbox limits the blast radius. > +> **Uninstall:** `curl -sfL .../uninstall.sh | bash` --- @@ -56,11 +37,26 @@ ### Step 1: Install Loki -Run the install command from the TL;DR above. The installer verifies AWS permissions, lets you select your **agent pack**, instance size, and deployment method (CloudFormation / SAM / Terraform), then deploys automatically. +Run the install command from the TL;DR above. The installer walks you through **pack**, **profile**, **instance size**, and **deploy method** (CloudFormation or Terraform). + +**CLI flags for non-interactive deploys:** + +| Flag | Description | +|------|-------------| +| `--non-interactive` | Skip all prompts, use defaults (aliases: `--yes`, `-y`) | +| `--pack ` | Agent pack: `openclaw`, `claude-code`, `hermes`, `pi`, `ironclaw` | +| `--profile ` | Permission profile: `builder`, `account_assistant`, `personal_assistant` | +| `--method ` | Deploy method: `cfn` (CloudFormation), `terraform` / `tf` | + +**Permission profiles:** -Use `--non-interactive` (or `--yes` / `-y`) to skip all prompts and deploy with defaults: Terraform, OpenClaw pack, t4g.xlarge, all security services enabled. Add `--pack ` to pre-select a pack and `--method ` to pre-select the deploy method. +| Profile | IAM | Instance | Use case | +|---------|-----|----------|----------| +| 🔴 `builder` | AdministratorAccess | t4g.xlarge | Build apps, deploy infra, manage pipelines | +| 🟡 `account_assistant` | ReadOnlyAccess + Bedrock | t4g.medium | Cost analysis, architecture review, debugging | +| 🟢 `personal_assistant` | Bedrock only | t4g.medium | Writing, research, coding help, daily tasks | -**Agent packs available:** +**Agent packs:** | Pack | Description | Instance | Data Volume | |------|-------------|----------|-------------| | **OpenClaw** (default) | Stateful AI agent with 24/7 gateway, persistent memory, Telegram/Discord/Slack | t4g.xlarge recommended | 80GB | From e5f5f7db8d5de748d200fb0f55bcca0141596535 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 11:32:20 +0000 Subject: [PATCH 093/172] fix: add claude-code to registry.yaml, shell-profile, update test counts - claude-code was in registry.json but missing from registry.yaml - Added resources/shell-profile.sh for claude-code pack - Updated test-registry-parser to expect 5 agent packs (was 4) --- packs/claude-code/resources/shell-profile.sh | 14 ++++++++++++++ packs/registry.yaml | 13 +++++++++++++ tests/test-registry-parser.sh | 5 +++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 packs/claude-code/resources/shell-profile.sh diff --git a/packs/claude-code/resources/shell-profile.sh b/packs/claude-code/resources/shell-profile.sh new file mode 100755 index 0000000..8a4b783 --- /dev/null +++ b/packs/claude-code/resources/shell-profile.sh @@ -0,0 +1,14 @@ +# Claude Code shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +# This file defines aliases and the welcome banner for the Claude Code pack. + +PACK_ALIASES=' +alias cc="claude" +' + +PACK_BANNER_NAME="Claude Code Agent Environment" +PACK_BANNER_EMOJI="🤖" +PACK_BANNER_COMMANDS=' + claude → Launch Claude Code + claude --help → Show Claude Code options + claude --version → Show installed version +' diff --git a/packs/registry.yaml b/packs/registry.yaml index 276a954..b2f6139 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -15,6 +15,19 @@ packs: description: "OpenAI-compatible Bedrock proxy (systemd daemon on localhost)" order: 0 + claude-code: + type: agent + description: "Claude Code — Anthropic's coding agent with native Bedrock support" + deps: [] + instance_type: t4g.large + root_volume_gb: 40 + data_volume_gb: 0 + default_model: "us.anthropic.claude-sonnet-4-6" + ports: {} + brain: false + claude_code: false + experimental: false + openclaw: type: agent description: "OpenClaw — stateful AI agent with persistent gateway" diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh index 8a5d730..f7150c4 100644 --- a/tests/test-registry-parser.sh +++ b/tests/test-registry-parser.sh @@ -63,10 +63,11 @@ get_value() { } # ---- Test: real registry.json ----------------------------------------------- -echo "=== Test: real registry.json (4 agent packs) ===" +echo "=== Test: real registry.json (5 agent packs) ===" output=$(list_agents "$REGISTRY") -assert_count "lists exactly 4 agents" 4 "$output" +assert_count "lists exactly 5 agents" 5 "$output" assert_contains "includes openclaw" "openclaw|" "$output" +assert_contains "includes claude-code" "claude-code|" "$output" assert_contains "includes hermes" "hermes|" "$output" assert_contains "includes pi" "pi|" "$output" assert_contains "includes ironclaw" "ironclaw|" "$output" From 73e2b2d2969b73b2e4c986df8b0b3c14ebca9100 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 11:32:29 +0000 Subject: [PATCH 094/172] ci: bump version to 0.5.41 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index d81573d..69b5dc6 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.40" +INSTALLER_VERSION="0.5.41" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 829285008b7432450f43e4bfa287126235af6b6d Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 12:19:03 +0000 Subject: [PATCH 095/172] =?UTF-8?q?feat:=20add=20nemoclaw=20pack=20?= =?UTF-8?q?=E2=80=94=20sandboxed=20OpenClaw=20via=20OpenShell=20with=20Bed?= =?UTF-8?q?rock=20inference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NemoClaw pack: OpenClaw in NVIDIA OpenShell sandbox (Landlock + seccomp + netns) - Bedrock-native via bedrockify (no NVIDIA API key or GPU required) - personal_assistant profile only (sandbox blocks AWS API access) - Instance: t4g.xlarge minimum (NemoClaw needs 4 vCPU / 8GB+ RAM) - Docker + cgroup v2 config, upstream curl installer (not npm install -g) - Host-side inference routing: agent → inference.local → OpenShell GW → bedrockify - Brain file injection via docker cp - Network policy: Telegram + npm + GitHub egress only - Experimental: true (NemoClaw is alpha) Files: packs/nemoclaw/manifest.yaml packs/nemoclaw/install.sh packs/nemoclaw/resources/network-policy.yaml packs/nemoclaw/resources/shell-profile.sh packs/registry.yaml (nemoclaw entry added) packs/registry.json (nemoclaw entry added) --- packs/nemoclaw/install.sh | 287 +++++++++++++++++++ packs/nemoclaw/manifest.yaml | 53 ++++ packs/nemoclaw/resources/network-policy.yaml | 37 +++ packs/nemoclaw/resources/shell-profile.sh | 25 ++ packs/registry.json | 15 + packs/registry.yaml | 17 ++ 6 files changed, 434 insertions(+) create mode 100755 packs/nemoclaw/install.sh create mode 100644 packs/nemoclaw/manifest.yaml create mode 100644 packs/nemoclaw/resources/network-policy.yaml create mode 100644 packs/nemoclaw/resources/shell-profile.sh diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh new file mode 100755 index 0000000..e875d7d --- /dev/null +++ b/packs/nemoclaw/install.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +# packs/nemoclaw/install.sh — Install NemoClaw (OpenClaw in sandboxed OpenShell via bedrockify) +# +# Usage: +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6] [--bedrockify-port 8090] +# [--sandbox-name loki-assistant] [--telegram-token TOKEN] [--allowed-chat-ids IDS] +# +# Assumes: +# - bedrockify is already installed and running (see packs/bedrockify/) +# - Docker available (installed by Step 1 if absent) +# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) +# - Profile: personal_assistant ONLY (sandbox blocks AWS API access) +# +# Idempotent: safe to re-run. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" +PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6")" +PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" +PACK_ARG_SANDBOX_NAME="$(pack_config_get sandbox_name "loki-assistant")" +PACK_ARG_TELEGRAM_TOKEN="$(pack_config_get telegram_token "")" +PACK_ARG_ALLOWED_CHAT_IDS="$(pack_config_get allowed_chat_ids "")" +PACK_ARG_PROFILE="$(pack_config_get profile "")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat </dev/null; then + log "Docker is already running — skipping install" +else + log "Installing Docker..." + if command -v dnf &>/dev/null; then + dnf install -y docker + elif command -v apt-get &>/dev/null; then + apt-get install -y docker.io + else + fail "Unsupported package manager — cannot install Docker" + fi +fi + +# Configure cgroup v2 (required for NemoClaw preflight on AL2023) +DAEMON_JSON="/etc/docker/daemon.json" +DOCKER_NEEDS_RESTART=false +if [[ ! -f "${DAEMON_JSON}" ]] || ! grep -q '"default-cgroupns-mode"' "${DAEMON_JSON}" 2>/dev/null; then + log "Configuring Docker cgroup v2 mode..." + mkdir -p /etc/docker + if [[ -f "${DAEMON_JSON}" ]] && [[ -s "${DAEMON_JSON}" ]] && command -v jq &>/dev/null; then + # Merge into existing config to avoid clobbering other settings + jq '. + {"default-cgroupns-mode": "host"}' "${DAEMON_JSON}" > "${DAEMON_JSON}.tmp" \ + && mv "${DAEMON_JSON}.tmp" "${DAEMON_JSON}" + else + echo '{"default-cgroupns-mode": "host"}' > "${DAEMON_JSON}" + fi + DOCKER_NEEDS_RESTART=true + ok "Docker daemon.json updated with cgroup v2 config" +else + log "Docker cgroup v2 already configured" +fi + +# Enable and start Docker +if ! systemctl is-enabled --quiet docker 2>/dev/null; then + systemctl enable docker +fi +if ! systemctl is-active --quiet docker 2>/dev/null; then + systemctl start docker +elif [[ "${DOCKER_NEEDS_RESTART}" == "true" ]]; then + log "Restarting Docker to apply cgroup v2 config..." + systemctl restart docker +fi + +# Add ec2-user to docker group (idempotent) +if id ec2-user &>/dev/null; then + usermod -aG docker ec2-user 2>/dev/null || true +fi + +# Verify Docker is functional +if ! docker info &>/dev/null; then + fail "Docker is installed but not functional. Check systemctl status docker." +fi +ok "Docker is running and functional" + +# ── Step 2: Verify bedrockify health ───────────────────────────────────────── +step "Verifying bedrockify health" + +check_bedrockify_health "${BEDROCKIFY_PORT}" + +# ── Step 3: Install NemoClaw + OpenShell ───────────────────────────────────── +step "Installing NemoClaw + OpenShell" + +if command -v nemoclaw &>/dev/null; then + NEMOCLAW_EXISTING="$(nemoclaw --version 2>/dev/null || echo unknown)" + log "nemoclaw already installed (${NEMOCLAW_EXISTING}) — reinstalling" +fi + +# NOTE: npm install -g is broken (upstream bug GH-503) +# Use the upstream curl installer which clones repo + npm links +export NEMOCLAW_NON_INTERACTIVE=1 +export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 +curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash + +# Refresh PATH for current session +export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}" + +if ! command -v nemoclaw &>/dev/null; then + fail "nemoclaw command not found after install. Check PATH or install output." +fi + +NEMOCLAW_VERSION="$(nemoclaw --version 2>/dev/null || echo unknown)" +ok "NemoClaw installed: ${NEMOCLAW_VERSION}" + +if ! command -v openshell &>/dev/null; then + warn "openshell command not found — may be installed in a non-standard location" +else + OPENSHELL_VERSION="$(openshell --version 2>/dev/null || echo unknown)" + ok "OpenShell installed: ${OPENSHELL_VERSION}" +fi + +# ── Step 4: Create sandbox (non-interactive onboard) ───────────────────────── +step "Creating NemoClaw sandbox (non-interactive)" + +export NEMOCLAW_SANDBOX_NAME="${SANDBOX_NAME}" +export NEMOCLAW_PROVIDER=custom +export COMPATIBLE_API_KEY=unused # bedrockify uses IAM, not API keys +export NEMOCLAW_COMPATIBLE_BASE_URL="http://127.0.0.1:${BEDROCKIFY_PORT}/v1" +export NEMOCLAW_MODEL="${MODEL}" +export NEMOCLAW_POLICY_MODE=suggested + +# Apply network policy +NETWORK_POLICY="${SCRIPT_DIR}/resources/network-policy.yaml" +if [[ -f "${NETWORK_POLICY}" ]]; then + export NEMOCLAW_NETWORK_POLICY="${NETWORK_POLICY}" + log "Network policy: ${NETWORK_POLICY}" +fi + +nemoclaw onboard --non-interactive --yes-i-accept-third-party-software + +# Verify sandbox created +if ! nemoclaw "${SANDBOX_NAME}" status &>/dev/null; then + warn "nemoclaw status check inconclusive — sandbox may still be initializing" +fi +ok "Sandbox '${SANDBOX_NAME}' onboarded" + +# ── Step 5: Configure Telegram bridge ──────────────────────────────────────── +step "Configuring Telegram bridge" + +if [[ -n "${TELEGRAM_TOKEN}" ]]; then + log "Setting up Telegram bridge for sandbox '${SANDBOX_NAME}'..." + export TELEGRAM_BOT_TOKEN="${TELEGRAM_TOKEN}" + export ALLOWED_CHAT_IDS="${ALLOWED_CHAT_IDS}" + # NemoClaw's host-side bridge picks up these env vars automatically + ok "Telegram bridge environment configured (token set)" +else + log "No Telegram token provided — Telegram bridge skipped" +fi + +# ── Step 6: Inject brain files ──────────────────────────────────────────────── +step "Injecting brain files into sandbox" + +BRAIN_DIR="${HOME}/.openclaw/workspace" +BRAIN_FILES=(SOUL.md USER.md IDENTITY.md AGENTS.md) + +# Get running container ID for the sandbox (exact match first, fallback to substring) +CONTAINER_ID="$(docker ps --filter "name=^/${SANDBOX_NAME}$" --format '{{.ID}}' 2>/dev/null | head -1)" +if [[ -z "${CONTAINER_ID}" ]]; then + CONTAINER_ID="$(docker ps --filter "name=${SANDBOX_NAME}" -q 2>/dev/null | head -1)" +fi + +if [[ -z "${CONTAINER_ID}" ]]; then + warn "Sandbox container '${SANDBOX_NAME}' not found in running containers — skipping brain injection" + warn "Run brain injection manually after sandbox starts: docker cp SOUL.md :/sandbox/.openclaw/workspace/" +else + SANDBOX_WORKSPACE="/sandbox/.openclaw/workspace" + for brain_file in "${BRAIN_FILES[@]}"; do + src="${BRAIN_DIR}/${brain_file}" + if [[ -f "${src}" ]]; then + docker cp "${src}" "${CONTAINER_ID}:${SANDBOX_WORKSPACE}/" 2>/dev/null && \ + ok "Injected ${brain_file}" || \ + warn "Failed to inject ${brain_file} (container may not have ${SANDBOX_WORKSPACE})" + else + warn "Brain file not found, skipping: ${src}" + fi + done +fi + +# ── Step 7: Health check polling + done marker ──────────────────────────────── +step "Health check" + +log "Polling for sandbox '${SANDBOX_NAME}' to reach running state (timeout: 120s)..." + +if timeout 120 bash -c " + until nemoclaw '${SANDBOX_NAME}' status --json 2>/dev/null | \\ + python3 -c \"import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('sandbox',{}).get('status')=='running' else 1)\" >/dev/null 2>&1; do + sleep 5 + done +" 2>/dev/null; then + ok "Sandbox '${SANDBOX_NAME}' is running" +else + warn "Sandbox did not reach running state within 120s — check: nemoclaw ${SANDBOX_NAME} status" +fi + +write_done_marker "nemoclaw" + +# Install shell profile (aliases + banner) if /etc/profile.d exists +SHELL_PROFILE="${SCRIPT_DIR}/resources/shell-profile.sh" +if [[ -f "${SHELL_PROFILE}" && -d /etc/profile.d ]]; then + cp "${SHELL_PROFILE}" /etc/profile.d/nemoclaw.sh 2>/dev/null && \ + ok "Shell profile installed: /etc/profile.d/nemoclaw.sh" || \ + warn "Could not install shell profile (permission denied?)" +fi + +printf "\n[PACK:nemoclaw] INSTALLED — sandbox '%s' ready (model: %s via bedrockify:%s)\n" \ + "${SANDBOX_NAME}" "${MODEL}" "${BEDROCKIFY_PORT}" diff --git a/packs/nemoclaw/manifest.yaml b/packs/nemoclaw/manifest.yaml new file mode 100644 index 0000000..ee730c5 --- /dev/null +++ b/packs/nemoclaw/manifest.yaml @@ -0,0 +1,53 @@ +name: nemoclaw +version: "1.0.0" +type: agent +description: "NemoClaw — OpenClaw in sandboxed OpenShell with Bedrock inference (personal_assistant only)" + +deps: + - bedrockify + +requirements: + arch: + - arm64 + - amd64 + os: + - al2023 + - ubuntu2204 + min_instance_type: t4g.xlarge # NemoClaw minimum: 4 vCPU / 8GB RAM + +compatible_profiles: + - personal_assistant # ONLY profile — sandbox blocks AWS API access + +params: + - name: region + description: "AWS region for Bedrock" + default: us-east-1 + - name: model + description: "Bedrock model ID (via bedrockify)" + default: "us.anthropic.claude-sonnet-4-6" + - name: bedrockify-port + description: "Port where bedrockify is running" + default: "8090" + - name: sandbox-name + description: "NemoClaw sandbox name" + default: "loki-assistant" + - name: telegram-token + description: "Telegram bot token (optional, enables Telegram bridge)" + default: "" + - name: allowed-chat-ids + description: "Comma-separated Telegram chat IDs (optional)" + default: "" + +health_check: + # NemoClaw manages its own lifecycle — no systemd service + # Polling loop instead of systemctl is-active: + command: "nemoclaw loki-assistant status --json 2>/dev/null | jq -r .sandbox.status" + expect: "running" + timeout: 120 + poll_interval: 5 + +provides: + commands: + - nemoclaw + - openshell + services: [] # NemoClaw manages its own lifecycle (not systemd) diff --git a/packs/nemoclaw/resources/network-policy.yaml b/packs/nemoclaw/resources/network-policy.yaml new file mode 100644 index 0000000..adcab2d --- /dev/null +++ b/packs/nemoclaw/resources/network-policy.yaml @@ -0,0 +1,37 @@ +# NemoClaw baseline network policy +# Declarative egress rules for the sandboxed OpenClaw agent. +# +# NOTE: bedrockify endpoint is NOT listed here. +# Inference routing is entirely host-side: +# Agent → inference.local → OpenShell gateway (HOST) → localhost:8090 → bedrockify → Bedrock API +# The sandbox network never traverses the bedrockify/Bedrock boundary directly. + +network: + - name: telegram-api + endpoints: + - host: "api.telegram.org" + port: 443 + binaries: ["*"] + rules: + - methods: ["POST", "GET"] + paths: ["/*"] + + - name: npm-registry + endpoints: + - host: "registry.npmjs.org" + port: 443 + binaries: ["npm", "node"] + rules: + - methods: ["GET"] + paths: ["/*"] + + - name: github + endpoints: + - host: "github.com" + port: 443 + - host: "raw.githubusercontent.com" + port: 443 + binaries: ["git", "curl"] + rules: + - methods: ["GET"] + paths: ["/*"] diff --git a/packs/nemoclaw/resources/shell-profile.sh b/packs/nemoclaw/resources/shell-profile.sh new file mode 100644 index 0000000..c4d57c8 --- /dev/null +++ b/packs/nemoclaw/resources/shell-profile.sh @@ -0,0 +1,25 @@ +# NemoClaw shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +# This file defines aliases and the welcome banner for the NemoClaw pack. + +PACK_ALIASES=' +alias nemo="nemoclaw" +alias nemo-status="nemoclaw loki-assistant status --json | python3 -m json.tool" +alias nemo-logs="nemoclaw loki-assistant logs --follow" +alias nemo-restart="nemoclaw loki-assistant restart" +alias nemo-shell="nemoclaw loki-assistant shell" +alias nemo-stop="nemoclaw loki-assistant stop" +alias nemo-start="nemoclaw loki-assistant start" +alias nemo-brain="nemoclaw loki-assistant brain" +' + +PACK_BANNER_NAME="NemoClaw Sandboxed Agent Environment" +PACK_BANNER_EMOJI="🛡️" +PACK_BANNER_COMMANDS=' + nemoclaw loki-assistant status → Sandbox status + nemoclaw loki-assistant logs -f → Follow sandbox logs + nemoclaw loki-assistant shell → Open shell inside sandbox + nemoclaw loki-assistant restart → Restart sandbox + nemoclaw loki-assistant brain → Manage brain files + nemo-status → Status (JSON formatted) + nemo-logs → Follow logs (alias) +' diff --git a/packs/registry.json b/packs/registry.json index 63629d9..4152c6f 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -67,6 +67,21 @@ "claude_code": false, "experimental": true }, + "nemoclaw": { + "type": "agent", + "description": "NemoClaw -- OpenClaw in sandboxed OpenShell with Bedrock inference", + "deps": ["bedrockify"], + "instance_type": "t4g.xlarge", + "root_volume_gb": 40, + "data_volume_gb": 80, + "default_model": "us.anthropic.claude-sonnet-4-6", + "ports": {}, + "brain": true, + "claude_code": false, + "experimental": true, + "requires_docker": true, + "compatible_profiles": ["personal_assistant"] + }, "claude-code": { "type": "agent", "description": "Claude Code — Anthropic's coding agent with native Bedrock support", diff --git a/packs/registry.yaml b/packs/registry.yaml index b2f6139..535f95a 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -84,3 +84,20 @@ packs: brain: false claude_code: false experimental: true + + nemoclaw: + type: agent + description: "NemoClaw — OpenClaw in sandboxed OpenShell with Bedrock inference" + deps: + - bedrockify + instance_type: t4g.xlarge # NemoClaw requires 4 vCPU / 8GB+ RAM + root_volume_gb: 40 + data_volume_gb: 80 + default_model: "us.anthropic.claude-sonnet-4-6" + ports: {} + brain: true + claude_code: false + experimental: true + requires_docker: true + compatible_profiles: + - personal_assistant # Sandbox blocks AWS API access From 08e833be3685f09e29d4372515047670f3cd91d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 12:19:15 +0000 Subject: [PATCH 096/172] ci: bump version to 0.5.42 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 69b5dc6..eca7c26 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.41" +INSTALLER_VERSION="0.5.42" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 4cc70b0ae373fc35d8621d3519291170947bb5cb Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 12:20:44 +0000 Subject: [PATCH 097/172] fix: add nemoclaw to deploy templates + instance size override + profile guard - CFN AllowedValues: add nemoclaw to PackName - Terraform variables: add nemoclaw to pack_name validation - install.sh: pack minimum instance override (nemoclaw forces t4g.xlarge even when personal_assistant defaults to t4g.medium) - install.sh: early profile+pack compatibility check (nemoclaw + non-personal_assistant = fail) --- deploy/cloudformation/template.yaml | 1 + deploy/terraform/variables.tf | 6 ++--- install.sh | 35 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index c58a197..3a5e883 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -120,6 +120,7 @@ Parameters: - hermes - pi - ironclaw + - nemoclaw Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." ProfileName: diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 5dec783..4dbf755 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -15,12 +15,12 @@ variable "profile_name" { } variable "pack_name" { - description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, or ironclaw)" + description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, ironclaw, or nemoclaw)" type = string default = "openclaw" validation { - condition = contains(["openclaw", "claude-code", "hermes", "pi", "ironclaw"], var.pack_name) - error_message = "pack_name must be openclaw, claude-code, hermes, pi, or ironclaw." + condition = contains(["openclaw", "claude-code", "hermes", "pi", "ironclaw", "nemoclaw"], var.pack_name) + error_message = "pack_name must be openclaw, claude-code, hermes, pi, ironclaw, or nemoclaw." } } diff --git a/install.sh b/install.sh index eca7c26..60023cd 100755 --- a/install.sh +++ b/install.sh @@ -699,6 +699,21 @@ collect_config() { # ---- Profile selection (REQUIRED) ---------------------------------------- choose_profile + # ---- Profile + Pack compatibility check ---------------------------------- + if [[ "$PACK_NAME" == "nemoclaw" && "${PROFILE_NAME:-}" != "personal_assistant" ]]; then + echo "" + echo -e " ${RED}✗ NemoClaw is only compatible with the personal_assistant profile.${NC}" + echo "" + echo " NemoClaw runs the agent in an isolated sandbox that blocks all AWS API" + echo " access. The ${PROFILE_NAME} profile requires AWS access to function." + echo "" + echo " Options:" + echo " • Use --pack openclaw with --profile ${PROFILE_NAME}" + echo " • Use --pack nemoclaw with --profile personal_assistant" + echo "" + fail "Incompatible pack/profile combination: ${PACK_NAME} + ${PROFILE_NAME}" + fi + prompt "AWS region" DEPLOY_REGION "$REGION" # Count existing deployments to generate a smart default env name @@ -734,6 +749,26 @@ collect_config() { esac ;; esac + + # Pack minimum override: if the pack requires a larger instance than the profile default, upgrade + if [ -n "$registry" ]; then + local pack_min_type + pack_min_type=$(jq -r --arg p "$PACK_NAME" '.packs[$p].instance_type // ""' "$registry" 2>/dev/null || echo "") + case "$pack_min_type" in + t4g.xlarge) + if [[ "$default_size_choice" == "1" || "$default_size_choice" == "2" ]]; then + default_size_choice="3" + info "${PACK_NAME} requires t4g.xlarge minimum — upgrading from profile default" + fi + ;; + t4g.large) + if [[ "$default_size_choice" == "1" ]]; then + default_size_choice="2" + info "${PACK_NAME} requires t4g.large minimum — upgrading from profile default" + fi + ;; + esac + fi echo "" echo " Instance sizes:" echo " 1) t4g.medium -- 2 vCPU, 4GB (~\$25/mo) light use" From 80c0d7022d31ddf5ee7d01cf486233293b2454a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 12:21:01 +0000 Subject: [PATCH 098/172] ci: bump version to 0.5.43 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 60023cd..6725fe1 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.42" +INSTALLER_VERSION="0.5.43" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 22a671ff9879ca4bd2a18b2ccd78ad82274ca4cf Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 12:26:34 +0000 Subject: [PATCH 099/172] fix: nemoclaw install.sh needs sudo for privileged operations Pack install scripts run as ec2-user (not root) via bootstrap.sh. Docker install, daemon.json, systemctl, /etc/profile.d all need sudo. Caught by live deploy test: 'Error: This command has to be run with superuser privileges' --- packs/nemoclaw/install.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index e875d7d..50dc1e6 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -105,9 +105,9 @@ if systemctl is-active --quiet docker 2>/dev/null; then else log "Installing Docker..." if command -v dnf &>/dev/null; then - dnf install -y docker + sudo dnf install -y docker elif command -v apt-get &>/dev/null; then - apt-get install -y docker.io + sudo apt-get install -y docker.io else fail "Unsupported package manager — cannot install Docker" fi @@ -118,13 +118,13 @@ DAEMON_JSON="/etc/docker/daemon.json" DOCKER_NEEDS_RESTART=false if [[ ! -f "${DAEMON_JSON}" ]] || ! grep -q '"default-cgroupns-mode"' "${DAEMON_JSON}" 2>/dev/null; then log "Configuring Docker cgroup v2 mode..." - mkdir -p /etc/docker + sudo mkdir -p /etc/docker if [[ -f "${DAEMON_JSON}" ]] && [[ -s "${DAEMON_JSON}" ]] && command -v jq &>/dev/null; then # Merge into existing config to avoid clobbering other settings - jq '. + {"default-cgroupns-mode": "host"}' "${DAEMON_JSON}" > "${DAEMON_JSON}.tmp" \ - && mv "${DAEMON_JSON}.tmp" "${DAEMON_JSON}" + jq '. + {"default-cgroupns-mode": "host"}' "${DAEMON_JSON}" | sudo tee "${DAEMON_JSON}.tmp" > /dev/null \ + && sudo mv "${DAEMON_JSON}.tmp" "${DAEMON_JSON}" else - echo '{"default-cgroupns-mode": "host"}' > "${DAEMON_JSON}" + echo '{"default-cgroupns-mode": "host"}' | sudo tee "${DAEMON_JSON}" > /dev/null fi DOCKER_NEEDS_RESTART=true ok "Docker daemon.json updated with cgroup v2 config" @@ -134,18 +134,18 @@ fi # Enable and start Docker if ! systemctl is-enabled --quiet docker 2>/dev/null; then - systemctl enable docker + sudo systemctl enable docker fi if ! systemctl is-active --quiet docker 2>/dev/null; then - systemctl start docker + sudo systemctl start docker elif [[ "${DOCKER_NEEDS_RESTART}" == "true" ]]; then log "Restarting Docker to apply cgroup v2 config..." - systemctl restart docker + sudo systemctl restart docker fi # Add ec2-user to docker group (idempotent) if id ec2-user &>/dev/null; then - usermod -aG docker ec2-user 2>/dev/null || true + sudo usermod -aG docker ec2-user 2>/dev/null || true fi # Verify Docker is functional @@ -278,7 +278,7 @@ write_done_marker "nemoclaw" # Install shell profile (aliases + banner) if /etc/profile.d exists SHELL_PROFILE="${SCRIPT_DIR}/resources/shell-profile.sh" if [[ -f "${SHELL_PROFILE}" && -d /etc/profile.d ]]; then - cp "${SHELL_PROFILE}" /etc/profile.d/nemoclaw.sh 2>/dev/null && \ + sudo cp "${SHELL_PROFILE}" /etc/profile.d/nemoclaw.sh 2>/dev/null && \ ok "Shell profile installed: /etc/profile.d/nemoclaw.sh" || \ warn "Could not install shell profile (permission denied?)" fi From 4ccefe9ba31207d97c725e6ed8b1ce58664e2d76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 12:26:48 +0000 Subject: [PATCH 100/172] ci: bump version to 0.5.44 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 6725fe1..49fcde1 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.43" +INSTALLER_VERSION="0.5.44" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 4b47428b909ca791067fe9fbf028c51eeec406af Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 12:35:28 +0000 Subject: [PATCH 101/172] fix: use sudo for all docker commands in nemoclaw install.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pack install scripts run as ec2-user. Docker group membership requires a new shell session to take effect. Use sudo for docker info, docker ps, and docker cp until the session is refreshed. Caught by live deploy: 'Docker is installed but not functional' after successful install — docker info fails without group or sudo. --- packs/nemoclaw/install.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index 50dc1e6..5feef8e 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -148,8 +148,8 @@ if id ec2-user &>/dev/null; then sudo usermod -aG docker ec2-user 2>/dev/null || true fi -# Verify Docker is functional -if ! docker info &>/dev/null; then +# Verify Docker is functional (use sudo — group membership requires new session) +if ! sudo docker info &>/dev/null; then fail "Docker is installed but not functional. Check systemctl status docker." fi ok "Docker is running and functional" @@ -235,9 +235,9 @@ BRAIN_DIR="${HOME}/.openclaw/workspace" BRAIN_FILES=(SOUL.md USER.md IDENTITY.md AGENTS.md) # Get running container ID for the sandbox (exact match first, fallback to substring) -CONTAINER_ID="$(docker ps --filter "name=^/${SANDBOX_NAME}$" --format '{{.ID}}' 2>/dev/null | head -1)" +CONTAINER_ID="$(sudo docker ps --filter "name=^/${SANDBOX_NAME}$" --format '{{.ID}}' 2>/dev/null | head -1)" if [[ -z "${CONTAINER_ID}" ]]; then - CONTAINER_ID="$(docker ps --filter "name=${SANDBOX_NAME}" -q 2>/dev/null | head -1)" + CONTAINER_ID="$(sudo docker ps --filter "name=${SANDBOX_NAME}" -q 2>/dev/null | head -1)" fi if [[ -z "${CONTAINER_ID}" ]]; then @@ -248,7 +248,7 @@ else for brain_file in "${BRAIN_FILES[@]}"; do src="${BRAIN_DIR}/${brain_file}" if [[ -f "${src}" ]]; then - docker cp "${src}" "${CONTAINER_ID}:${SANDBOX_WORKSPACE}/" 2>/dev/null && \ + sudo docker cp "${src}" "${CONTAINER_ID}:${SANDBOX_WORKSPACE}/" 2>/dev/null && \ ok "Injected ${brain_file}" || \ warn "Failed to inject ${brain_file} (container may not have ${SANDBOX_WORKSPACE})" else From 9648d8437dae007fd9d776dfb8f65ce1a95b9a50 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 12:35:37 +0000 Subject: [PATCH 102/172] ci: bump version to 0.5.45 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 49fcde1..78a008d 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.44" +INSTALLER_VERSION="0.5.45" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 6d86b801e4c36ef24f55ca15dcb705fd68ffd4f8 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 12:47:11 +0000 Subject: [PATCH 103/172] fix: use 'sg docker' for docker group access in nemoclaw install.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NemoClaw's onboard command checks Docker internally as the current user. Adding ec2-user to the docker group doesn't take effect until a new login session. Use 'sg docker -c' to activate the group for the current shell without requiring re-login. Previous fix (sudo) doesn't work because nemoclaw onboard also calls docker internally — we can't sudo inside nemoclaw's own Docker checks. Caught by live deploy: 'Docker is not running' even though Docker was installed and running — NemoClaw's preflight couldn't access the socket. --- packs/nemoclaw/install.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index 5feef8e..9aeb6c7 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -148,8 +148,8 @@ if id ec2-user &>/dev/null; then sudo usermod -aG docker ec2-user 2>/dev/null || true fi -# Verify Docker is functional (use sudo — group membership requires new session) -if ! sudo docker info &>/dev/null; then +# Verify Docker is functional (use sg docker — group membership requires new session) +if ! sg docker -c "docker info" &>/dev/null; then fail "Docker is installed but not functional. Check systemctl status docker." fi ok "Docker is running and functional" @@ -207,7 +207,9 @@ if [[ -f "${NETWORK_POLICY}" ]]; then log "Network policy: ${NETWORK_POLICY}" fi -nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +# CRITICAL: NemoClaw's onboard checks Docker as current user. +# The docker group was just added — needs 'sg docker' to activate without a new login session. +sg docker -c "nemoclaw onboard --non-interactive --yes-i-accept-third-party-software" # Verify sandbox created if ! nemoclaw "${SANDBOX_NAME}" status &>/dev/null; then @@ -235,9 +237,9 @@ BRAIN_DIR="${HOME}/.openclaw/workspace" BRAIN_FILES=(SOUL.md USER.md IDENTITY.md AGENTS.md) # Get running container ID for the sandbox (exact match first, fallback to substring) -CONTAINER_ID="$(sudo docker ps --filter "name=^/${SANDBOX_NAME}$" --format '{{.ID}}' 2>/dev/null | head -1)" +CONTAINER_ID="$(sg docker -c "docker ps --filter 'name=^/${SANDBOX_NAME}$' --format '{{.ID}}'" 2>/dev/null | head -1)" if [[ -z "${CONTAINER_ID}" ]]; then - CONTAINER_ID="$(sudo docker ps --filter "name=${SANDBOX_NAME}" -q 2>/dev/null | head -1)" + CONTAINER_ID="$(sg docker -c "docker ps --filter 'name=${SANDBOX_NAME}' -q" 2>/dev/null | head -1)" fi if [[ -z "${CONTAINER_ID}" ]]; then @@ -248,7 +250,7 @@ else for brain_file in "${BRAIN_FILES[@]}"; do src="${BRAIN_DIR}/${brain_file}" if [[ -f "${src}" ]]; then - sudo docker cp "${src}" "${CONTAINER_ID}:${SANDBOX_WORKSPACE}/" 2>/dev/null && \ + sg docker -c "docker cp '${src}' '${CONTAINER_ID}:${SANDBOX_WORKSPACE}/'" 2>/dev/null && \ ok "Injected ${brain_file}" || \ warn "Failed to inject ${brain_file} (container may not have ${SANDBOX_WORKSPACE})" else From a1e4ee0985a2ecb4521e1d899214509166bec4d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 12:47:21 +0000 Subject: [PATCH 104/172] ci: bump version to 0.5.46 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 78a008d..63cc6c2 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.45" +INSTALLER_VERSION="0.5.46" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 904802b04f85a6ece3fc0ff29b3035d8d98f420e Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:04:40 +0000 Subject: [PATCH 105/172] =?UTF-8?q?fix:=20docker=20group=20access=20?= =?UTF-8?q?=E2=80=94=20use=20sg=20docker=20subshell=20for=20nemoclaw=20onb?= =?UTF-8?q?oard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: bootstrap.sh runs packs via 'sudo -u ec2-user' which creates a session BEFORE the docker group was added. Neither 'sg docker -c cmd' on a single command nor sudo picks up the new group in the subprocess environment that NemoClaw's onboard uses internally. Fix: wrap the entire nemoclaw onboard call in 'sg docker -c' with a full subshell that re-exports all env vars and re-activates mise/PATH. This gives NemoClaw's internal Docker checks proper socket access. --- packs/nemoclaw/install.sh | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index 9aeb6c7..92b0bcb 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -148,8 +148,10 @@ if id ec2-user &>/dev/null; then sudo usermod -aG docker ec2-user 2>/dev/null || true fi -# Verify Docker is functional (use sg docker — group membership requires new session) -if ! sg docker -c "docker info" &>/dev/null; then +# Verify Docker is functional +# NOTE: bootstrap.sh runs packs via 'sudo -u ec2-user' which doesn't pick up +# newly added groups. Use 'sudo docker' for verification. +if ! sudo docker info &>/dev/null; then fail "Docker is installed but not functional. Check systemctl status docker." fi ok "Docker is running and functional" @@ -193,23 +195,30 @@ fi # ── Step 4: Create sandbox (non-interactive onboard) ───────────────────────── step "Creating NemoClaw sandbox (non-interactive)" -export NEMOCLAW_SANDBOX_NAME="${SANDBOX_NAME}" -export NEMOCLAW_PROVIDER=custom -export COMPATIBLE_API_KEY=unused # bedrockify uses IAM, not API keys -export NEMOCLAW_COMPATIBLE_BASE_URL="http://127.0.0.1:${BEDROCKIFY_PORT}/v1" -export NEMOCLAW_MODEL="${MODEL}" -export NEMOCLAW_POLICY_MODE=suggested - # Apply network policy NETWORK_POLICY="${SCRIPT_DIR}/resources/network-policy.yaml" if [[ -f "${NETWORK_POLICY}" ]]; then - export NEMOCLAW_NETWORK_POLICY="${NETWORK_POLICY}" log "Network policy: ${NETWORK_POLICY}" fi -# CRITICAL: NemoClaw's onboard checks Docker as current user. -# The docker group was just added — needs 'sg docker' to activate without a new login session. -sg docker -c "nemoclaw onboard --non-interactive --yes-i-accept-third-party-software" +# CRITICAL: NemoClaw's onboard checks Docker internally as the current user. +# bootstrap.sh runs packs via 'sudo -u ec2-user' which doesn't pick up newly +# added docker group. We need to re-exec with the docker group active. +# Use 'sg docker' to spawn a subshell with the group, then run onboard inside it. +sg docker -c " + export NEMOCLAW_NON_INTERACTIVE=1 + export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 + export NEMOCLAW_SANDBOX_NAME='${SANDBOX_NAME}' + export NEMOCLAW_PROVIDER=custom + export COMPATIBLE_API_KEY=unused + export NEMOCLAW_COMPATIBLE_BASE_URL='http://127.0.0.1:${BEDROCKIFY_PORT}/v1' + export NEMOCLAW_MODEL='${MODEL}' + export NEMOCLAW_POLICY_MODE=suggested + export NEMOCLAW_NETWORK_POLICY='${NETWORK_POLICY}' + export PATH='/home/ec2-user/.local/bin:/usr/local/bin:${PATH}' + eval \"\$(/home/ec2-user/.local/bin/mise activate bash 2>/dev/null)\" 2>/dev/null || true + nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +" # Verify sandbox created if ! nemoclaw "${SANDBOX_NAME}" status &>/dev/null; then @@ -237,7 +246,7 @@ BRAIN_DIR="${HOME}/.openclaw/workspace" BRAIN_FILES=(SOUL.md USER.md IDENTITY.md AGENTS.md) # Get running container ID for the sandbox (exact match first, fallback to substring) -CONTAINER_ID="$(sg docker -c "docker ps --filter 'name=^/${SANDBOX_NAME}$' --format '{{.ID}}'" 2>/dev/null | head -1)" +CONTAINER_ID="$(sg docker -c "docker ps --filter 'name=^/${SANDBOX_NAME}\$' --format '{{.ID}}'" 2>/dev/null | head -1)" if [[ -z "${CONTAINER_ID}" ]]; then CONTAINER_ID="$(sg docker -c "docker ps --filter 'name=${SANDBOX_NAME}' -q" 2>/dev/null | head -1)" fi From c60be1437421711c7e0778f199aafd11671e8811 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:04:48 +0000 Subject: [PATCH 106/172] ci: bump version to 0.5.47 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 63cc6c2..9f317cc 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.46" +INSTALLER_VERSION="0.5.47" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From c6f5d9b29fffda0834bd25507a587a8fe7c9ed7f Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:10:47 +0000 Subject: [PATCH 107/172] fix: add shasum wrapper for AL2023 (NemoClaw/OpenShell needs shasum) AL2023 ships sha256sum (from coreutils) but not shasum (perl-based). NemoClaw's install-openshell.sh calls 'shasum -a 256' for checksum verification. Create a thin wrapper at /usr/local/bin/shasum that delegates to sha256sum. Caught by live deploy: 'shasum: command not found' during OpenShell install --- packs/nemoclaw/install.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index 92b0bcb..09788f6 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -185,6 +185,24 @@ fi NEMOCLAW_VERSION="$(nemoclaw --version 2>/dev/null || echo unknown)" ok "NemoClaw installed: ${NEMOCLAW_VERSION}" +# AL2023 has sha256sum but not shasum — NemoClaw's OpenShell installer needs shasum +if ! command -v shasum &>/dev/null && command -v sha256sum &>/dev/null; then + log "Creating shasum wrapper for AL2023 compatibility (NemoClaw/OpenShell needs shasum)..." + cat > /tmp/shasum-wrapper.sh << 'SHAWRAP' +#!/usr/bin/env bash +# shasum compatibility wrapper for AL2023 (sha256sum → shasum -a 256) +if [[ "$1" == "-a" && "$2" == "256" ]]; then + shift 2 + sha256sum "$@" +else + sha256sum "$@" +fi +SHAWRAP + chmod +x /tmp/shasum-wrapper.sh + sudo cp /tmp/shasum-wrapper.sh /usr/local/bin/shasum + ok "shasum wrapper installed at /usr/local/bin/shasum" +fi + if ! command -v openshell &>/dev/null; then warn "openshell command not found — may be installed in a non-standard location" else From a39c6396f07bf8521ad1aa15833edde1694c41ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:10:59 +0000 Subject: [PATCH 108/172] ci: bump version to 0.5.48 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9f317cc..13fbca3 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.47" +INSTALLER_VERSION="0.5.48" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 30e57d6db0be60288e563412537d7567fc26e99c Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:28:34 +0000 Subject: [PATCH 109/172] fix: correct env var for NemoClaw custom endpoint URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEMOCLAW_COMPATIBLE_BASE_URL → NEMOCLAW_ENDPOINT_URL (confirmed from NemoClaw source: bin/lib/onboard.js line for custom provider) Live test on AL2023/ARM64 t4g.xlarge confirmed working: - Sandbox 'loki-assistant' created with Landlock + seccomp + netns - Model: us.anthropic.claude-sonnet-4-6 via bedrockify (custom endpoint) - Docker image built (37 steps), 1355 MiB pushed to OpenShell gateway - Network policies applied (pypi, npm presets) --- packs/nemoclaw/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index 09788f6..ecf6e4d 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -229,7 +229,7 @@ sg docker -c " export NEMOCLAW_SANDBOX_NAME='${SANDBOX_NAME}' export NEMOCLAW_PROVIDER=custom export COMPATIBLE_API_KEY=unused - export NEMOCLAW_COMPATIBLE_BASE_URL='http://127.0.0.1:${BEDROCKIFY_PORT}/v1' + export NEMOCLAW_ENDPOINT_URL='http://127.0.0.1:${BEDROCKIFY_PORT}/v1' export NEMOCLAW_MODEL='${MODEL}' export NEMOCLAW_POLICY_MODE=suggested export NEMOCLAW_NETWORK_POLICY='${NETWORK_POLICY}' From b95f98edd98d5a69de4d52712aad73cce6d7b60c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:28:45 +0000 Subject: [PATCH 110/172] ci: bump version to 0.5.49 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 13fbca3..eb2de26 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.48" +INSTALLER_VERSION="0.5.49" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 76412fc2f0f06e031c3fd5352bd4b79face12e2f Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:43:05 +0000 Subject: [PATCH 111/172] fix: move shasum wrapper install BEFORE nemoclaw onboard The nemoclaw onboard auto-installs OpenShell which calls shasum for checksum verification. The shasum wrapper must exist before onboard runs, not after the NemoClaw CLI install. Both CFN and Terraform deploys now confirmed working: - Sandbox 'loki-assistant' with Landlock + seccomp + netns - Model: us.anthropic.claude-sonnet-4-6 via bedrockify custom endpoint - Docker image 37 steps, 1355 MiB pushed to OpenShell gateway - Network policies: pypi, npm presets applied --- packs/nemoclaw/install.sh | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index ecf6e4d..e5f0f46 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -164,6 +164,25 @@ check_bedrockify_health "${BEDROCKIFY_PORT}" # ── Step 3: Install NemoClaw + OpenShell ───────────────────────────────────── step "Installing NemoClaw + OpenShell" +# AL2023 has sha256sum but not shasum — NemoClaw's OpenShell installer needs shasum +# Must be installed BEFORE nemoclaw onboard (which auto-installs OpenShell) +if ! command -v shasum &>/dev/null && command -v sha256sum &>/dev/null; then + log "Creating shasum wrapper for AL2023 compatibility (NemoClaw/OpenShell needs shasum)..." + cat > /tmp/shasum-wrapper.sh << 'SHAWRAP' +#!/usr/bin/env bash +# shasum compatibility wrapper for AL2023 (sha256sum → shasum -a 256) +if [[ "$1" == "-a" && "$2" == "256" ]]; then + shift 2 + sha256sum "$@" +else + sha256sum "$@" +fi +SHAWRAP + chmod +x /tmp/shasum-wrapper.sh + sudo cp /tmp/shasum-wrapper.sh /usr/local/bin/shasum + ok "shasum wrapper installed at /usr/local/bin/shasum" +fi + if command -v nemoclaw &>/dev/null; then NEMOCLAW_EXISTING="$(nemoclaw --version 2>/dev/null || echo unknown)" log "nemoclaw already installed (${NEMOCLAW_EXISTING}) — reinstalling" @@ -185,24 +204,6 @@ fi NEMOCLAW_VERSION="$(nemoclaw --version 2>/dev/null || echo unknown)" ok "NemoClaw installed: ${NEMOCLAW_VERSION}" -# AL2023 has sha256sum but not shasum — NemoClaw's OpenShell installer needs shasum -if ! command -v shasum &>/dev/null && command -v sha256sum &>/dev/null; then - log "Creating shasum wrapper for AL2023 compatibility (NemoClaw/OpenShell needs shasum)..." - cat > /tmp/shasum-wrapper.sh << 'SHAWRAP' -#!/usr/bin/env bash -# shasum compatibility wrapper for AL2023 (sha256sum → shasum -a 256) -if [[ "$1" == "-a" && "$2" == "256" ]]; then - shift 2 - sha256sum "$@" -else - sha256sum "$@" -fi -SHAWRAP - chmod +x /tmp/shasum-wrapper.sh - sudo cp /tmp/shasum-wrapper.sh /usr/local/bin/shasum - ok "shasum wrapper installed at /usr/local/bin/shasum" -fi - if ! command -v openshell &>/dev/null; then warn "openshell command not found — may be installed in a non-standard location" else From 88efc8c7522f89a12346887d1dacc12671dd99bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:43:15 +0000 Subject: [PATCH 112/172] ci: bump version to 0.5.50 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index eb2de26..188468c 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.49" +INSTALLER_VERSION="0.5.50" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From beeac11a52f72cf4cae67698b0fc1f8acc20fb25 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:47:25 +0000 Subject: [PATCH 113/172] =?UTF-8?q?docs:=20add=20NemoClaw=20to=20README=20?= =?UTF-8?q?=E2=80=94=20pack=20table,=20one-liners,=20standalone=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added NemoClaw to agent packs table + pack system table - Added sandboxed personal_assistant one-liner example - Added NemoClaw standalone usage example - Updated --pack flag docs to include nemoclaw - Added NemoClaw to open source attribution line --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0dc4ab1..fecfb11 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ > > # Personal assistant (Bedrock only, no AWS access) > curl -sfL .../install.sh | bash -s -- --non-interactive --pack claude-code --profile personal_assistant +> +> # Sandboxed personal assistant (NemoClaw — isolated in OpenShell sandbox) +> curl -sfL .../install.sh | bash -s -- --non-interactive --pack nemoclaw --profile personal_assistant > ``` > > Requires: AWS CLI + admin access on a **dedicated sandbox account**. @@ -44,7 +47,7 @@ Run the install command from the TL;DR above. The installer walks you through ** | Flag | Description | |------|-------------| | `--non-interactive` | Skip all prompts, use defaults (aliases: `--yes`, `-y`) | -| `--pack ` | Agent pack: `openclaw`, `claude-code`, `hermes`, `pi`, `ironclaw` | +| `--pack ` | Agent pack: `openclaw`, `claude-code`, `hermes`, `nemoclaw`, `pi`, `ironclaw` | | `--profile ` | Permission profile: `builder`, `account_assistant`, `personal_assistant` | | `--method ` | Deploy method: `cfn` (CloudFormation), `terraform` / `tf` | @@ -64,6 +67,7 @@ Run the install command from the TL;DR above. The installer walks you through ** | **Hermes** *(experimental)* | NousResearch CLI agent — lighter, terminal-focused, self-improving skills | t4g.medium sufficient | None needed (set to 0) | | **Pi** *(experimental)* | Minimal terminal coding harness — read, write, edit, bash tools | t4g.medium sufficient | None needed (set to 0) | | **IronClaw** *(experimental)* | Rust-based AI agent by NEAR AI — static binary, fast startup | t4g.medium sufficient | None needed (set to 0) | +| **NemoClaw** *(experimental)* | OpenClaw in NVIDIA OpenShell sandbox — Landlock + seccomp + netns isolation, Bedrock via bedrockify. `personal_assistant` profile only. | t4g.xlarge required | 80GB | The installer discovers packs dynamically and asks which to deploy. Experimental packs are clearly marked. @@ -179,6 +183,7 @@ Loki uses a **pack-based architecture** for deploying different AI agent runtime | `hermes` | Agent *(experimental)* | NousResearch Hermes CLI agent. Self-improving skills, learning loop, lightweight. Uses bedrockify for model access. | | `pi` | Agent *(experimental)* | Pi Coding Agent. Minimal terminal coding harness with read, write, edit, bash tools. Pure Node.js. | | `ironclaw` | Agent *(experimental)* | IronClaw by NEAR AI. Rust-based agent with shell/file tools, MCP support. Single static binary. | +| `nemoclaw` | Agent *(experimental)* | NemoClaw — OpenClaw inside an [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) sandbox with Landlock, seccomp, and network namespace isolation. Inference routed through bedrockify on the host (no NVIDIA API key needed). **Only compatible with `personal_assistant` profile** — the sandbox blocks all AWS API access. Requires Docker + t4g.xlarge. | ### How It Works @@ -210,6 +215,9 @@ bash packs/hermes/install.sh --region us-east-1 --hermes-model anthropic/claude- # Or for OpenClaw bash packs/openclaw/install.sh --region us-east-1 --model us.anthropic.claude-opus-4-6-v1 --port 3001 + +# Or for NemoClaw (sandboxed OpenClaw — needs Docker, personal_assistant only) +bash packs/nemoclaw/install.sh --region us-east-1 --model us.anthropic.claude-sonnet-4-6 --profile personal_assistant ``` ### Adding New Packs @@ -460,7 +468,7 @@ Loki is: Loki is fully open source. The deployment templates, brain files, skills, and bootstrap scripts are all available at [github.com/inceptionstack/loki-agent](https://github.com/inceptionstack/loki-agent). -Built on [OpenClaw](https://github.com/openclaw/openclaw) and [Hermes](https://github.com/NousResearch/hermes-agent) — choose your agent runtime at deploy time. +Built on [OpenClaw](https://github.com/openclaw/openclaw), [Hermes](https://github.com/NousResearch/hermes-agent), and [NemoClaw](https://github.com/NVIDIA/NemoClaw) — choose your agent runtime at deploy time. ### InceptionStack Repositories From 935b7ab7cee8f692e42d5a4c96fd54b0ae720652 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:49:11 +0000 Subject: [PATCH 114/172] fix: update test-registry-parser to expect 6 agent packs (added nemoclaw) test-registry-parser.sh hardcoded expected pack count as 5. With nemoclaw added, it's now 6. Added nemoclaw assertion too. --- tests/test-registry-parser.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh index f7150c4..e8e25d6 100644 --- a/tests/test-registry-parser.sh +++ b/tests/test-registry-parser.sh @@ -63,14 +63,15 @@ get_value() { } # ---- Test: real registry.json ----------------------------------------------- -echo "=== Test: real registry.json (5 agent packs) ===" +echo "=== Test: real registry.json (6 agent packs) ===" output=$(list_agents "$REGISTRY") -assert_count "lists exactly 5 agents" 5 "$output" +assert_count "lists exactly 6 agents" 6 "$output" assert_contains "includes openclaw" "openclaw|" "$output" assert_contains "includes claude-code" "claude-code|" "$output" assert_contains "includes hermes" "hermes|" "$output" assert_contains "includes pi" "pi|" "$output" assert_contains "includes ironclaw" "ironclaw|" "$output" +assert_contains "includes nemoclaw" "nemoclaw|" "$output" bedrockify_as_pack=$(echo "$output" | grep -c '^bedrockify|' || true) assert_eq "excludes base packs (bedrockify)" "0" "$bedrockify_as_pack" From e4cdc6c68b7b875b80b30e9b11eb216436686b7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:49:21 +0000 Subject: [PATCH 115/172] ci: bump version to 0.5.51 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 188468c..ff93a73 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.50" +INSTALLER_VERSION="0.5.51" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 8e336a3136e7b4bfc2b519014a3487c645ab9c74 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 13:50:33 +0000 Subject: [PATCH 116/172] fix: test-registry-parser uses 'at least' count, not exact Exact pack count breaks every time a new pack is added. Now asserts >= 6 so future packs don't require test changes. --- tests/test-registry-parser.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh index e8e25d6..56f6e92 100644 --- a/tests/test-registry-parser.sh +++ b/tests/test-registry-parser.sh @@ -63,9 +63,14 @@ get_value() { } # ---- Test: real registry.json ----------------------------------------------- -echo "=== Test: real registry.json (6 agent packs) ===" +echo "=== Test: real registry.json (agent packs) ===" output=$(list_agents "$REGISTRY") -assert_count "lists exactly 6 agents" 6 "$output" +agent_count=$(echo "$output" | grep -c . || true) +if [[ "$agent_count" -ge 6 ]]; then + echo " ✓ lists at least 6 agents (found $agent_count)"; PASS=$((PASS + 1)) +else + echo " ✗ expected at least 6 agents, found $agent_count"; FAIL=$((FAIL + 1)) +fi assert_contains "includes openclaw" "openclaw|" "$output" assert_contains "includes claude-code" "claude-code|" "$output" assert_contains "includes hermes" "hermes|" "$output" From de48021a15f6239f17fb8e35cfd15db07a60ae20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 13:50:42 +0000 Subject: [PATCH 117/172] ci: bump version to 0.5.52 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index ff93a73..f01d5fa 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.51" +INSTALLER_VERSION="0.5.52" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From fa4c423203af87cd812d627a6c96065980775eeb Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 14:01:46 +0000 Subject: [PATCH 118/172] fix: address final review warnings W1-W3 W1: Telegram params (token, chat IDs) now logged when set, making it clear whether they came from config or defaults. W2: Telegram bridge config persisted to ~/.nemoclaw/telegram.env (chmod 600) instead of ephemeral env vars that vanish on exit. W3: manifest.yaml health_check uses ${NEMOCLAW_SANDBOX_NAME:-loki-assistant} instead of hardcoded 'loki-assistant', respecting custom sandbox names. --- packs/nemoclaw/install.sh | 20 ++++++++++++++++++-- packs/nemoclaw/manifest.yaml | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packs/nemoclaw/install.sh b/packs/nemoclaw/install.sh index e5f0f46..239c834 100755 --- a/packs/nemoclaw/install.sh +++ b/packs/nemoclaw/install.sh @@ -83,6 +83,9 @@ PROFILE="${PACK_ARG_PROFILE}" pack_banner "nemoclaw" log "region=${REGION} model=${MODEL} bedrockify-port=${BEDROCKIFY_PORT} sandbox-name=${SANDBOX_NAME}" +if [[ -n "${TELEGRAM_TOKEN}" ]]; then + log "telegram-token= allowed-chat-ids=${ALLOWED_CHAT_IDS:-}" +fi # ── Prerequisites ───────────────────────────────────────────────────────────── require_cmd curl python3 @@ -250,10 +253,23 @@ step "Configuring Telegram bridge" if [[ -n "${TELEGRAM_TOKEN}" ]]; then log "Setting up Telegram bridge for sandbox '${SANDBOX_NAME}'..." + + # Persist Telegram config to ~/.nemoclaw/telegram.env so the bridge can read it + # across reboots and service restarts (env vars don't survive script exit) + TELEGRAM_ENV="${HOME}/.nemoclaw/telegram.env" + mkdir -p "${HOME}/.nemoclaw" + cat > "${TELEGRAM_ENV}" << TGEOF +# NemoClaw Telegram bridge config (written by nemoclaw pack installer) +TELEGRAM_BOT_TOKEN=${TELEGRAM_TOKEN} +ALLOWED_CHAT_IDS=${ALLOWED_CHAT_IDS} +TGEOF + chmod 600 "${TELEGRAM_ENV}" + ok "Telegram config persisted to ${TELEGRAM_ENV}" + + # Also export for current session in case bridge starts now export TELEGRAM_BOT_TOKEN="${TELEGRAM_TOKEN}" export ALLOWED_CHAT_IDS="${ALLOWED_CHAT_IDS}" - # NemoClaw's host-side bridge picks up these env vars automatically - ok "Telegram bridge environment configured (token set)" + ok "Telegram bridge configured (token set, config persisted)" else log "No Telegram token provided — Telegram bridge skipped" fi diff --git a/packs/nemoclaw/manifest.yaml b/packs/nemoclaw/manifest.yaml index ee730c5..879f66a 100644 --- a/packs/nemoclaw/manifest.yaml +++ b/packs/nemoclaw/manifest.yaml @@ -40,8 +40,8 @@ params: health_check: # NemoClaw manages its own lifecycle — no systemd service - # Polling loop instead of systemctl is-active: - command: "nemoclaw loki-assistant status --json 2>/dev/null | jq -r .sandbox.status" + # Uses default sandbox name; override sandbox-name param to change + command: "nemoclaw ${NEMOCLAW_SANDBOX_NAME:-loki-assistant} status --json 2>/dev/null | jq -r .sandbox.status" expect: "running" timeout: 120 poll_interval: 5 From abfea0f0b3b48ccc5e7f014d84d999bcbc722b6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 14:01:57 +0000 Subject: [PATCH 119/172] ci: bump version to 0.5.53 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index f01d5fa..689a908 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.52" +INSTALLER_VERSION="0.5.53" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From d50654d9cd4000a2b033babe26fb7990037bf2a4 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 16:25:50 +0000 Subject: [PATCH 120/172] feat: add git pre-commit hook to run all unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/pre-commit — discovers and runs all test scripts (same as CI) - scripts/setup-hooks.sh — installs hooks with one command - Blocks commit if any test fails; skip with --no-verify Setup: bash scripts/setup-hooks.sh --- scripts/pre-commit | 48 ++++++++++++++++++++++++++++++++++++++++++ scripts/setup-hooks.sh | 24 +++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 scripts/pre-commit create mode 100755 scripts/setup-hooks.sh diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..f92b52d --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Git pre-commit hook — runs all unit tests before allowing commit. +# Install: cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +# Or run: bash scripts/setup-hooks.sh + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +REPO_ROOT="$(git rev-parse --show-toplevel)" +FAILED=0 + +run_test() { + local test_script="$1" + local name + name="$(basename "$(dirname "$test_script")")/$(basename "$test_script")" + printf "${CYAN}▸${NC} Running %s..." "$name" + if bash "$test_script" > /dev/null 2>&1; then + printf " ${GREEN}✓${NC}\n" + else + printf " ${RED}✗${NC}\n" + FAILED=$((FAILED + 1)) + fi +} + +echo "" +printf "${BOLD}Pre-commit: running tests...${NC}\n" +echo "" + +# Discover and run all test scripts (same as CI) +while IFS= read -r test_script; do + [[ -f "$test_script" ]] && run_test "$test_script" +done < <(find "${REPO_ROOT}/packs" -maxdepth 2 -name "test.sh" -type f 2>/dev/null; \ + find "${REPO_ROOT}/tests" -name "test-*.sh" -type f 2>/dev/null; \ + find "${REPO_ROOT}/packs" -maxdepth 1 -name "test-packs.sh" -type f 2>/dev/null) + +echo "" +if [[ "$FAILED" -gt 0 ]]; then + printf "${RED}✗ %d test(s) failed — commit blocked.${NC}\n" "$FAILED" + printf " Fix failures and try again. Skip with: git commit --no-verify\n\n" + exit 1 +else + printf "${GREEN}✓ All tests passed.${NC}\n\n" +fi diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh new file mode 100755 index 0000000..94ad0e2 --- /dev/null +++ b/scripts/setup-hooks.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Install git hooks for local development. +# Usage: bash scripts/setup-hooks.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOOKS_DIR="${REPO_ROOT}/.git/hooks" + +if [[ ! -d "${REPO_ROOT}/.git" ]]; then + echo "Error: not a git repository. Run from the repo root." >&2 + exit 1 +fi + +mkdir -p "${HOOKS_DIR}" + +# Install pre-commit hook +cp "${REPO_ROOT}/scripts/pre-commit" "${HOOKS_DIR}/pre-commit" +chmod +x "${HOOKS_DIR}/pre-commit" + +echo "✓ Git hooks installed:" +echo " pre-commit → runs all unit tests before commit" +echo "" +echo " Skip with: git commit --no-verify" From 195dd1f12f2ce3cd300e4e1065a6e5d2164d2b40 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 16:26:20 +0000 Subject: [PATCH 121/172] ci: bump version to 0.5.54 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 689a908..4478081 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.53" +INSTALLER_VERSION="0.5.54" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 9e16c32d74e051588488b6a4adef966b3080cdb7 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 16:36:21 +0000 Subject: [PATCH 122/172] =?UTF-8?q?feat:=20add=20kiro-cli=20pack=20?= =?UTF-8?q?=E2=80=94=20AWS=20agentic=20IDE=20terminal=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kiro CLI: AWS's agentic IDE CLI (kiro.dev) with MCP server support - No bedrockify dep — Kiro uses its own cloud inference - Pre-installs AWS MCP servers (terraform, ecs, eks, core, docs) via uvx - Interactive login required after deploy: kiro-cli login --use-device-flow - Shell profile with login reminder on SSM connect - t4g.medium, arm64+amd64, experimental: true - Updated CFN AllowedValues + TF validation + registry + tests Files: packs/kiro-cli/manifest.yaml packs/kiro-cli/install.sh packs/kiro-cli/resources/shell-profile.sh packs/registry.yaml (kiro-cli entry) packs/registry.json (kiro-cli entry) deploy/cloudformation/template.yaml deploy/terraform/variables.tf tests/test-registry-parser.sh --- deploy/cloudformation/template.yaml | 3 +- deploy/terraform/variables.tf | 6 +- packs/kiro-cli/install.sh | 173 ++++++++++++++++++++++ packs/kiro-cli/manifest.yaml | 35 +++++ packs/kiro-cli/resources/shell-profile.sh | 27 ++++ packs/registry.json | 12 ++ packs/registry.yaml | 12 ++ tests/test-registry-parser.sh | 1 + 8 files changed, 265 insertions(+), 4 deletions(-) create mode 100755 packs/kiro-cli/install.sh create mode 100644 packs/kiro-cli/manifest.yaml create mode 100644 packs/kiro-cli/resources/shell-profile.sh diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 3a5e883..68c0585 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -121,7 +121,8 @@ Parameters: - pi - ironclaw - nemoclaw - Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi' and 'ironclaw' are experimental." + - kiro-cli + Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'hermes' is the NousResearch CLI agent (lighter). 'pi', 'ironclaw', and 'kiro-cli' are experimental." ProfileName: Type: String diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index 4dbf755..a8fa1d6 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -15,12 +15,12 @@ variable "profile_name" { } variable "pack_name" { - description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, ironclaw, or nemoclaw)" + description = "Agent pack to deploy (openclaw, claude-code, hermes, pi, ironclaw, nemoclaw, or kiro-cli)" type = string default = "openclaw" validation { - condition = contains(["openclaw", "claude-code", "hermes", "pi", "ironclaw", "nemoclaw"], var.pack_name) - error_message = "pack_name must be openclaw, claude-code, hermes, pi, ironclaw, or nemoclaw." + condition = contains(["openclaw", "claude-code", "hermes", "pi", "ironclaw", "nemoclaw", "kiro-cli"], var.pack_name) + error_message = "pack_name must be openclaw, claude-code, hermes, pi, ironclaw, nemoclaw, or kiro-cli." } } diff --git a/packs/kiro-cli/install.sh b/packs/kiro-cli/install.sh new file mode 100755 index 0000000..bdec9f8 --- /dev/null +++ b/packs/kiro-cli/install.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# packs/kiro-cli/install.sh — Install Kiro CLI (AWS agentic IDE terminal client) +# +# Usage: +# ./install.sh [--region us-east-1] +# +# Kiro CLI uses its own cloud inference (not Bedrock/bedrockify). +# Requires interactive login AFTER install: +# kiro-cli login --use-device-flow +# +# Idempotent: safe to re-run. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=../common.sh +source "${SCRIPT_DIR}/../common.sh" + +# ── Defaults ────────────────────────────────────────────────────────────────── +PACK_ARG_REGION="$(pack_config_get region "us-east-1")" + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { + cat </dev/null; then + KIROCLI_EXISTING="$(kiro-cli --version 2>/dev/null || echo unknown)" + log "kiro-cli already installed (${KIROCLI_EXISTING}) — reinstalling" +fi + +curl -fsSL https://cli.kiro.dev/install -o /tmp/install-kiro-cli.sh +sudo -u ec2-user bash /tmp/install-kiro-cli.sh +rm -f /tmp/install-kiro-cli.sh + +# Refresh PATH for current session +export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}" + +if ! command -v kiro-cli &>/dev/null; then + fail "kiro-cli command not found after install. Check PATH or installer output." +fi + +KIROCLI_VERSION="$(kiro-cli --version 2>/dev/null || echo unknown)" +ok "Kiro CLI installed: ${KIROCLI_VERSION}" + +# ── Step 2: Install MCP server prerequisites ────────────────────────────────── +step "Installing MCP server prerequisites (uv + pip)" + +if ! command -v uv &>/dev/null; then + log "Installing uv (Python package manager)..." + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="${HOME}/.cargo/bin:${HOME}/.local/bin:${PATH}" +fi + +if command -v uv &>/dev/null; then + ok "uv available: $(uv --version 2>/dev/null || echo unknown)" +else + warn "uv not found after install — MCP servers may not install correctly" +fi + +if ! command -v uvx &>/dev/null; then + warn "uvx not found — MCP tool runner unavailable; skipping MCP server installs" +else + ok "uvx available" +fi + +# ── Step 3: Install common AWS MCP servers ──────────────────────────────────── +step "Installing common AWS MCP servers" + +if command -v uvx &>/dev/null; then + MCP_SERVERS=( + "awslabs.terraform-mcp-server" + "awslabs.ecs-mcp-server" + "awslabs.eks-mcp-server" + "awslabs.core-mcp-server" + "awslabs.aws-documentation-mcp-server" + ) + + for mcp_server in "${MCP_SERVERS[@]}"; do + log "Caching MCP server: ${mcp_server}" + uvx --from "${mcp_server}" true 2>/dev/null && \ + ok "Cached: ${mcp_server}" || \ + warn "Could not pre-cache ${mcp_server} (will be fetched on first use)" + done +else + warn "uvx not available — skipping MCP server pre-cache (servers will be fetched on first use)" +fi + +# ── Step 4: Post-install instructions ──────────────────────────────────────── +step "Post-install notice" + +cat <<'NOTICE' + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + [KIRO CLI] INTERACTIVE LOGIN REQUIRED +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Kiro CLI requires IAM Identity Center (SSO) authentication. + This step cannot be automated — it requires a browser. + + Run this command interactively after connecting to the instance: + + kiro-cli login --use-device-flow + + This will print a device code + URL. Open the URL in your + browser, enter the code, and authenticate with your AWS SSO. + + Usage after login: + kiro-cli # Start interactive CLI + kiro-cli --agent platform-engineer # Start with specific agent + kiro-cli settings chat.defaultAgent # Show/set default agent + /model # Select AI model (inside CLI) + /tools # List MCP tools (inside CLI) + + MCP server config: ~/.kiro/agents/*.json + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +NOTICE + +# Install shell profile (aliases + banner) +SHELL_PROFILE="${SCRIPT_DIR}/resources/shell-profile.sh" +if [[ -f "${SHELL_PROFILE}" && -d /etc/profile.d ]]; then + sudo cp "${SHELL_PROFILE}" /etc/profile.d/kiro-cli.sh 2>/dev/null && \ + ok "Shell profile installed: /etc/profile.d/kiro-cli.sh" || \ + warn "Could not install shell profile (permission denied?)" +fi + +# ── Done ────────────────────────────────────────────────────────────────────── +write_done_marker "kiro-cli" +printf "\n[PACK:kiro-cli] INSTALLED — run 'kiro-cli login --use-device-flow' to authenticate\n" diff --git a/packs/kiro-cli/manifest.yaml b/packs/kiro-cli/manifest.yaml new file mode 100644 index 0000000..d2f4041 --- /dev/null +++ b/packs/kiro-cli/manifest.yaml @@ -0,0 +1,35 @@ +name: kiro-cli +version: "1.0.0" +type: agent +description: "Kiro CLI — AWS agentic IDE terminal client with MCP server support (experimental)" + +deps: [] + +requirements: + arch: + - arm64 + - amd64 + os: + - al2023 + - ubuntu2204 + min_instance_type: t4g.medium + +params: + - name: region + description: "AWS region (informational; Kiro CLI uses its own cloud inference)" + default: us-east-1 + +health_check: + command: "kiro-cli --version" + timeout: 10 + +provides: + commands: + - kiro-cli + services: [] + +instance_type: t4g.medium +root_volume_gb: 40 +data_volume_gb: 0 + +experimental: true diff --git a/packs/kiro-cli/resources/shell-profile.sh b/packs/kiro-cli/resources/shell-profile.sh new file mode 100644 index 0000000..df12373 --- /dev/null +++ b/packs/kiro-cli/resources/shell-profile.sh @@ -0,0 +1,27 @@ +# Kiro CLI shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +# Defines aliases and welcome banner for the kiro-cli pack. +# +# NOTE: Kiro CLI requires interactive login after install. +# Run: kiro-cli login --use-device-flow + +PACK_ALIASES=' +alias kiro="kiro-cli" +alias kiro-agent="kiro-cli --agent" +alias kiro-login="kiro-cli login --use-device-flow" +' + +PACK_BANNER_NAME="Kiro CLI Agent Environment" +PACK_BANNER_EMOJI="⚡" +PACK_BANNER_COMMANDS=' + kiro-cli → Start interactive Kiro CLI + kiro-cli --agent platform-engineer → Start with specific agent + kiro-cli login --use-device-flow → Authenticate (REQUIRED first run) + kiro-cli settings chat.defaultAgent → Show/set default agent +' + +# ⚠ Login reminder: check if kiro-cli is authenticated +if command -v kiro-cli &>/dev/null; then + if ! kiro-cli status &>/dev/null 2>&1; then + printf '\n\033[0;33m⚠ Kiro CLI: not authenticated. Run: kiro-cli login --use-device-flow\033[0m\n\n' + fi +fi diff --git a/packs/registry.json b/packs/registry.json index 4152c6f..2c9b87e 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -82,6 +82,18 @@ "requires_docker": true, "compatible_profiles": ["personal_assistant"] }, + "kiro-cli": { + "type": "agent", + "description": "Kiro CLI -- AWS agentic IDE terminal client with MCP server support (experimental)", + "deps": [], + "instance_type": "t4g.medium", + "root_volume_gb": 40, + "data_volume_gb": 0, + "ports": {}, + "brain": false, + "claude_code": false, + "experimental": true + }, "claude-code": { "type": "agent", "description": "Claude Code — Anthropic's coding agent with native Bedrock support", diff --git a/packs/registry.yaml b/packs/registry.yaml index 535f95a..e32cb1f 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -101,3 +101,15 @@ packs: requires_docker: true compatible_profiles: - personal_assistant # Sandbox blocks AWS API access + + kiro-cli: + type: agent + description: "Kiro CLI — AWS agentic IDE terminal client with MCP server support (experimental)" + deps: [] + instance_type: t4g.medium + root_volume_gb: 40 + data_volume_gb: 0 + ports: {} + brain: false + claude_code: false + experimental: true diff --git a/tests/test-registry-parser.sh b/tests/test-registry-parser.sh index 56f6e92..3bd80e4 100644 --- a/tests/test-registry-parser.sh +++ b/tests/test-registry-parser.sh @@ -77,6 +77,7 @@ assert_contains "includes hermes" "hermes|" "$output" assert_contains "includes pi" "pi|" "$output" assert_contains "includes ironclaw" "ironclaw|" "$output" assert_contains "includes nemoclaw" "nemoclaw|" "$output" +assert_contains "includes kiro-cli" "kiro-cli|" "$output" bedrockify_as_pack=$(echo "$output" | grep -c '^bedrockify|' || true) assert_eq "excludes base packs (bedrockify)" "0" "$bedrockify_as_pack" From e45dc3d49ac85e343ca05a8ef67716f7ad4c4fa2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 16:36:46 +0000 Subject: [PATCH 123/172] ci: bump version to 0.5.55 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 4478081..351cf3d 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.54" +INSTALLER_VERSION="0.5.55" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From b7d578cfe2e65f6ac904ea308b20c388bc91c9a0 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 17:02:00 +0000 Subject: [PATCH 124/172] fix: kiro-cli use uvenv install (match AWS sample repo), add build tools - MCP servers: uvenv install instead of uvx --from (matches sample repo pattern) - Add gcc + python3-devel for MCP servers with C extensions - Shell profile: simplified login reminder (kiro-cli status may not exist) --- packs/kiro-cli/install.sh | 33 ++++++++++++++++------- packs/kiro-cli/resources/shell-profile.sh | 6 ++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packs/kiro-cli/install.sh b/packs/kiro-cli/install.sh index bdec9f8..e72ad19 100755 --- a/packs/kiro-cli/install.sh +++ b/packs/kiro-cli/install.sh @@ -86,8 +86,15 @@ KIROCLI_VERSION="$(kiro-cli --version 2>/dev/null || echo unknown)" ok "Kiro CLI installed: ${KIROCLI_VERSION}" # ── Step 2: Install MCP server prerequisites ────────────────────────────────── -step "Installing MCP server prerequisites (uv + pip)" +step "Installing MCP server prerequisites (uv + uvenv + build tools)" +# Install build tools for MCP servers with C extensions (matches AWS sample repo) +log "Installing build tools for MCP servers..." +if command -v dnf &>/dev/null; then + sudo dnf install -y -q gcc python3-devel 2>/dev/null || warn "Failed to install build tools (gcc, python3-devel)" +fi + +# Install uv (fast Python package manager) if not present if ! command -v uv &>/dev/null; then log "Installing uv (Python package manager)..." curl -LsSf https://astral.sh/uv/install.sh | sh @@ -100,16 +107,22 @@ else warn "uv not found after install — MCP servers may not install correctly" fi -if ! command -v uvx &>/dev/null; then - warn "uvx not found — MCP tool runner unavailable; skipping MCP server installs" +# Install uvenv (MCP server installer used by AWS samples) +if ! command -v uvenv &>/dev/null; then + log "Installing uvenv..." + pip3 install uvenv 2>/dev/null || warn "pip3 install uvenv failed" +fi + +if command -v uvenv &>/dev/null; then + ok "uvenv available" else - ok "uvx available" + warn "uvenv not found — will skip MCP server installs" fi # ── Step 3: Install common AWS MCP servers ──────────────────────────────────── step "Installing common AWS MCP servers" -if command -v uvx &>/dev/null; then +if command -v uvenv &>/dev/null; then MCP_SERVERS=( "awslabs.terraform-mcp-server" "awslabs.ecs-mcp-server" @@ -119,13 +132,13 @@ if command -v uvx &>/dev/null; then ) for mcp_server in "${MCP_SERVERS[@]}"; do - log "Caching MCP server: ${mcp_server}" - uvx --from "${mcp_server}" true 2>/dev/null && \ - ok "Cached: ${mcp_server}" || \ - warn "Could not pre-cache ${mcp_server} (will be fetched on first use)" + log "Installing MCP server: ${mcp_server}" + uvenv install "${mcp_server}" 2>/dev/null && \ + ok "Installed: ${mcp_server}" || \ + warn "Could not install ${mcp_server} (will be fetched on first use)" done else - warn "uvx not available — skipping MCP server pre-cache (servers will be fetched on first use)" + warn "uvenv not available — skipping MCP server installs (install manually with: uvenv install awslabs.)" fi # ── Step 4: Post-install instructions ──────────────────────────────────────── diff --git a/packs/kiro-cli/resources/shell-profile.sh b/packs/kiro-cli/resources/shell-profile.sh index df12373..73cf2fc 100644 --- a/packs/kiro-cli/resources/shell-profile.sh +++ b/packs/kiro-cli/resources/shell-profile.sh @@ -19,9 +19,7 @@ PACK_BANNER_COMMANDS=' kiro-cli settings chat.defaultAgent → Show/set default agent ' -# ⚠ Login reminder: check if kiro-cli is authenticated +# ⚠ Login reminder: check if kiro-cli is installed but remind about auth if command -v kiro-cli &>/dev/null; then - if ! kiro-cli status &>/dev/null 2>&1; then - printf '\n\033[0;33m⚠ Kiro CLI: not authenticated. Run: kiro-cli login --use-device-flow\033[0m\n\n' - fi + printf '\n\033[0;33m⚠ Kiro CLI installed. If not yet authenticated, run: kiro-cli login --use-device-flow\033[0m\n\n' fi From a5366c2e04fd11efb7c60f580979476494e05476 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 17:02:25 +0000 Subject: [PATCH 125/172] ci: bump version to 0.5.56 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 351cf3d..3b3749e 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.55" +INSTALLER_VERSION="0.5.56" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From a590cb895b350e404a6016e36154c9afa00e0213 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 17:10:14 +0000 Subject: [PATCH 126/172] =?UTF-8?q?docs:=20add=20kiro-cli=20to=20README=20?= =?UTF-8?q?=E2=80=94=20pack=20tables,=20one-liners,=20standalone=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fecfb11..39cb5f0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ > > # Sandboxed personal assistant (NemoClaw — isolated in OpenShell sandbox) > curl -sfL .../install.sh | bash -s -- --non-interactive --pack nemoclaw --profile personal_assistant +> +> # Kiro CLI agent (AWS agentic IDE — requires interactive login after deploy) +> curl -sfL .../install.sh | bash -s -- --non-interactive --pack kiro-cli --profile builder > ``` > > Requires: AWS CLI + admin access on a **dedicated sandbox account**. @@ -47,7 +50,7 @@ Run the install command from the TL;DR above. The installer walks you through ** | Flag | Description | |------|-------------| | `--non-interactive` | Skip all prompts, use defaults (aliases: `--yes`, `-y`) | -| `--pack ` | Agent pack: `openclaw`, `claude-code`, `hermes`, `nemoclaw`, `pi`, `ironclaw` | +| `--pack ` | Agent pack: `openclaw`, `claude-code`, `hermes`, `nemoclaw`, `kiro-cli`, `pi`, `ironclaw` | | `--profile ` | Permission profile: `builder`, `account_assistant`, `personal_assistant` | | `--method ` | Deploy method: `cfn` (CloudFormation), `terraform` / `tf` | @@ -68,6 +71,7 @@ Run the install command from the TL;DR above. The installer walks you through ** | **Pi** *(experimental)* | Minimal terminal coding harness — read, write, edit, bash tools | t4g.medium sufficient | None needed (set to 0) | | **IronClaw** *(experimental)* | Rust-based AI agent by NEAR AI — static binary, fast startup | t4g.medium sufficient | None needed (set to 0) | | **NemoClaw** *(experimental)* | OpenClaw in NVIDIA OpenShell sandbox — Landlock + seccomp + netns isolation, Bedrock via bedrockify. `personal_assistant` profile only. | t4g.xlarge required | 80GB | +| **Kiro CLI** *(experimental)* | AWS agentic IDE terminal client with MCP server support. Uses own cloud inference (not Bedrock). Requires interactive SSO login after deploy. | t4g.medium sufficient | None needed (set to 0) | The installer discovers packs dynamically and asks which to deploy. Experimental packs are clearly marked. @@ -184,6 +188,7 @@ Loki uses a **pack-based architecture** for deploying different AI agent runtime | `pi` | Agent *(experimental)* | Pi Coding Agent. Minimal terminal coding harness with read, write, edit, bash tools. Pure Node.js. | | `ironclaw` | Agent *(experimental)* | IronClaw by NEAR AI. Rust-based agent with shell/file tools, MCP support. Single static binary. | | `nemoclaw` | Agent *(experimental)* | NemoClaw — OpenClaw inside an [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) sandbox with Landlock, seccomp, and network namespace isolation. Inference routed through bedrockify on the host (no NVIDIA API key needed). **Only compatible with `personal_assistant` profile** — the sandbox blocks all AWS API access. Requires Docker + t4g.xlarge. | +| `kiro-cli` | Agent *(experimental)* | [Kiro CLI](https://kiro.dev/docs/cli) — AWS agentic IDE terminal client with MCP server support. Uses its own cloud inference (no Bedrock/bedrockify). Pre-installs AWS MCP servers (terraform, ecs, eks, core, docs). **Requires interactive SSO login after deploy:** `kiro-cli login --use-device-flow`. | ### How It Works @@ -218,6 +223,9 @@ bash packs/openclaw/install.sh --region us-east-1 --model us.anthropic.claude-op # Or for NemoClaw (sandboxed OpenClaw — needs Docker, personal_assistant only) bash packs/nemoclaw/install.sh --region us-east-1 --model us.anthropic.claude-sonnet-4-6 --profile personal_assistant + +# Or for Kiro CLI (no bedrockify needed, requires interactive login after install) +bash packs/kiro-cli/install.sh --region us-east-1 ``` ### Adding New Packs @@ -468,7 +476,7 @@ Loki is: Loki is fully open source. The deployment templates, brain files, skills, and bootstrap scripts are all available at [github.com/inceptionstack/loki-agent](https://github.com/inceptionstack/loki-agent). -Built on [OpenClaw](https://github.com/openclaw/openclaw), [Hermes](https://github.com/NousResearch/hermes-agent), and [NemoClaw](https://github.com/NVIDIA/NemoClaw) — choose your agent runtime at deploy time. +Built on [OpenClaw](https://github.com/openclaw/openclaw), [Hermes](https://github.com/NousResearch/hermes-agent), [NemoClaw](https://github.com/NVIDIA/NemoClaw), and [Kiro CLI](https://kiro.dev) — choose your agent runtime at deploy time. ### InceptionStack Repositories From c9b9796437b343d4002e33056026a12824a2e819 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 17:52:02 +0000 Subject: [PATCH 127/172] fix: read from /dev/tty so 'curl loki.run | bash' works interactively When piped (curl | bash), stdin is the script content, not the terminal. All read -rp calls now use '< /dev/tty' to read user input from the terminal directly, matching the pattern used by mise, nvm, and rustup. This enables: curl -sfL loki.run | bash --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 3b3749e..b4f3e86 100755 --- a/install.sh +++ b/install.sh @@ -100,7 +100,7 @@ prompt() { return fi local display="$text"; [[ -n "$default" ]] && display="$text [$default]" - read -rp "$(echo -e "${BOLD}${display}:${NC} ")" value + read -rp "$(echo -e "${BOLD}${display}:${NC} ")" value < /dev/tty printf -v "$var" '%s' "${value:-$default}" } @@ -108,7 +108,7 @@ confirm() { local text="$1" default="${2:-default_no}" if [[ "$AUTO_YES" == true ]]; then return 0; fi local hint="[y/N]"; [[ "$default" == "default_yes" ]] && hint="[Y/n]" - read -rp "$(echo -e "${BOLD}${text} ${hint}:${NC} ")" answer + read -rp "$(echo -e "${BOLD}${text} ${hint}:${NC} ")" answer < /dev/tty case "$default" in default_yes) [[ ! "$answer" =~ ^[Nn]$ ]] ;; *) [[ "$answer" =~ ^[Yy]$ ]] ;; @@ -122,7 +122,7 @@ toggle() { return fi local hint="[Y/n]"; [[ "$default" == "false" ]] && hint="[y/N]" - read -rp "$(echo -e " ${text} ${hint}: ")" answer + read -rp "$(echo -e " ${text} ${hint}: ")" answer < /dev/tty case "$default" in true) [[ "$answer" =~ ^[Nn]$ ]] && printf -v "$var" '%s' "false" || printf -v "$var" '%s' "true" ;; false) [[ "$answer" =~ ^[Yy]$ ]] && printf -v "$var" '%s' "true" || printf -v "$var" '%s' "false" ;; From 023e30d7fa60c30861c713527ad8c4d15a30c73c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 17:52:27 +0000 Subject: [PATCH 128/172] ci: bump version to 0.5.57 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b4f3e86..696c271 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.56" +INSTALLER_VERSION="0.5.57" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 5cc8b64ac7f75c70f60d8dd01950f42a8e3a4a75 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 17:56:01 +0000 Subject: [PATCH 129/172] docs: update README with loki.run short URLs - Install: curl -sfL loki.run | bash - Uninstall: curl -sfL uninstall.loki.run | bash - All one-liner examples use loki.run instead of raw GitHub URLs - Note bash/zsh/CloudShell compatibility --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 39cb5f0..09c27a0 100644 --- a/README.md +++ b/README.md @@ -8,34 +8,34 @@ > **TL;DR — deploy Loki:** > > ```sh -> curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh | bash +> curl -sfL loki.run | bash > ``` > -> The installer walks you through pack, profile, and deploy method interactively. +> Works in **bash**, **zsh**, and **AWS CloudShell**. The installer walks you through pack, profile, and deploy method interactively. > > **One-liner examples (non-interactive):** > ```sh > # Full builder agent (can create/modify/delete AWS resources) -> curl -sfL .../install.sh | bash -s -- --non-interactive --pack openclaw --profile builder +> curl -sfL loki.run | bash -s -- -y --pack openclaw --profile builder > > # Read-only advisor (can see everything, change nothing) -> curl -sfL .../install.sh | bash -s -- --non-interactive --pack openclaw --profile account_assistant +> curl -sfL loki.run | bash -s -- -y --pack openclaw --profile account_assistant > > # Personal assistant (Bedrock only, no AWS access) -> curl -sfL .../install.sh | bash -s -- --non-interactive --pack claude-code --profile personal_assistant +> curl -sfL loki.run | bash -s -- -y --pack claude-code --profile personal_assistant > > # Sandboxed personal assistant (NemoClaw — isolated in OpenShell sandbox) -> curl -sfL .../install.sh | bash -s -- --non-interactive --pack nemoclaw --profile personal_assistant +> curl -sfL loki.run | bash -s -- -y --pack nemoclaw --profile personal_assistant > > # Kiro CLI agent (AWS agentic IDE — requires interactive login after deploy) -> curl -sfL .../install.sh | bash -s -- --non-interactive --pack kiro-cli --profile builder +> curl -sfL loki.run | bash -s -- -y --pack kiro-cli --profile builder > ``` > > Requires: AWS CLI + admin access on a **dedicated sandbox account**. > > ⚠️ Deploy in a clean account — LLMs make mistakes, a sandbox limits the blast radius. > -> **Uninstall:** `curl -sfL .../uninstall.sh | bash` +> **Uninstall:** `curl -sfL uninstall.loki.run | bash` --- @@ -43,7 +43,7 @@ ### Step 1: Install Loki -Run the install command from the TL;DR above. The installer walks you through **pack**, **profile**, **instance size**, and **deploy method** (CloudFormation or Terraform). +Run `curl -sfL loki.run | bash` — the installer walks you through **pack**, **profile**, **instance size**, and **deploy method** (CloudFormation or Terraform). **CLI flags for non-interactive deploys:** @@ -166,7 +166,7 @@ If you want to use Loki via Telegram, run the Telegram bootstraps located at: [` Remove one or all Loki deployments from your account: ```sh -curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/uninstall.sh -o /tmp/loki-uninstall.sh && bash /tmp/loki-uninstall.sh +curl -sfL uninstall.loki.run | bash ``` Finds deployments by tag, lets you pick which to remove, deletes CloudFormation stacks or cleans up resources manually (Terraform deploys), and optionally removes state buckets/lock tables. From 66d15800afec3a67c748895611f7c2dfb89ca3ad Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 18:57:14 +0000 Subject: [PATCH 130/172] fix: openclaw pack should not install claude code by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude_code: true → false in both registry.yaml and registry.json. Claude Code should only be installed when explicitly selected as the pack. --- packs/registry.json | 30 ++++++++++++++++++++++-------- packs/registry.yaml | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packs/registry.json b/packs/registry.json index 2c9b87e..5c77e4e 100644 --- a/packs/registry.json +++ b/packs/registry.json @@ -18,20 +18,26 @@ "openclaw": { "type": "agent", "description": "OpenClaw -- stateful AI agent with persistent gateway", - "deps": ["bedrockify"], + "deps": [ + "bedrockify" + ], "instance_type": "t4g.xlarge", "root_volume_gb": 40, "data_volume_gb": 80, "default_model": "us.anthropic.claude-opus-4-6-v1", - "ports": { "gateway": 3001 }, + "ports": { + "gateway": 3001 + }, "brain": true, - "claude_code": true, + "claude_code": false, "experimental": false }, "hermes": { "type": "agent", "description": "Hermes -- NousResearch CLI agent via bedrockify (experimental)", - "deps": ["bedrockify"], + "deps": [ + "bedrockify" + ], "instance_type": "t4g.medium", "root_volume_gb": 40, "data_volume_gb": 0, @@ -44,7 +50,9 @@ "pi": { "type": "agent", "description": "Pi -- minimal terminal coding harness (experimental)", - "deps": ["bedrockify"], + "deps": [ + "bedrockify" + ], "instance_type": "t4g.medium", "root_volume_gb": 40, "data_volume_gb": 0, @@ -57,7 +65,9 @@ "ironclaw": { "type": "agent", "description": "IronClaw -- Rust-based AI agent by NEAR AI (experimental)", - "deps": ["bedrockify"], + "deps": [ + "bedrockify" + ], "instance_type": "t4g.medium", "root_volume_gb": 40, "data_volume_gb": 0, @@ -70,7 +80,9 @@ "nemoclaw": { "type": "agent", "description": "NemoClaw -- OpenClaw in sandboxed OpenShell with Bedrock inference", - "deps": ["bedrockify"], + "deps": [ + "bedrockify" + ], "instance_type": "t4g.xlarge", "root_volume_gb": 40, "data_volume_gb": 80, @@ -80,7 +92,9 @@ "claude_code": false, "experimental": true, "requires_docker": true, - "compatible_profiles": ["personal_assistant"] + "compatible_profiles": [ + "personal_assistant" + ] }, "kiro-cli": { "type": "agent", diff --git a/packs/registry.yaml b/packs/registry.yaml index e32cb1f..c6e42a3 100644 --- a/packs/registry.yaml +++ b/packs/registry.yaml @@ -40,7 +40,7 @@ packs: ports: gateway: 3001 brain: true - claude_code: true + claude_code: false experimental: false hermes: From beb715a8cfd97c76a0bc4aadba341531cab7de45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 18:57:41 +0000 Subject: [PATCH 131/172] ci: bump version to 0.5.58 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 696c271..41f8da0 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.57" +INSTALLER_VERSION="0.5.58" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From cfc8087face688c35ed4d4263c79df0419050712 Mon Sep 17 00:00:00 2001 From: Loki Date: Sat, 4 Apr 2026 23:25:23 +0000 Subject: [PATCH 132/172] fix: add heartbeat config to default openclaw deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config-gen.py now includes heartbeat block with: model: sonnet 4.6 (lighter model for heartbeats) target: telegram every: 30m lightContext: true isolatedSession: true Without this, deployed agents had no scheduled checks — no idle monitoring, no security scans, no app health checks. This is a core feature for proactive assistant behavior. --- packs/openclaw/resources/config-gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packs/openclaw/resources/config-gen.py b/packs/openclaw/resources/config-gen.py index 1b8d025..f8edde7 100644 --- a/packs/openclaw/resources/config-gen.py +++ b/packs/openclaw/resources/config-gen.py @@ -20,7 +20,7 @@ home = os.path.expanduser("~") cfg = { "models": {"providers": {"amazon-bedrock": {"baseUrl": f"https://bedrock-runtime.{bedrock_region}.amazonaws.com", "auth": "aws-sdk", "api": "bedrock-converse-stream", "models": []}}, "bedrockDiscovery": {"enabled": True, "region": "us-east-1", "providerFilter": ["anthropic"]}}, - "agents": {"defaults": {"model": {"primary": f"amazon-bedrock/{model}", "fallbacks": ["amazon-bedrock/us.anthropic.claude-sonnet-4-6"]}, "workspace": f"{home}/.openclaw/workspace", "compaction": {"mode": "safeguard"}, "maxConcurrent": 4, "subagents": {"maxConcurrent": 8}}}, + "agents": {"defaults": {"model": {"primary": f"amazon-bedrock/{model}", "fallbacks": ["amazon-bedrock/us.anthropic.claude-sonnet-4-6"]}, "workspace": f"{home}/.openclaw/workspace", "compaction": {"mode": "safeguard"}, "heartbeat": {"model": f"amazon-bedrock/us.anthropic.claude-sonnet-4-6", "target": "telegram", "every": "30m", "lightContext": True, "isolatedSession": True}, "maxConcurrent": 4, "subagents": {"maxConcurrent": 8}}}, "tools": {"web": {"search": {"enabled": False}, "fetch": {"enabled": True}}}, "hooks": {"internal": {"enabled": True, "entries": {"boot-md": {"enabled": True}, "bootstrap-extra-files": {"enabled": True}, "command-logger": {"enabled": True}, "session-memory": {"enabled": True}}}}, "gateway": {"port": int(gw_port), "mode": "local", "bind": "loopback", "auth": {"mode": "token", "token": gw_token}} From bb0378c7a91fef273961d7d965b3ccb26baf80b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Apr 2026 23:25:49 +0000 Subject: [PATCH 133/172] ci: bump version to 0.5.59 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 41f8da0..8dcd858 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.58" +INSTALLER_VERSION="0.5.59" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 989d150ace739d3ddacd3298b575e6369bc876bd Mon Sep 17 00:00:00 2001 From: Loki Date: Sun, 5 Apr 2026 08:10:37 +0000 Subject: [PATCH 134/172] Add BOOTSTRAP-IDLE-SHUTDOWN to optional bootstraps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idle shutdown for EC2 agents — systemd timer checks every 5min, sends Telegram warning after 1h idle, shuts down on next run. Fully independent of OpenClaw gateway. --- .../optional/BOOTSTRAP-IDLE-SHUTDOWN.md | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 bootstraps/optional/BOOTSTRAP-IDLE-SHUTDOWN.md diff --git a/bootstraps/optional/BOOTSTRAP-IDLE-SHUTDOWN.md b/bootstraps/optional/BOOTSTRAP-IDLE-SHUTDOWN.md new file mode 100644 index 0000000..b09d532 --- /dev/null +++ b/bootstraps/optional/BOOTSTRAP-IDLE-SHUTDOWN.md @@ -0,0 +1,283 @@ +# BOOTSTRAP-IDLE-SHUTDOWN.md — Idle Shutdown for EC2 Agents + +> **Purpose:** Automatically shut down the EC2 instance when the user has been idle for over 1 hour. Sends a Telegram warning before shutdown. Fully independent of the OpenClaw gateway — runs via systemd timer. + +--- + +## How It Works + +1. A systemd timer fires every 5 minutes +2. It runs a bash script that reads the OpenClaw session JSONL files to find the last user message timestamp +3. If idle > 1 hour: sends a Telegram alert via Bot API (direct curl, no OpenClaw dependency) +4. On the next run (5 min later), if still idle: `sudo shutdown -h now` +5. State is tracked in `memory/heartbeat-state.json` (`idleShutdownAlertSent` flag) + +--- + +## Prerequisites + +- EC2 instance with `sudo` access for `ec2-user` +- OpenClaw installed and configured with Telegram channel +- Telegram bot token and your Telegram chat ID (numeric) +- Python 3 available (`/usr/bin/python3`) + +--- + +## Step 1 — Create the Python Helper + +Save to `~/.openclaw/workspace/idle-check.py`: + +```python +#!/usr/bin/env python3 +"""Helper for idle-check scripts""" +import sys, json +from datetime import datetime, timezone + +def parse_ts(ts): + for fmt in ('%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%SZ'): + try: + return datetime.strptime(ts, fmt).replace(tzinfo=timezone.utc) + except: + pass + return None + +cmd = sys.argv[1] + +if cmd == '--latest-ts': + latest = None + for line in sys.stdin: + try: + obj = json.loads(line) + ts = obj.get('createdAt') or obj.get('timestamp') or obj.get('ts') + if ts and (latest is None or ts > latest): + latest = ts + except: + pass + print(latest or '') + +elif cmd == '--hours-idle': + ts = sys.argv[2] + dt = parse_ts(ts) + if dt is None: + print('PARSE_ERROR') + sys.exit(1) + now = datetime.now(timezone.utc) + hours = (now - dt).total_seconds() / 3600 + print(f'{hours:.4f}') + +elif cmd == '--should-shutdown': + hours = float(sys.argv[2]) + threshold = float(sys.argv[3]) + print('yes' if hours > threshold else 'no') + +elif cmd == '--get-state': + state_file = sys.argv[2] + key = sys.argv[3] + try: + with open(state_file) as f: + d = json.load(f) + print(str(d.get(key, False)).lower()) + except: + print('false') + +elif cmd == '--set-state': + state_file = sys.argv[2] + key = sys.argv[3] + val = sys.argv[4] + parsed_val = True if val == 'true' else False if val == 'false' else val + try: + with open(state_file) as f: + d = json.load(f) + except: + d = {} + d[key] = parsed_val + with open(state_file, 'w') as f: + json.dump(d, f, indent=2) +``` + +--- + +## Step 2 — Create the Idle Check Script + +Save to `~/.openclaw/workspace/loki-idle-check.sh`. **Replace the bot token and chat ID with your own.** + +```bash +#!/bin/bash +# loki-idle-check.sh — Standalone idle monitor, runs via systemd timer every 5 min +# No model involved. Checks last user message and shuts down if idle > 1 hour. +# Sends Telegram alert before shutdown using Bot API directly (no openclaw needed). + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +STATE_FILE="$HOME/.openclaw/workspace/memory/heartbeat-state.json" +SESSIONS_DIR="$HOME/.openclaw/agents/main/sessions" +PYTHON_SCRIPT="$SCRIPT_DIR/idle-check.py" +IDLE_THRESHOLD_HOURS=1.0 +TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN_HERE" +TELEGRAM_CHAT_ID="YOUR_NUMERIC_CHAT_ID_HERE" + +# Get latest user message timestamp +LATEST_TS=$(grep -h '"role":"user"' "$SESSIONS_DIR"/*.jsonl 2>/dev/null | python3 "$PYTHON_SCRIPT" --latest-ts) + +if [[ -z "$LATEST_TS" ]]; then + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) ERROR: No user messages found in session logs." >> /tmp/loki-idle-check.log + exit 1 +fi + +HOURS_IDLE=$(python3 "$PYTHON_SCRIPT" --hours-idle "$LATEST_TS") + +if [[ "$HOURS_IDLE" == "PARSE_ERROR" ]]; then + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) ERROR: Could not parse timestamp: $LATEST_TS" >> /tmp/loki-idle-check.log + exit 1 +fi + +echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) idle=${HOURS_IDLE}h last_msg=${LATEST_TS}" >> /tmp/loki-idle-check.log + +SHOULD_SHUTDOWN=$(python3 "$PYTHON_SCRIPT" --should-shutdown "$HOURS_IDLE" "$IDLE_THRESHOLD_HOURS") + +if [[ "$SHOULD_SHUTDOWN" == "yes" ]]; then + ALERT_SENT=$(python3 "$PYTHON_SCRIPT" --get-state "$STATE_FILE" idleShutdownAlertSent) + + if [[ "$ALERT_SENT" == "false" ]]; then + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) IDLE >1h — sending Telegram alert" >> /tmp/loki-idle-check.log + + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -H "Content-Type: application/json" \ + -d "{\"chat_id\":\"${TELEGRAM_CHAT_ID}\",\"text\":\"🐺 Loki here — I've been idle for over an hour. Shutting down in ~5 minutes to save costs. Run wake-loki.sh to bring me back.\"}" \ + >> /tmp/loki-idle-check.log 2>&1 + + python3 "$PYTHON_SCRIPT" --set-state "$STATE_FILE" idleShutdownAlertSent true + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Alert sent. Will shutdown on next run." >> /tmp/loki-idle-check.log + + else + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Alert already sent — SHUTTING DOWN NOW" >> /tmp/loki-idle-check.log + sudo shutdown -h now + fi + +else + # Active — reset alert flag if user came back + python3 "$PYTHON_SCRIPT" --set-state "$STATE_FILE" idleShutdownAlertSent false +fi +``` + +--- + +## Step 3 — Create the Systemd Timer + +Run as root (or with sudo): + +```bash +sudo tee /etc/systemd/system/loki-idle-check.service << 'EOF' +[Unit] +Description=Loki idle check — shutdown if user is away for over 1 hour + +[Service] +Type=oneshot +User=ec2-user +ExecStart=/bin/bash /home/ec2-user/.openclaw/workspace/loki-idle-check.sh +TimeoutSec=30 +EOF + +sudo tee /etc/systemd/system/loki-idle-check.timer << 'EOF' +[Unit] +Description=Loki idle check timer — every 5 minutes + +[Timer] +OnBootSec=5min +OnUnitActiveSec=5min +AccuracySec=10s +Persistent=true + +[Install] +WantedBy=timers.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now loki-idle-check.timer +``` + +Verify it's running: +```bash +sudo systemctl status loki-idle-check.timer +``` + +Test immediately: +```bash +sudo systemctl start loki-idle-check.service +cat /tmp/loki-idle-check.log +``` + +--- + +## Step 4 — Create the Wake Script + +Save locally (on your laptop/phone) as `wake-loki.sh`. Needs an IAM user with only `ec2:StartInstances` on the specific instance. + +```bash +#!/bin/bash +# wake-loki.sh — Start the Loki EC2 instance +AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY \ +AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY \ +AWS_DEFAULT_REGION=us-east-1 \ +aws ec2 start-instances --instance-ids YOUR_INSTANCE_ID \ + && echo "✅ Loki is starting up! Give it ~60 seconds." +``` + +Create a minimal IAM user for the wake script: +```bash +# Create policy — only allows starting this one instance +aws iam create-policy --policy-name loki-wakeup-policy --policy-document '{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["ec2:StartInstances"], + "Resource": "arn:aws:ec2:REGION:ACCOUNT_ID:instance/INSTANCE_ID" + },{ + "Effect": "Allow", + "Action": "ec2:DescribeInstances", + "Resource": "*" + }] +}' + +aws iam create-user --user-name loki-wakeup +aws iam attach-user-policy --user-name loki-wakeup --policy-arn arn:aws:iam::ACCOUNT_ID:policy/loki-wakeup-policy +aws iam create-access-key --user-name loki-wakeup +# Save the output — bake into wake-loki.sh +``` + +Store credentials in Secrets Manager for future reference: +```bash +aws secretsmanager create-secret \ + --name "openclaw/loki-wakeup-credentials" \ + --secret-string '{"access_key_id":"...","secret_access_key":"...","instance_id":"...","region":"us-east-1"}' +``` + +--- + +## State File + +`~/.openclaw/workspace/memory/heartbeat-state.json`: +```json +{ + "idleShutdownAlertSent": false +} +``` + +--- + +## Log File + +Check `/tmp/loki-idle-check.log` to see every run: +``` +2026-04-05T08:06:23Z idle=0.0077h last_msg=2026-04-05T08:05:55.617Z +2026-04-05T08:06:26Z idle=0.0085h last_msg=2026-04-05T08:05:55.617Z +``` + +--- + +## Notes + +- The timer is **completely independent of OpenClaw** — if the gateway crashes, idle shutdown still works +- Telegram alert uses **direct Bot API** via `curl` — no openclaw dependency +- The two-step shutdown (alert → wait 5min → shutdown) gives the user time to come back +- Session JSONL path: `~/.openclaw/agents/main/sessions/*.jsonl` — user messages have `"role":"user"` +- Idle threshold is `IDLE_THRESHOLD_HOURS=1.0` — change to any value From 674cfcd97797f344ff423c5360e8bc026821c970 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Apr 2026 08:10:50 +0000 Subject: [PATCH 135/172] ci: bump version to 0.5.60 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 8dcd858..fdea0c7 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.59" +INSTALLER_VERSION="0.5.60" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From fff6b32e8fd93e30aeb8b45ffd85408895070733 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Mon, 6 Apr 2026 02:27:51 +0300 Subject: [PATCH 136/172] feat: overhaul install.sh UX, reliability, and DRY refactoring - Require gum, remove all fallback UI paths - Stream terraform apply progress live with color-coded output - Show terraform errors inline using gum format - Display debug log locations on any script failure (EXIT trap) - Handle Ctrl-C properly with INT trap - Replace DynamoDB lock table with S3-native locking (use_lockfile=true) - Add terraform validate as a visible step before apply - Detect and fix wrong-architecture terraform (Rosetta on Apple Silicon) - Add --debug-in-repo flag for local development testing - Make bedrock form off by default (CFN + Terraform) - Auto-upgrade terraform if version < 1.10 - Install terraform to /tmp on CloudShell for disk space - Replace set -x with targeted dbg() logging - Add CI validation workflow (shellcheck, terraform fmt/validate) - DRY up helpers: run_or_fail, animate_spinner, detect_platform - Remove bootstrap spinner flickering Co-Authored-By: Loki @ Roy --- .github/workflows/validate.yml | 47 ++ deploy/cloudformation/template.yaml | 12 +- deploy/terraform/main.tf | 20 +- deploy/terraform/variables.tf | 11 + install.sh | 876 +++++++++++++++++----------- lib/terraform-check.sh | 28 + uninstall.sh | 23 +- 7 files changed, 640 insertions(+), 377 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100755 lib/terraform-check.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..45245e5 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,47 @@ +name: Validate + +on: + push: + branches: [main, 'feature/**'] + paths: + - 'deploy/terraform/**' + - 'deploy/cloudformation/**' + - 'install.sh' + - 'uninstall.sh' + - '.github/workflows/validate.yml' + pull_request: + paths: + - 'deploy/terraform/**' + - 'deploy/cloudformation/**' + - 'install.sh' + - 'uninstall.sh' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Bash syntax check + run: | + bash -n install.sh + bash -n uninstall.sh + echo "Bash syntax OK" + + - name: ShellCheck + run: | + shellcheck --severity=error install.sh uninstall.sh || true + + - uses: hashicorp/setup-terraform@v3 + + - name: Terraform fmt check + run: terraform fmt -check -diff deploy/terraform/ + + - name: Terraform validate + run: | + cd deploy/terraform + terraform init -backend=false -input=false + terraform validate diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 68c0585..f01dbf6 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -247,6 +247,12 @@ Parameters: NoEcho: true Description: "Direct API key from your AI provider (e.g. Anthropic). Only needed when Model Access Mode is 'api-key'." + EnableBedrockForm: + Type: String + Default: 'false' + AllowedValues: ['true', 'false'] + Description: "Submit the Bedrock model access use-case form. Only needed once per account — skip if Bedrock is already enabled." + RequestQuotaIncreases: Type: String Default: 'false' @@ -331,6 +337,7 @@ Conditions: IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] NeedsAdminUser: !Condition IsBuilder RunSecurityServices: !Not [!Condition IsPersonalAssistant] + RunBedrockForm: !Equals [!Ref EnableBedrockForm, 'true'] # ============================================================================ # RESOURCES @@ -618,6 +625,7 @@ Resources: # -------------------------------------------------------------------------- BedrockFormLambdaRole: Type: AWS::IAM::Role + Condition: RunBedrockForm Properties: RoleName: !Sub '${EnvironmentName}-bedrock-form-role' AssumeRolePolicyDocument: @@ -648,6 +656,7 @@ Resources: BedrockFormFunction: Type: AWS::Lambda::Function + Condition: RunBedrockForm Properties: FunctionName: !Sub '${EnvironmentName}-bedrock-form' Runtime: python3.12 @@ -756,6 +765,7 @@ Resources: BedrockFormCustomResource: Type: Custom::BedrockForm + Condition: RunBedrockForm DependsOn: BedrockFormLambdaRole Properties: ServiceToken: !GetAtt BedrockFormFunction.Arn @@ -1093,8 +1103,6 @@ Resources: # -------------------------------------------------------------------------- Instance: Type: AWS::EC2::Instance - DependsOn: - - BedrockFormCustomResource CreationPolicy: ResourceSignal: Timeout: PT30M diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 40993d7..b805b70 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -208,7 +208,8 @@ resource "aws_iam_instance_profile" "main" { # Bedrock Model Access (Lambda + invocation) # ============================================================================ resource "aws_iam_role" "bedrock_form_lambda" { - name = "${var.environment_name}-bedrock-form-role" + count = var.enable_bedrock_form == "true" ? 1 : 0 + name = "${var.environment_name}-bedrock-form-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -221,13 +222,15 @@ resource "aws_iam_role" "bedrock_form_lambda" { } resource "aws_iam_role_policy_attachment" "bedrock_form_lambda_basic" { - role = aws_iam_role.bedrock_form_lambda.name + count = var.enable_bedrock_form == "true" ? 1 : 0 + role = aws_iam_role.bedrock_form_lambda[0].name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } resource "aws_iam_role_policy" "bedrock_form" { - name = "bedrock-form" - role = aws_iam_role.bedrock_form_lambda.id + count = var.enable_bedrock_form == "true" ? 1 : 0 + name = "bedrock-form" + role = aws_iam_role.bedrock_form_lambda[0].id policy = jsonencode({ Version = "2012-10-17" @@ -254,6 +257,7 @@ resource "aws_iam_role_policy" "bedrock_form" { } data "archive_file" "bedrock_form" { + count = var.enable_bedrock_form == "true" ? 1 : 0 type = "zip" output_path = "${path.module}/.lambda_zips/bedrock_form.zip" @@ -330,18 +334,20 @@ def handler(event, context): } resource "aws_lambda_function" "bedrock_form" { + count = var.enable_bedrock_form == "true" ? 1 : 0 function_name = "${var.environment_name}-bedrock-form" - role = aws_iam_role.bedrock_form_lambda.arn + role = aws_iam_role.bedrock_form_lambda[0].arn handler = "index.handler" runtime = "python3.12" timeout = 120 - filename = data.archive_file.bedrock_form.output_path - source_code_hash = data.archive_file.bedrock_form.output_base64sha256 + filename = data.archive_file.bedrock_form[0].output_path + source_code_hash = data.archive_file.bedrock_form[0].output_base64sha256 depends_on = [aws_iam_role_policy.bedrock_form] } resource "null_resource" "bedrock_form_invoke" { + count = var.enable_bedrock_form == "true" ? 1 : 0 depends_on = [aws_lambda_function.bedrock_form] provisioner "local-exec" { diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index a8fa1d6..6a3c734 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -168,6 +168,17 @@ variable "request_quota_increases" { } } +variable "enable_bedrock_form" { + type = string + default = "false" + description = "Submit the Bedrock model access use-case form. Only needed once per account — skip if Bedrock is already enabled." + + validation { + condition = contains(["true", "false"], var.enable_bedrock_form) + error_message = "Must be true or false." + } +} + variable "enable_security_hub" { type = bool default = true diff --git a/install.sh b/install.sh index fdea0c7..5a4fd9a 100755 --- a/install.sh +++ b/install.sh @@ -4,6 +4,7 @@ # Flags: --non-interactive / -y Accept all defaults, minimal prompts # --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) # --method Pre-select deploy method: cfn, terraform (or tf) +# --debug-in-repo Copy local repo to /tmp instead of cloning (for local testing) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then @@ -12,23 +13,51 @@ fi set -euo pipefail +# Save original dir before changing (needed for --debug-in-repo) +_ORIG_DIR="$(pwd)" # Ensure we run from a safe CWD — avoid interference from local .env, direnv, etc. +# (--debug-in-repo will cd back after arg parsing) cd "$HOME" 2>/dev/null || cd /tmp export AWS_PAGER="" export PAGER="" aws() { command aws --no-cli-pager "$@"; } -# Catch unexpected exits so they're not silent; clean up temp clone dir if set -trap ' - echo -e "\n\033[0;31m✗ Installer exited unexpectedly at line $LINENO\033[0m" >&2 +# Persistent log file for debugging (survives script exit) +INSTALL_LOG="/tmp/loki-install.log" +: > "$INSTALL_LOG" + +show_debug_locations() { + echo -e "\033[1;33m Debug info:\033[0m" >&2 + if [[ -s "${INSTALL_LOG:-}" ]]; then + echo -e "\033[1;33m Installer log: ${INSTALL_LOG}\033[0m" >&2 + fi + if [[ -s "${_TF_LOG:-}" ]]; then + echo -e "\033[1;33m Terraform log: ${_TF_LOG}\033[0m" >&2 + fi if [[ -n "${CLONE_DIR:-}" && "${CLONE_DIR}" == /tmp/* && -d "$CLONE_DIR" ]]; then - echo -e "\033[1;33m⚠ Temp clone directory left at: ${CLONE_DIR}\033[0m" >&2 + echo -e "\033[1;33m Clone dir: ${CLONE_DIR}\033[0m" >&2 fi if [[ -n "${TF_WORKDIR:-}" && -d "$TF_WORKDIR" ]]; then - echo -e "\033[1;33m⚠ Temp Terraform workdir left at: ${TF_WORKDIR}\033[0m" >&2 + echo -e "\033[1;33m Terraform dir: ${TF_WORKDIR}\033[0m" >&2 + fi +} + +# Ctrl-C: kill background jobs and exit immediately +trap ' + echo -e "\n\033[0;31m✗ Interrupted\033[0m" >&2 + kill 0 2>/dev/null + exit 130 +' INT + +# Always show debug info on non-zero exit (EXIT trap is more reliable than ERR) +trap ' + exit_code=$? + if [[ $exit_code -ne 0 ]]; then + echo -e "\n\033[0;31m✗ Installer failed (exit code $exit_code)\033[0m" >&2 + show_debug_locations fi -' ERR +' EXIT REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" @@ -44,6 +73,7 @@ AUTO_YES=false PRESELECT_PACK="" PRESELECT_METHOD="" PRESELECT_PROFILE="" +DEBUG_IN_REPO=false while [[ $# -gt 0 ]]; do case "$1" in --non-interactive|--yes|-y) AUTO_YES=true; shift ;; @@ -65,17 +95,30 @@ while [[ $# -gt 0 ]]; do exit 1 fi PRESELECT_PROFILE="$2"; shift 2 ;; + --debug-in-repo) DEBUG_IN_REPO=true; shift ;; *) shift ;; esac done +# If --debug-in-repo, go back to the original directory (before cd $HOME) +if [[ "$DEBUG_IN_REPO" == "true" ]]; then + cd "$_ORIG_DIR" +fi +SCRIPT_DIR="$_ORIG_DIR" + +# Debug logging — writes to install log only, never to terminal +dbg() { + [[ "$DEBUG_IN_REPO" == "true" ]] && echo "[DBG] $*" >> "$INSTALL_LOG" + return 0 +} + # Deploy method constants DEPLOY_CFN_CONSOLE=1 DEPLOY_CFN_CLI=2 DEPLOY_TERRAFORM=3 # Stamped at release; fall back to git info at runtime -INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$(dirname "$0")" rev-parse --short HEAD 2>/dev/null || echo dev)}" -INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$(dirname "$0")" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" +INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)}" +INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$SCRIPT_DIR" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" # Detect AWS CloudShell (limited ~1GB home dir, use /tmp for large files) IS_CLOUDSHELL=false @@ -83,15 +126,106 @@ if [[ -n "${AWS_EXECUTION_ENV:-}" && "${AWS_EXECUTION_ENV}" == *"CloudShell"* ]] IS_CLOUDSHELL=true fi +# ============================================================================ +# gum — UI toolkit (installed to /tmp, no root required) +# ============================================================================ +GUM="" # set by install_gum — required, script fails without it +GUM_VERSION="0.14.5" # fallback version + +# ── Shared platform detection ──────────────────────────────────────────────── +# Sets DETECTED_OS and DETECTED_ARCH. Accepts optional arch style: +# "go" → amd64/arm64 (Terraform, Go binaries) +# default → x86_64/arm64 (gum, generic) +DETECTED_OS="" +DETECTED_ARCH="" + +# Get real hardware arch (uname -m and sysctl hw.machine lie under Rosetta) +hw_arch() { + if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null)" == "1" ]]; then + echo "arm64" + else + uname -m + fi +} + +detect_platform() { + local arch_style="${1:-default}" + case "$(uname -s)" in + Darwin) DETECTED_OS="Darwin" ;; + Linux) DETECTED_OS="Linux" ;; + *) DETECTED_OS=""; return 1 ;; + esac + case "$(hw_arch)" in + x86_64|amd64) + if [[ "$arch_style" == "go" ]]; then DETECTED_ARCH="amd64"; else DETECTED_ARCH="x86_64"; fi ;; + aarch64|arm64) DETECTED_ARCH="arm64" ;; + *) DETECTED_ARCH=""; return 1 ;; + esac +} + +install_gum() { + # Already installed? + if command -v gum &>/dev/null; then + GUM="gum"; return 0 + fi + local gum_bin="/tmp/gum-bin/gum" + if [[ -x "$gum_bin" ]]; then + GUM="$gum_bin"; return 0 + fi + + detect_platform || fail "Unsupported OS/architecture for gum: $(uname -s)/$(uname -m)" + local os="$DETECTED_OS" arch="$DETECTED_ARCH" + + # Try to get latest version from GitHub API, fall back to known good + local version + version=$(curl -sf https://api.github.com/repos/charmbracelet/gum/releases/latest 2>/dev/null \ + | grep '"tag_name"' | head -1 | sed 's/.*"v\([^"]*\)".*/\1/' || echo "") + [[ -z "$version" ]] && version="$GUM_VERSION" + + local url="https://github.com/charmbracelet/gum/releases/download/v${version}/gum_${version}_${os}_${arch}.tar.gz" + mkdir -p /tmp/gum-bin + if curl -sfL "$url" | tar xz --strip-components=1 -C /tmp/gum-bin 2>/dev/null; then + chmod +x "$gum_bin" + GUM="$gum_bin" + else + fail "Could not install gum. Check network connectivity and try again." + fi +} + # ============================================================================ # UI helpers # ============================================================================ -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' +MAGENTA='\033[0;35m' + +info() { echo -e " ${BLUE}▸${NC} $1"; } +ok() { echo -e " ${GREEN}✓${NC} $1"; } +warn() { echo -e " ${YELLOW}⚠${NC} $1"; } +fail() { echo -e " ${RED}✗${NC} $1"; show_debug_locations; exit 1; } + +# ── Elapsed time formatting ────────────────────────────────────────────────── +elapsed_fmt() { + local secs=$1 + if [[ $secs -lt 60 ]]; then + printf '%ds' "$secs" + else + printf '%dm %ds' "$((secs / 60))" "$((secs % 60))" + fi +} + +# ── Step progress tracker ──────────────────────────────────────────────────── +STEP_NUM=0 +TOTAL_STEPS=7 +STEP_NAMES=() -info() { echo -e "${BLUE}▸${NC} $1"; } -ok() { echo -e "${GREEN}✓${NC} $1"; } -warn() { echo -e "${YELLOW}⚠${NC} $1"; } -fail() { echo -e "${RED}✗${NC} $1"; exit 1; } +step() { + STEP_NUM=$((STEP_NUM + 1)) + STEP_NAMES+=("$1") + echo "" + $GUM style --foreground 117 --bold --border double --border-foreground 240 \ + --padding "0 2" --margin "0 2" "[${STEP_NUM}/${TOTAL_STEPS}] $1" + echo "" +} prompt() { local text="$1" var="$2" default="${3:-}" @@ -99,20 +233,21 @@ prompt() { printf -v "$var" '%s' "$default" return fi - local display="$text"; [[ -n "$default" ]] && display="$text [$default]" - read -rp "$(echo -e "${BOLD}${display}:${NC} ")" value < /dev/tty + local value + value=$($GUM input --header "$text" --value "$default" --placeholder "$text" < /dev/tty) || value="$default" printf -v "$var" '%s' "${value:-$default}" } confirm() { local text="$1" default="${2:-default_no}" if [[ "$AUTO_YES" == true ]]; then return 0; fi - local hint="[y/N]"; [[ "$default" == "default_yes" ]] && hint="[Y/n]" - read -rp "$(echo -e "${BOLD}${text} ${hint}:${NC} ")" answer < /dev/tty - case "$default" in - default_yes) [[ ! "$answer" =~ ^[Nn]$ ]] ;; - *) [[ "$answer" =~ ^[Yy]$ ]] ;; - esac + local rc=0 + if [[ "$default" == "default_yes" ]]; then + $GUM confirm --default=yes "$text" < /dev/tty || rc=$? + else + $GUM confirm "$text" < /dev/tty || rc=$? + fi + return $rc } toggle() { @@ -121,12 +256,13 @@ toggle() { printf -v "$var" '%s' "$default" return fi - local hint="[Y/n]"; [[ "$default" == "false" ]] && hint="[y/N]" - read -rp "$(echo -e " ${text} ${hint}: ")" answer < /dev/tty - case "$default" in - true) [[ "$answer" =~ ^[Nn]$ ]] && printf -v "$var" '%s' "false" || printf -v "$var" '%s' "true" ;; - false) [[ "$answer" =~ ^[Yy]$ ]] && printf -v "$var" '%s' "true" || printf -v "$var" '%s' "false" ;; - esac + local rc=0 + if [[ "$default" == "true" ]]; then + $GUM confirm --default=yes " $text" < /dev/tty || rc=$? + else + $GUM confirm " $text" < /dev/tty || rc=$? + fi + [[ $rc -eq 0 ]] && printf -v "$var" '%s' "true" || printf -v "$var" '%s' "false" } require_cmd() { command -v "$1" &>/dev/null || fail "$2"; } @@ -140,6 +276,54 @@ json_field() { jq -r ".$1" 2>/dev/null; } # URL-encode a string url_encode() { jq -rn --arg s "$1" '$s | @uri'; } +# ── Reusable helpers (DRY) ────────────────────────────────────────────────── + +# Animate a gum spinner with label for N seconds. +# Usage: animate_spinner

: pre-select permission profile (builder, account_assistant, personal_assistant) +# --simple / --advanced: pre-select install mode AUTO_YES=false PRESELECT_PACK="" PRESELECT_METHOD="" PRESELECT_PROFILE="" +INSTALL_MODE="" # "simple" or "advanced", empty = ask DEBUG_IN_REPO=false while [[ $# -gt 0 ]]; do case "$1" in --non-interactive|--yes|-y) AUTO_YES=true; shift ;; + --simple) INSTALL_MODE="simple"; shift ;; + --advanced) INSTALL_MODE="advanced"; shift ;; --pack) if [[ $# -lt 2 || "$2" == --* ]]; then echo -e "\033[0;31m✗\033[0m --pack requires a pack name (e.g. --pack openclaw, --pack claude-code)" >&2 @@ -119,6 +125,7 @@ DEPLOY_TERRAFORM=3 # Stamped at release; fall back to git info at runtime INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)}" INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$SCRIPT_DIR" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" +REPO_BRANCH="${REPO_BRANCH:-$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}" # Detect AWS CloudShell (limited ~1GB home dir, use /tmp for large files) IS_CLOUDSHELL=false @@ -196,7 +203,7 @@ install_gum() { # UI helpers # ============================================================================ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' -MAGENTA='\033[0;35m' +MAGENTA='\033[0;35m'; WHITE='\033[1;37m' info() { echo -e " ${BLUE}▸${NC} $1"; } ok() { echo -e " ${GREEN}✓${NC} $1"; } @@ -434,14 +441,13 @@ show_banner() { echo "" echo "" - echo -e " ${CYAN}██╗ ██████╗ ██╗ ██╗██╗${NC}" - echo -e " ${CYAN}██║ ██╔═══██╗██║ ██╔╝██║${NC}" - echo -e " ${BLUE}██║ ██║ ██║█████╔╝ ██║${NC}" - echo -e " ${BLUE}██║ ██║ ██║██╔═██╗ ██║${NC}" - echo -e " ${MAGENTA}███████╗╚██████╔╝██║ ██╗██║${NC}" - echo -e " ${MAGENTA}╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${NC}" + echo -e " ${CYAN} __ __ _ ${NC}" + echo -e " ${CYAN} / / ____ / /__ (_)${NC}" + echo -e " ${BLUE} / / / __ \\/ //_// / ${NC}" + echo -e " ${BLUE} / /___/ /_/ / ,< / / ${NC}" + echo -e " ${MAGENTA}/_____/\\____/_/|_/_/ ${NC}" echo "" - echo -e " ${BOLD}AWS Installer${NC} ${DIM}${version_line}${NC}" + echo -e " ${DIM}AWS Agent Installer ${version_line}${NC}" echo "" if [[ "$AUTO_YES" == true ]]; then local auto_msg="Running in non-interactive mode" @@ -480,13 +486,18 @@ preflight_checks() { echo "" echo -e " ${BOLD}Account:${NC} ${ACCOUNT_ID}" echo -e " ${BOLD}Region:${NC} ${REGION}" + echo -e " ${BOLD}Branch:${NC} ${REPO_BRANCH} ${DIM}(used by EC2 bootstrap)${NC}" echo "" - warn "Loki will get AdministratorAccess on this ENTIRE account." - warn "Use a dedicated sandbox account — never deploy in production." - echo "" - confirm_or_abort "Deploy to account ${ACCOUNT_ID} in ${REGION}?" "default_yes" - check_permissions + if [[ "$INSTALL_MODE" != "simple" ]]; then + warn "Loki will get AdministratorAccess on this ENTIRE account." + warn "Use a dedicated sandbox account — never deploy in production." + echo "" + confirm_or_abort "Deploy to account ${ACCOUNT_ID} in ${REGION}?" "default_yes" + check_permissions + else + ok "Using current account and region" + fi } check_vpc_quota() { @@ -584,8 +595,8 @@ check_existing_deployments() { # Offer to reuse an existing VPC instead of creating a new one local reuse_vpc=true - if [[ "$AUTO_YES" == true ]]; then - info "Auto mode: reusing first existing VPC" + if [[ "$AUTO_YES" == true || "$INSTALL_MODE" == "simple" ]]; then + info "Reusing existing VPC" else if ! confirm "Reuse an existing VPC?" "default_yes"; then reuse_vpc=false @@ -594,7 +605,7 @@ check_existing_deployments() { if [[ "$reuse_vpc" == true ]]; then local chosen_vpc - if [[ ${#vpc_ids[@]} -eq 1 || "$AUTO_YES" == true ]]; then + if [[ ${#vpc_ids[@]} -eq 1 || "$AUTO_YES" == true || "$INSTALL_MODE" == "simple" ]]; then chosen_vpc="${vpc_ids[0]}" info "Using VPC: ${chosen_vpc}" else @@ -729,36 +740,8 @@ choose_deploy_method() { fi # If Terraform selected and not installed, handle it now — before config questions. - # This avoids the user filling out all config only to be blocked at deploy time. if [[ "$DEPLOY_METHOD" == "$DEPLOY_TERRAFORM" ]]; then - if terraform_ok; then - ok "Terraform: $(terraform_version_string)" - else - if command -v terraform &>/dev/null; then - local tf_bin; tf_bin=$(file "$(command -v terraform)" 2>/dev/null || echo "") - local host; host=$(hw_arch) - if [[ ("$host" == "arm64" && "$tf_bin" != *"arm64"*) || ("$host" == "x86_64" && "$tf_bin" != *"x86_64"* && "$tf_bin" != *"x86-64"*) ]]; then - warn "Terraform $(terraform_version_string) is wrong architecture (need native ${host})." - else - warn "Terraform $(terraform_version_string) is too old (need >= 1.10)." - fi - else - warn "Terraform is not installed on this system." - fi - echo "" - echo " Loki can install Terraform locally now (no root/sudo required)." - echo " This works in AWS CloudShell, EC2, macOS, and most Linux environments." - echo "" - if confirm "Install Terraform locally before continuing?" "default_yes"; then - install_terraform - else - echo "" - echo " Install it manually, then re-run this installer:" - echo " https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli" - echo "" - fail "Terraform >= 1.10 is required." - fi - fi + ensure_terraform_available fi } @@ -812,42 +795,70 @@ choose_profile() { ok "Profile selected: ${PROFILE_NAME}" } -collect_config() { - step "Configuration" - - # ---- Pack selection (dynamically discovered from registry.json) ----------- - # CLONE_DIR may not be set yet (repo is cloned after config collection). - # If the local file isn't available, fetch from GitHub. - local registry="${CLONE_DIR:-}/packs/registry.json" - if [[ ! -f "$registry" ]]; then +# ============================================================================ +# Pack registry loading (shared by simple + advanced modes) +# ============================================================================ +_PACK_REGISTRY="" +PACK_NAMES=() +PACK_DESCS=() +PACK_EXPERIMENTAL=() + +load_pack_registry() { + _PACK_REGISTRY="${CLONE_DIR:-}/packs/registry.json" + if [[ ! -f "$_PACK_REGISTRY" ]]; then local registry_url="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/packs/registry.json" - registry="/tmp/loki-registry-$$.json" - curl -sfL "$registry_url" -o "$registry" 2>/dev/null || registry="" + _PACK_REGISTRY="/tmp/loki-registry-$$.json" + curl -sfL "$registry_url" -o "$_PACK_REGISTRY" 2>/dev/null || _PACK_REGISTRY="" fi - local -a pack_names=() - local -a pack_descs=() - local -a pack_experimental=() - - # Parse agent packs from registry.json via jq + PACK_NAMES=() + PACK_DESCS=() + PACK_EXPERIMENTAL=() while IFS='|' read -r pname pdesc pexp; do - pack_names+=("$pname") - pack_descs+=("$pdesc") - pack_experimental+=("$pexp") - done < <([ -n "$registry" ] && jq -r ' + PACK_NAMES+=("$pname") + PACK_DESCS+=("$pdesc") + PACK_EXPERIMENTAL+=("$pexp") + done < <([ -n "$_PACK_REGISTRY" ] && jq -r ' .packs | to_entries[] | select(.value.type == "agent") | "\(.key)|\(.value.description // .key)|\(if .value.experimental then "true" else "false" end)" - ' "$registry" 2>/dev/null \ + ' "$_PACK_REGISTRY" 2>/dev/null \ || echo "openclaw|OpenClaw -- stateful AI agent with persistent gateway|false") +} + +# ============================================================================ +# Install mode selection: simple (default) or advanced +# ============================================================================ +choose_install_mode() { + if [[ -n "$INSTALL_MODE" ]]; then + return # pre-selected via --simple or --advanced + fi + if [[ "$AUTO_YES" == true ]]; then + INSTALL_MODE="simple" + return + fi + local mode_choice + mode_choice=$($GUM choose --header "Install mode" \ + --selected "Simple — quick setup, smart defaults" \ + "Simple — quick setup, smart defaults" \ + "Advanced — full control over all settings" < /dev/tty) || mode_choice="" + case "$mode_choice" in + Simple*) INSTALL_MODE="simple" ;; + *) INSTALL_MODE="advanced" ;; + esac +} - # If pack was pre-selected via --pack, find and validate it +# ============================================================================ +# Shared: pack selection (used by both simple and advanced modes) +# ============================================================================ +choose_pack() { + # If pack was pre-selected via --pack, validate it if [[ -n "${PRESELECT_PACK}" ]]; then local found=false - for i in "${!pack_names[@]}"; do - if [[ "${pack_names[$i]}" == "${PRESELECT_PACK}" ]]; then - PACK_NAME="${pack_names[$i]}" + for i in "${!PACK_NAMES[@]}"; do + if [[ "${PACK_NAMES[$i]}" == "${PRESELECT_PACK}" ]]; then + PACK_NAME="${PACK_NAMES[$i]}" found=true - if [[ "${pack_experimental[$i]}" == "true" ]]; then + if [[ "${PACK_EXPERIMENTAL[$i]}" == "true" ]]; then warn "${PACK_NAME} is experimental — expect rough edges" fi ok "Pack pre-selected: ${PACK_NAME}" @@ -859,54 +870,118 @@ collect_config() { echo -e " ${RED}✗ Unknown pack: '${PRESELECT_PACK}'${NC}" echo "" echo " Available packs:" - for i in "${!pack_names[@]}"; do - echo " - ${pack_names[$i]}" + for i in "${!PACK_NAMES[@]}"; do + echo " - ${PACK_NAMES[$i]}" done echo "" fail "Pack '${PRESELECT_PACK}' not found. Use --pack with one of the packs listed above." fi + return fi - if [[ -z "${PRESELECT_PACK}" ]]; then - # Build display items for gum choose + # Interactive: build display items for gum choose local -a gum_items=() local default_item="" - for i in "${!pack_names[@]}"; do - local item="${pack_names[$i]} — ${pack_descs[$i]}" - [[ "${pack_experimental[$i]}" == "true" ]] && item+=" (experimental)" + for i in "${!PACK_NAMES[@]}"; do + local item="${PACK_NAMES[$i]} — ${PACK_DESCS[$i]}" + [[ "${PACK_EXPERIMENTAL[$i]}" == "true" ]] && item+=" (experimental)" gum_items+=("$item") - [[ "${pack_names[$i]}" == "openclaw" ]] && default_item="$item" + [[ "${PACK_NAMES[$i]}" == "openclaw" ]] && default_item="$item" done local pack_choice - pack_choice=$($GUM choose --header "Agent to deploy" \ + local header="${1:-Agent to deploy}" + pack_choice=$($GUM choose --header "$header" \ ${default_item:+--selected "$default_item"} \ "${gum_items[@]}" < /dev/tty) PACK_NAME="${pack_choice%% —*}" - for i in "${!pack_names[@]}"; do - if [[ "${pack_names[$i]}" == "$PACK_NAME" && "${pack_experimental[$i]}" == "true" ]]; then + for i in "${!PACK_NAMES[@]}"; do + if [[ "${PACK_NAMES[$i]}" == "$PACK_NAME" && "${PACK_EXPERIMENTAL[$i]}" == "true" ]]; then warn "${PACK_NAME} is experimental — expect rough edges" fi done - ok "Selected pack: ${PACK_NAME}" - fi # end of interactive pack selection - - # ---- Profile selection (REQUIRED) ---------------------------------------- - choose_profile + ok "Agent: ${PACK_NAME}" +} - # ---- Profile + Pack compatibility check ---------------------------------- +# Check pack/profile compatibility +check_pack_profile_compat() { if [[ "$PACK_NAME" == "nemoclaw" && "${PROFILE_NAME:-}" != "personal_assistant" ]]; then - echo "" - echo -e " ${RED}✗ NemoClaw is only compatible with the personal_assistant profile.${NC}" - echo "" - echo " NemoClaw runs the agent in an isolated sandbox that blocks all AWS API" - echo " access. The ${PROFILE_NAME} profile requires AWS access to function." - echo "" - echo " Options:" - echo " • Use --pack openclaw with --profile ${PROFILE_NAME}" - echo " • Use --pack nemoclaw with --profile personal_assistant" - echo "" - fail "Incompatible pack/profile combination: ${PACK_NAME} + ${PROFILE_NAME}" + if [[ "$INSTALL_MODE" == "simple" ]]; then + echo "" + echo -e " ${RED}✗ NemoClaw requires the personal_assistant profile.${NC}" + echo " Switching to personal_assistant automatically." + PROFILE_NAME="personal_assistant" + ok "Profile adjusted: ${PROFILE_NAME}" + else + echo "" + echo -e " ${RED}✗ NemoClaw is only compatible with the personal_assistant profile.${NC}" + echo "" + echo " NemoClaw runs the agent in an isolated sandbox that blocks all AWS API" + echo " access. The ${PROFILE_NAME} profile requires AWS access to function." + echo "" + echo " Options:" + echo " • Use --pack openclaw with --profile ${PROFILE_NAME}" + echo " • Use --pack nemoclaw with --profile personal_assistant" + echo "" + fail "Incompatible pack/profile combination: ${PACK_NAME} + ${PROFILE_NAME}" + fi fi +} + +# ============================================================================ +# Simple mode: pack + profile → auto-configure everything else +# ============================================================================ +collect_config_simple() { + step "Configuration (simple)" + + load_pack_registry + local registry="$_PACK_REGISTRY" + + choose_pack "Which agent do you want to deploy?" + choose_profile + check_pack_profile_compat + + # ---- Auto-configure everything else ---- + DEPLOY_REGION="${REGION:-us-east-1}" + DEPLOY_METHOD="$DEPLOY_TERRAFORM" + + # Instance type: profile determines size in simple mode + case "$PROFILE_NAME" in + builder) INSTANCE_TYPE="t4g.xlarge" ;; + account_assistant) INSTANCE_TYPE="t4g.medium" ;; + personal_assistant) INSTANCE_TYPE="t4g.medium" ;; + *) INSTANCE_TYPE="t4g.xlarge" ;; + esac + + # Environment name: auto-generate + local existing_count + existing_count=$(aws ec2 describe-vpcs \ + --filters "Name=tag:loki:managed,Values=true" \ + --region "$DEPLOY_REGION" \ + --query 'length(Vpcs)' --output text 2>/dev/null || echo "0") + local ts_suffix; ts_suffix=$(date +%s | tail -c 4) + ENV_NAME="${PACK_NAME}-$((existing_count + 1))-${ts_suffix}" + LOKI_WATERMARK="$ENV_NAME" + + # Security: all on for builder/account_assistant, all off for personal_assistant + case "$PROFILE_NAME" in + personal_assistant) + SECURITY_HUB="false"; GUARDDUTY="false"; INSPECTOR="false" + ACCESS_ANALYZER="false"; CONFIG_RECORDER="false" ;; + *) + SECURITY_HUB="true"; GUARDDUTY="true"; INSPECTOR="true" + ACCESS_ANALYZER="true"; CONFIG_RECORDER="true" ;; + esac +} + +collect_config() { + step "Configuration" + + load_pack_registry + local registry="$_PACK_REGISTRY" + + choose_pack + choose_profile + check_pack_profile_compat prompt "AWS region" DEPLOY_REGION "$REGION" @@ -979,7 +1054,7 @@ collect_config() { collect_security_config() { echo "" - echo -e " ${BOLD}Security services${NC} (~\$5/mo total, individually toggleable):" + echo -e " ${BOLD}Security services${NC} (~\$5/mo total):" echo "" if confirm "Enable all security services?" "default_yes"; then @@ -989,16 +1064,30 @@ collect_security_config() { return fi + # Multi-select: user picks which to enable echo "" - echo -e " Pick which to enable:" - echo "" - toggle "AWS Security Hub" SECURITY_HUB true - toggle "Amazon GuardDuty" GUARDDUTY true - toggle "Amazon Inspector" INSPECTOR true - toggle "IAM Access Analyzer" ACCESS_ANALYZER true - toggle "AWS Config Recorder" CONFIG_RECORDER true + local selected + selected=$($GUM choose --no-limit \ + --header "Select services to enable (space to toggle, enter to confirm)" \ + --selected "AWS Security Hub,Amazon GuardDuty,Amazon Inspector,IAM Access Analyzer,AWS Config Recorder" \ + "AWS Security Hub" \ + "Amazon GuardDuty" \ + "Amazon Inspector" \ + "IAM Access Analyzer" \ + "AWS Config Recorder" < /dev/tty) || selected="" + + SECURITY_HUB="false"; GUARDDUTY="false"; INSPECTOR="false" + ACCESS_ANALYZER="false"; CONFIG_RECORDER="false" + while IFS= read -r svc; do + case "$svc" in + "AWS Security Hub") SECURITY_HUB="true" ;; + "Amazon GuardDuty") GUARDDUTY="true" ;; + "Amazon Inspector") INSPECTOR="true" ;; + "IAM Access Analyzer") ACCESS_ANALYZER="true" ;; + "AWS Config Recorder") CONFIG_RECORDER="true" ;; + esac + done <<< "$selected" - echo "" local enabled="" [[ "$SECURITY_HUB" == "true" ]] && enabled+=" SecurityHub" [[ "$GUARDDUTY" == "true" ]] && enabled+=" GuardDuty" @@ -1012,8 +1101,8 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console, CFN CLI, Terraform # ============================================================================ # ⚠ KEEP THESE THREE ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId) -PARAM_TF_NAMES=(environment_name pack_name profile_name instance_type model_mode bedrock_region loki_watermark enable_bedrock_form enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder existing_vpc_id existing_subnet_id) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId RepoBranch) +PARAM_TF_NAMES=(environment_name pack_name profile_name instance_type model_mode bedrock_region loki_watermark enable_bedrock_form enable_security_hub enable_guardduty enable_inspector enable_access_analyzer enable_config_recorder existing_vpc_id existing_subnet_id repo_branch) PARAM_VALUES=() # populated by build_deploy_params() # Populate PARAM_VALUES from user config (call after collect_config) @@ -1034,6 +1123,7 @@ build_deploy_params() { "$CONFIG_RECORDER" "${EXISTING_VPC_ID:-}" "${EXISTING_SUBNET_ID:-}" + "$REPO_BRANCH" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ @@ -1075,22 +1165,58 @@ format_tf_vars() { show_summary() { step "Review & confirm" + local security_summary="" + if [[ "$SECURITY_HUB" == "true" && "$GUARDDUTY" == "true" && "$INSPECTOR" == "true" \ + && "$ACCESS_ANALYZER" == "true" && "$CONFIG_RECORDER" == "true" ]]; then + security_summary="all enabled" + elif [[ "$SECURITY_HUB" == "false" && "$GUARDDUTY" == "false" && "$INSPECTOR" == "false" \ + && "$ACCESS_ANALYZER" == "false" && "$CONFIG_RECORDER" == "false" ]]; then + security_summary="all disabled" + else + local enabled_list="" + [[ "$SECURITY_HUB" == "true" ]] && enabled_list+="Hub " + [[ "$GUARDDUTY" == "true" ]] && enabled_list+="Guard " + [[ "$INSPECTOR" == "true" ]] && enabled_list+="Inspector " + [[ "$ACCESS_ANALYZER" == "true" ]] && enabled_list+="Analyzer " + [[ "$CONFIG_RECORDER" == "true" ]] && enabled_list+="Config " + security_summary="${enabled_list:-none}" + fi + + local deploy_method_label="Terraform" + case "$DEPLOY_METHOD" in + "$DEPLOY_CFN_CLI") deploy_method_label="CloudFormation CLI" ;; + "$DEPLOY_CFN_CONSOLE") deploy_method_label="CloudFormation Console" ;; + esac + local summary="" - summary+="Environment ${ENV_NAME}\n" - summary+="Pack ${PACK_NAME}\n" + summary+="Branch ${REPO_BRANCH}\n" + summary+="Deploy via ${deploy_method_label}\n" + summary+="Account ${ACCOUNT_ID}\n" + summary+="Agent ${PACK_NAME}\n" summary+="Profile ${PROFILE_NAME}\n" summary+="Instance ${INSTANCE_TYPE}\n" summary+="Region ${DEPLOY_REGION}\n" - summary+="Watermark ${LOKI_WATERMARK}\n" [[ -n "${EXISTING_VPC_ID:-}" ]] && summary+="VPC reuse ${EXISTING_VPC_ID}\n" - summary+="\n" - summary+="Security Hub:${SECURITY_HUB} Guard:${GUARDDUTY} Inspector:${INSPECTOR}\n" - summary+=" Analyzer:${ACCESS_ANALYZER} Config:${CONFIG_RECORDER}" + summary+="Security ${security_summary}\n" + summary+="Environment ${ENV_NAME}" echo -e "$summary" | $GUM style --border rounded --border-foreground 117 \ --foreground 255 --padding "1 2" --margin "0 2" --bold echo "" - confirm_or_abort "Proceed with deployment?" "default_yes" + + # In simple mode, offer "Change settings" to switch to advanced + if [[ "$INSTALL_MODE" == "simple" && "$AUTO_YES" != true ]]; then + local action + action=$($GUM choose --header "Ready to deploy?" \ + "Deploy" \ + "Change settings (advanced mode)" < /dev/tty) || action="Deploy" + if [[ "$action" == *"Change settings"* ]]; then + INSTALL_MODE="advanced" + return 1 # signal to re-run config in advanced mode + fi + else + confirm_or_abort "Proceed with deployment?" "default_yes" + fi } # ============================================================================ @@ -1236,30 +1362,66 @@ deploy_cfn_stack() { } wait_for_cfn_stack() { - local iterations=0 max_iterations=120 # 120 x 15s = 30 minutes local start_time=$SECONDS + local seen_events="" + local max_wait=1800 # 30 minutes + while true; do - local status rc=0 - status=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$DEPLOY_REGION" \ + local elapsed=$(( SECONDS - start_time )) + if [[ $elapsed -ge $max_wait ]]; then + warn "Timed out after 30 minutes. Check the CloudFormation console for status." + break + fi + + # Fetch recent stack events (newest first), show unseen ones + local events_json + events_json=$(aws cloudformation describe-stack-events \ + --stack-name "$STACK_NAME" --region "$DEPLOY_REGION" \ + --query 'StackEvents[0:20].[EventId,LogicalResourceId,ResourceStatus,ResourceStatusReason]' \ + --output json 2>/dev/null) || true + + if [[ -n "$events_json" ]]; then + # Process events in reverse (oldest first) so they appear chronologically + local count + count=$(echo "$events_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + for (( i=count-1; i>=0; i-- )); do + local event_id resource status reason + event_id=$(echo "$events_json" | python3 -c "import sys,json; e=json.load(sys.stdin)[$i]; print(e[0])" 2>/dev/null) + [[ -z "$event_id" ]] && continue + # Skip already-seen events + if [[ "$seen_events" == *"$event_id"* ]]; then continue; fi + seen_events+=" $event_id" + + resource=$(echo "$events_json" | python3 -c "import sys,json; e=json.load(sys.stdin)[$i]; print(e[1])" 2>/dev/null) + status=$(echo "$events_json" | python3 -c "import sys,json; e=json.load(sys.stdin)[$i]; print(e[2])" 2>/dev/null) + reason=$(echo "$events_json" | python3 -c "import sys,json; e=json.load(sys.stdin)[$i]; print(e[3] or '')" 2>/dev/null) + + case "$status" in + *COMPLETE) echo -e " ${GREEN}✓${NC} ${resource} ${DIM}${status}${NC}" ;; + *IN_PROGRESS) echo -e " ${BLUE}+${NC} ${resource} ${DIM}${status}${NC}" ;; + *FAILED*|*ROLLBACK*) + echo -e " ${RED}✗${NC} ${resource} ${status}" + [[ -n "$reason" ]] && echo -e " ${RED}${reason}${NC}" + ;; + esac + done + fi + + # Check overall stack status + local stack_status rc=0 + stack_status=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$DEPLOY_REGION" \ --query 'Stacks[0].StackStatus' --output text 2>&1) || rc=$? if [[ $rc -ne 0 ]]; then - echo ""; fail "Stack no longer exists or is inaccessible: $status" + echo ""; fail "Stack no longer exists or is inaccessible: $stack_status" fi - local elapsed=$(( SECONDS - start_time )) + local elapsed_str; elapsed_str=$(elapsed_fmt $elapsed) - case "$status" in - CREATE_COMPLETE) printf "\r%-60s\r" ""; ok "Stack created! ${DIM}(${elapsed_str})${NC}"; break ;; - *FAILED*|*ROLLBACK*) printf "\r%-60s\r" ""; fail "Stack failed: $status" ;; - *) - iterations=$((iterations + 1)) - if [[ $iterations -ge $max_iterations ]]; then - printf "\r%-60s\r" "" - warn "Timed out after 30 minutes waiting for stack. Check the CloudFormation console for status." - break - fi - animate_spinner 15 "$status" - ;; + case "$stack_status" in + CREATE_COMPLETE) echo ""; ok "Stack created! ${DIM}(${elapsed_str})${NC}"; break ;; + *FAILED*|*ROLLBACK_COMPLETE) echo ""; fail "Stack failed: $stack_status" ;; esac + + sleep 10 done } @@ -1324,18 +1486,37 @@ install_terraform() { fi } -ensure_terraform() { +# Check terraform is available, correct arch, correct version — offer to install if not +ensure_terraform_available() { if terraform_ok; then ok "Terraform: $(terraform_version_string)" return 0 fi - # Should have been handled in choose_deploy_method, but install as a safety net - install_terraform + if command -v terraform &>/dev/null; then + local tf_bin; tf_bin=$(file "$(command -v terraform)" 2>/dev/null || echo "") + local host; host=$(hw_arch) + if [[ ("$host" == "arm64" && "$tf_bin" != *"arm64"*) || ("$host" == "x86_64" && "$tf_bin" != *"x86_64"* && "$tf_bin" != *"x86-64"*) ]]; then + warn "Terraform $(terraform_version_string) is wrong architecture (need native ${host})." + else + warn "Terraform $(terraform_version_string) is too old (need >= 1.10)." + fi + else + warn "Terraform is not installed on this system." + fi + echo "" + echo " Loki can install Terraform locally now (no root/sudo required)." + echo " This works in AWS CloudShell, EC2, macOS, and most Linux environments." + echo "" + if confirm "Install Terraform locally?" "default_yes"; then + install_terraform + else + fail "Terraform >= 1.10 is required." + fi } # ============================================================================ deploy_terraform() { dbg "deploy_terraform: pwd=$(pwd)" - ensure_terraform + ensure_terraform_available cd deploy/terraform dbg "deploy_terraform: cd done, pwd=$(pwd), .git exists=$(test -d ../../.git && echo yes || echo no)" setup_terraform_backend @@ -1384,9 +1565,14 @@ EOF } terraform_init() { - # AWS provider is ~500MB — CloudShell /home is ~1GB. Use /tmp for plugin cache. + # Persistent plugin cache avoids re-downloading providers on every install. + # CloudShell: /home is ~1GB so use /tmp. Elsewhere: use ~/.terraform.d/plugin-cache. if [[ -z "${TF_PLUGIN_CACHE_DIR:-}" ]]; then - export TF_PLUGIN_CACHE_DIR="/tmp/terraform-plugin-cache" + if [[ "$IS_CLOUDSHELL" == "true" ]]; then + export TF_PLUGIN_CACHE_DIR="/tmp/terraform-plugin-cache" + else + export TF_PLUGIN_CACHE_DIR="${HOME}/.terraform.d/plugin-cache" + fi fi mkdir -p "$TF_PLUGIN_CACHE_DIR" @@ -1407,13 +1593,34 @@ terraform_init() { fi fi - info "Initializing Terraform (downloading providers, may take a minute)..." - info "Plugin cache: ${TF_PLUGIN_CACHE_DIR}" - run_or_fail "Terraform init" terraform init -input=false - grep -E 'Initializing|Installing|Installed' "$_RUN_LOG" | while IFS= read -r line; do - echo -e " ${BLUE}…${NC} ${line}" - done - rm -f "$_RUN_LOG" + info "Initializing Terraform (downloading providers)..." + dbg "run_or_fail: Terraform init -> terraform init -input=false" + local _init_log="/tmp/loki-tf-init-$$.log" + : > "$_init_log" + terraform init -input=false > "$_init_log" 2>&1 & + local tf_pid=$! + # Stream log in foreground (interruptible by Ctrl-C) + tail -f "$_init_log" 2>/dev/null | while IFS= read -r line; do + if [[ "$line" == *"Installing"* ]]; then echo -e " ${BLUE}▸${NC} ${line#"- "}" + elif [[ "$line" == *"Installed"* ]]; then echo -e " ${GREEN}✓${NC} ${line#"- "}" + elif [[ "$line" == *"Initializing"* ]]; then echo -e " ${DIM}${line}${NC}" + elif [[ "$line" == *"Error"* || "$line" == *"error"* ]]; then echo -e " ${RED}${line}${NC}" + fi + # Stop tailing once terraform exits + kill -0 $tf_pid 2>/dev/null || break + done & + local tail_pid=$! + local rc=0 + wait $tf_pid || rc=$? + kill $tail_pid 2>/dev/null; wait $tail_pid 2>/dev/null || true + { echo "=== Terraform init (rc=$rc) ==="; cat "$_init_log"; echo ""; } >> "$INSTALL_LOG" 2>/dev/null + if [[ $rc -ne 0 ]]; then + warn "Terraform init failed:" + tail -20 "$_init_log" | $GUM format -t code + rm -f "$_init_log" + fail "Terraform init exited with code $rc" + fi + rm -f "$_init_log" ok "Terraform initialized" } @@ -1434,21 +1641,29 @@ terraform_apply() { while IFS= read -r v; do tf_vars+=("$v") done < <(format_tf_vars) - # Stream terraform apply live — show progress as resources are created + # Stream terraform apply live — run in background so Ctrl-C works _TF_LOG="/tmp/loki-terraform-apply.log" - local rc=0 - terraform apply -auto-approve "${tf_vars[@]}" 2>&1 | tee "$_TF_LOG" | while IFS= read -r line; do + : > "$_TF_LOG" + terraform apply -auto-approve "${tf_vars[@]}" > "$_TF_LOG" 2>&1 & + local tf_pid=$! + # Stream log in background, filter for interesting lines + tail -f "$_TF_LOG" 2>/dev/null | while IFS= read -r line; do if [[ "$line" == *": Creating..."* ]]; then echo -e " ${BLUE}+${NC} ${line##*] }" elif [[ "$line" == *": Creation complete"* ]]; then echo -e " ${GREEN}✓${NC} ${line##*] }" elif [[ "$line" == *"Apply complete"* ]]; then echo -e "\n ${GREEN}${line}${NC}" elif [[ "$line" == *"Outputs:"* ]] || [[ "$line" == *" = "* ]]; then echo " $line" elif [[ "$line" == *"Error"* || "$line" == *"error"* ]]; then echo -e " ${RED}${line}${NC}" fi - done || rc=$? + kill -0 $tf_pid 2>/dev/null || break + done & + local tail_pid=$! + local rc=0 + wait $tf_pid || rc=$? + kill $tail_pid 2>/dev/null; wait $tail_pid 2>/dev/null || true + { echo "=== Terraform apply (rc=$rc) ==="; cat "$_TF_LOG"; echo ""; } >> "$INSTALL_LOG" 2>/dev/null if [[ $rc -ne 0 ]]; then echo "" warn "Terraform apply failed (exit code $rc)" - # Show last 40 lines in a formatted code block via gum local err_text err_text=$(tail -40 "$_TF_LOG") echo "$err_text" | $GUM format -t code @@ -1459,15 +1674,27 @@ terraform_apply() { # ============================================================================ # Ensure Loki-Session SSM document exists (instance-scoped, not account-wide) ensure_ssm_session_document() { + local doc_content='{"schemaVersion":"1.0","description":"SSM session for Loki - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"loki tui || exec bash --login\""}}}' + if aws ssm describe-document --name "$SSM_DOC_NAME" --region "$DEPLOY_REGION" &>/dev/null; then - ok "SSM session document: ${SSM_DOC_NAME}" + # Update existing document to latest version + aws ssm update-document \ + --name "$SSM_DOC_NAME" \ + --content "$doc_content" \ + --document-version '$LATEST' \ + --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + aws ssm update-document-default-version \ + --name "$SSM_DOC_NAME" \ + --document-version '$LATEST' \ + --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + ok "SSM session document: ${SSM_DOC_NAME} (updated)" return 0 fi - info "Creating ${SSM_DOC_NAME} SSM document (starts sessions as ec2-user)..." + info "Creating ${SSM_DOC_NAME} SSM document..." aws ssm create-document \ --name "$SSM_DOC_NAME" \ --document-type "Session" \ - --content '{"schemaVersion":"1.0","description":"SSM session for Loki - starts as ec2-user","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && exec bash --login"}}}' \ + --content "$doc_content" \ --region "$DEPLOY_REGION" >/dev/null 2>&1 || { warn "Could not create ${SSM_DOC_NAME} document (may need ssm:CreateDocument permission)" info "Connect with: aws ssm start-session --target \${INSTANCE_ID} --region \${DEPLOY_REGION}" @@ -1642,21 +1869,47 @@ show_complete() { # ============================================================================ # Main # ============================================================================ -main() { - install_gum # must run before anything that uses $GUM - show_banner - preflight_checks # step 1 - choose_deploy_method # step 2 - collect_config # step 3 - check_existing_deployments # Must run AFTER collect_config so DEPLOY_REGION is set - # Skip VPC quota check when reusing an existing VPC +run_config_and_review() { + if [[ "$INSTALL_MODE" == "simple" ]]; then + # Simple mode: pack + profile, then auto-configure, then terraform check + collect_config_simple + ensure_terraform_available + # Auto-detect VPC reuse + check_existing_deployments + else + # Advanced mode: full interactive flow + choose_deploy_method + collect_config + check_existing_deployments + fi + + # VPC quota check (skip if reusing) if [[ -z "${EXISTING_VPC_ID:-}" ]]; then - check_vpc_quota # Run after collect_config so we use DEPLOY_REGION + check_vpc_quota else ok "Skipping VPC quota check (reusing existing VPC ${EXISTING_VPC_ID})" fi - build_deploy_params # Populate parameter arrays from user config - show_summary # step 4 + + build_deploy_params + show_summary || { + # User chose "Change settings" → re-run in advanced mode with current values as preselects + PRESELECT_PACK="$PACK_NAME" + PRESELECT_PROFILE="$PROFILE_NAME" + PRESELECT_METHOD="terraform" + EXISTING_VPC_ID="" + EXISTING_SUBNET_ID="" + STEP_NUM=1 + run_config_and_review + return + } +} + +main() { + install_gum # must run before anything that uses $GUM + show_banner + choose_install_mode # simple (default) or advanced — needed before preflight + preflight_checks # step 1 + run_config_and_review # steps 2-4 (config → review) # Console deploy exits early (no clone, no bootstrap wait) if [[ "$DEPLOY_METHOD" == "$DEPLOY_CFN_CONSOLE" ]]; then diff --git a/packs/openclaw/install.sh b/packs/openclaw/install.sh index ba5cc8f..c957615 100755 --- a/packs/openclaw/install.sh +++ b/packs/openclaw/install.sh @@ -115,6 +115,22 @@ fi OC_VERSION="$(openclaw --version 2>/dev/null || echo unknown)" ok "OpenClaw installed: ${OC_VERSION}" +# ── Patch pi-coding-agent for AWS SDK (instance profile) auth ──────────────── +# pi-coding-agent's auth pre-flight rejects AWS SDK auth when no API key is set +# (EC2 instance roles use IMDS, not env vars). Patch two files: +# 1. model-registry.js: hasConfiguredAuth() must return true for amazon-bedrock +# 2. agent-session.js: _getRequiredRequestAuth() must allow undefined apiKey for bedrock +# These patches will be overwritten on OpenClaw update — upstream fix needed. +step "Patching pi-coding-agent for Bedrock instance-profile auth" + +PATCH_SCRIPT="${SCRIPT_DIR}/resources/patch-pi-agent.py" +if [[ -f "${PATCH_SCRIPT}" ]]; then + python3 "${PATCH_SCRIPT}" "${NODE_PREFIX}" && ok "pi-coding-agent patched for Bedrock auth" \ + || warn "pi-coding-agent patch had warnings (see above)" +else + warn "patch-pi-agent.py not found — skipping pi-coding-agent patches" +fi + # ── Workspace and state dir ─────────────────────────────────────────────────── step "Workspace setup" mkdir -p "${HOME}/.openclaw/workspace" @@ -167,6 +183,7 @@ fi export NODE_BIN OC_MAIN GW_PORT GW_TOKEN NODE_PREFIX OC_VERSION export USER_HOME="${HOME}" +export AWS_DEFAULT_REGION="${REGION}" envsubst < "${SERVICE_TPL}" > "${HOME}/.config/systemd/user/openclaw-gateway.service" chmod 600 "${HOME}/.config/systemd/user/openclaw-gateway.service" ok "Service unit written" diff --git a/packs/openclaw/resources/config-gen.py b/packs/openclaw/resources/config-gen.py index f8edde7..2546197 100644 --- a/packs/openclaw/resources/config-gen.py +++ b/packs/openclaw/resources/config-gen.py @@ -19,7 +19,8 @@ provider_key = os.environ.get("PROVIDER_KEY_ENV") or (sys.argv[9] if len(sys.argv) > 9 else "") home = os.path.expanduser("~") cfg = { - "models": {"providers": {"amazon-bedrock": {"baseUrl": f"https://bedrock-runtime.{bedrock_region}.amazonaws.com", "auth": "aws-sdk", "api": "bedrock-converse-stream", "models": []}}, "bedrockDiscovery": {"enabled": True, "region": "us-east-1", "providerFilter": ["anthropic"]}}, + "models": {"providers": {"amazon-bedrock": {"baseUrl": f"https://bedrock-runtime.{bedrock_region}.amazonaws.com", "auth": "aws-sdk", "api": "bedrock-converse-stream", "models": []}}}, + "plugins": {"entries": {"amazon-bedrock": {"config": {"discovery": {"enabled": True, "region": bedrock_region, "providerFilter": ["anthropic"]}}}}}, "agents": {"defaults": {"model": {"primary": f"amazon-bedrock/{model}", "fallbacks": ["amazon-bedrock/us.anthropic.claude-sonnet-4-6"]}, "workspace": f"{home}/.openclaw/workspace", "compaction": {"mode": "safeguard"}, "heartbeat": {"model": f"amazon-bedrock/us.anthropic.claude-sonnet-4-6", "target": "telegram", "every": "30m", "lightContext": True, "isolatedSession": True}, "maxConcurrent": 4, "subagents": {"maxConcurrent": 8}}}, "tools": {"web": {"search": {"enabled": False}, "fetch": {"enabled": True}}}, "hooks": {"internal": {"enabled": True, "entries": {"boot-md": {"enabled": True}, "bootstrap-extra-files": {"enabled": True}, "command-logger": {"enabled": True}, "session-memory": {"enabled": True}}}}, diff --git a/packs/openclaw/resources/openclaw-gateway.service.tpl b/packs/openclaw/resources/openclaw-gateway.service.tpl index 5a305c7..3c4dc65 100644 --- a/packs/openclaw/resources/openclaw-gateway.service.tpl +++ b/packs/openclaw/resources/openclaw-gateway.service.tpl @@ -10,6 +10,8 @@ RestartSec=5 KillMode=process Environment="HOME=${USER_HOME}" Environment="PATH=${USER_HOME}/.local/bin:${USER_HOME}/.local/share/mise/installs/node/current/bin:${NODE_PREFIX}/bin:/usr/local/bin:/usr/bin:/bin" +Environment=AWS_PROFILE=default +Environment=AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} Environment=OPENCLAW_GATEWAY_PORT=${GW_PORT} Environment=OPENCLAW_GATEWAY_TOKEN=${GW_TOKEN} Environment=OPENCLAW_SYSTEMD_UNIT=openclaw-gateway.service diff --git a/packs/openclaw/resources/patch-pi-agent.py b/packs/openclaw/resources/patch-pi-agent.py new file mode 100644 index 0000000..b8778cd --- /dev/null +++ b/packs/openclaw/resources/patch-pi-agent.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Patch pi-coding-agent for AWS SDK (instance profile) auth on Bedrock. + +pi-coding-agent's auth pre-flight rejects AWS SDK auth when no API key is +present (EC2 instance roles use IMDS, not env vars / API keys). This script +patches two files in the installed dist: + + 1. model-registry.js — hasConfiguredAuth() returns true for amazon-bedrock + 2. agent-session.js — _getRequiredRequestAuth() allows undefined apiKey + +Usage: python3 patch-pi-agent.py + +These patches are overwritten on OpenClaw update. Upstream fix tracked at: + OpenClaw auth-controller.ts should inject AWS SDK auth into pi's authStorage. +""" + +import re +import sys +from pathlib import Path + +MARKER = "/* LOKI-PATCH-BEDROCK-AUTH */" + + +def patch_model_registry(filepath: Path) -> bool: + """Make hasConfiguredAuth() return true for amazon-bedrock provider.""" + if not filepath.exists(): + print(f" [SKIP] {filepath} not found") + return True + + text = filepath.read_text() + if MARKER in text: + print(f" [OK] model-registry.js already patched") + return True + + # Find hasConfiguredAuth() method and inject bedrock check before the return + # Pattern: hasConfiguredAuth() { ... return ; } + pattern = r'(hasConfiguredAuth\s*\(\)\s*\{)' + replacement = ( + r'\1\n' + f' {MARKER}\n' + ' if (this.providerId === "amazon-bedrock") return true;\n' + ) + new_text, count = re.subn(pattern, replacement, text, count=1) + if count == 0: + print(f" [WARN] Could not find hasConfiguredAuth() in model-registry.js") + return False + + filepath.write_text(new_text) + print(f" [OK] Patched model-registry.js (hasConfiguredAuth)") + return True + + +def patch_agent_session(filepath: Path) -> bool: + """Allow undefined apiKey for amazon-bedrock in _getRequiredRequestAuth().""" + if not filepath.exists(): + print(f" [SKIP] {filepath} not found") + return True + + text = filepath.read_text() + if MARKER in text: + print(f" [OK] agent-session.js already patched") + return True + + # Find _getRequiredRequestAuth and inject bedrock early-return + # The method fetches auth and then throws if no apiKey — we intercept before the throw + pattern = r'(_getRequiredRequestAuth\s*\([^)]*\)\s*\{)' + replacement = ( + r'\1\n' + f' {MARKER}\n' + ' const _bedrockProvider = this.providerId || this._providerId || "";\n' + ' if (_bedrockProvider === "amazon-bedrock") {\n' + ' const _bAuth = await this._getRequestAuth?.() || { ok: true, headers: {} };\n' + ' return { apiKey: undefined, headers: _bAuth.headers || {} };\n' + ' }\n' + ) + new_text, count = re.subn(pattern, replacement, text, count=1) + if count == 0: + # Try alternate pattern — async method + pattern2 = r'(async\s+_getRequiredRequestAuth\s*\([^)]*\)\s*\{)' + replacement2 = ( + r'\1\n' + f' {MARKER}\n' + ' const _bedrockProvider = this.providerId || this._providerId || "";\n' + ' if (_bedrockProvider === "amazon-bedrock") {\n' + ' const _bAuth = await this._getRequestAuth?.() || { ok: true, headers: {} };\n' + ' return { apiKey: undefined, headers: _bAuth.headers || {} };\n' + ' }\n' + ) + new_text, count = re.subn(pattern2, replacement2, text, count=1) + + if count == 0: + print(f" [WARN] Could not find _getRequiredRequestAuth() in agent-session.js") + return False + + filepath.write_text(new_text) + print(f" [OK] Patched agent-session.js (_getRequiredRequestAuth)") + return True + + +def main(): + if len(sys.argv) < 2: + print("Usage: patch-pi-agent.py ", file=sys.stderr) + sys.exit(1) + + node_prefix = Path(sys.argv[1]) + pi_dist = node_prefix / "lib/node_modules/openclaw/node_modules/@mariozechner/pi-coding-agent/dist/core" + + if not pi_dist.exists(): + # Try alternate location (hoisted deps) + pi_dist_alt = node_prefix / "lib/node_modules/@mariozechner/pi-coding-agent/dist/core" + if pi_dist_alt.exists(): + pi_dist = pi_dist_alt + else: + print(f" [WARN] pi-coding-agent dist not found at {pi_dist}") + sys.exit(0) # non-fatal + + ok1 = patch_model_registry(pi_dist / "model-registry.js") + ok2 = patch_agent_session(pi_dist / "agent-session.js") + + if not (ok1 and ok2): + sys.exit(1) + + +if __name__ == "__main__": + main() From b9fb943c3712ac8767cd8bc88ba1217e24105904 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Apr 2026 16:01:34 +0000 Subject: [PATCH 142/172] ci: bump version to 0.5.63 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index c0dd5a4..a3a12d5 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.62" +INSTALLER_VERSION="0.5.63" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 38a13bd8fbf0e4e7624852f2d57de0ffff48ca6c Mon Sep 17 00:00:00 2001 From: Loki FastStart Date: Tue, 7 Apr 2026 09:11:19 +0000 Subject: [PATCH 143/172] Update README title to HN-friendly format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a73a066..6240372 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Loki: Turn OpenClaw and friends into FullStack Builder Agents in your AWS account. +# Self-Hosted AI Coding Agents on AWS — OpenClaw, Claude Code, Kiro, NemoClaw, Hermes [![Loki Agent Demo](https://img.youtube.com/vi/dJSk8DYlHvI/maxresdefault.jpg)](https://www.youtube.com/watch?v=dJSk8DYlHvI) ▶️ [Watch the full walkthrough on YouTube](https://www.youtube.com/watch?v=dJSk8DYlHvI) From 68239717f3e4ba649857802774635339052b1a7f Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 22:27:03 +0300 Subject: [PATCH 144/172] fix: detached HEAD fallback, SSM doc version, upstream fixes doc - REPO_BRANCH falls back to main when git returns HEAD (detached checkout) - SSM update-document-default-version uses numeric version from update-document response - Added upstream-fixes.md documenting OpenClaw/pi Bedrock auth issues - Tests for both fixes Co-Authored-By: Claude Opus 4.6 --- deploy/test-templates.sh | 67 +++++++++++++++++++++++++++++++ install.sh | 19 +++++---- upstream-fixes.md | 87 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 upstream-fixes.md diff --git a/deploy/test-templates.sh b/deploy/test-templates.sh index bc3b7d7..c2eff39 100644 --- a/deploy/test-templates.sh +++ b/deploy/test-templates.sh @@ -95,6 +95,73 @@ check_contains "$INSTALL_SH" 'PackName' "install.sh: PackName in PARAM_CFN_NAMES check_contains "$INSTALL_SH" 'pack_name' "install.sh: pack_name in PARAM_TF_NAMES" check_contains "$INSTALL_SH" 't4g.medium' "install.sh: hermes default size logic present" +# ── Branch detection & SSM doc version ────────────────────────────────────── +echo -e "${BOLD}Branch & SSM fixes${NC}" +check_contains "$INSTALL_SH" '[[ "$REPO_BRANCH" == "HEAD" ]]' "install.sh: detached HEAD falls back to main" +check_contains "$INSTALL_SH" 'REPO_BRANCH=' "install.sh: REPO_BRANCH is set" +check_contains "$TF_VARS" 'variable "repo_branch"' "TF: repo_branch variable defined" +check_contains "$TF_MAIN" "repo_branch" "TF main: repo_branch passed to userdata template" +check_contains "$TF_USERDATA" 'repo_branch' "TF userdata: uses repo_branch for git clone" +check_contains "$CFN_TEMPLATE" "RepoBranch" "CFN: RepoBranch parameter defined" +check_contains "$INSTALL_SH" "DocumentDescription.DocumentVersion" "install.sh: SSM update-document captures numeric version" +check_contains "$INSTALL_SH" 'new_version' "install.sh: SSM update-document-default-version uses captured version" + +echo "" + +# ── Branch detection unit tests ───────────────────────────────────────────── +echo -e "${BOLD}Branch detection (unit)${NC}" + +# Test: detached HEAD → main +_test_branch="HEAD" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "main" ]]; then + pass "Detached HEAD resolves to main" +else + fail "Detached HEAD should resolve to main, got: $_test_branch" +fi + +# Test: normal branch → unchanged +_test_branch="installer-ux-overhaul" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "installer-ux-overhaul" ]]; then + pass "Normal branch name preserved" +else + fail "Normal branch should be preserved, got: $_test_branch" +fi + +# Test: main → unchanged +_test_branch="main" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "main" ]]; then + pass "main branch preserved" +else + fail "main should be preserved, got: $_test_branch" +fi + +# Test: SSM version regex accepts numeric +_test_version="5" +if [[ "$_test_version" =~ ^[0-9]+$ ]]; then + pass "SSM version regex accepts numeric version" +else + fail "SSM version regex should accept '5'" +fi + +# Test: SSM version regex rejects $LATEST +_test_version='$LATEST' +if [[ "$_test_version" =~ ^[0-9]+$ ]]; then + fail "SSM version regex should reject '\$LATEST'" +else + pass "SSM version regex rejects \$LATEST" +fi + +# Test: SSM version regex rejects empty +_test_version="" +if [[ -n "$_test_version" && "$_test_version" =~ ^[0-9]+$ ]]; then + fail "SSM version regex should reject empty string" +else + pass "SSM version regex rejects empty string" +fi + echo "" echo -e "${BOLD}─────────────────────────────────────────────────${NC}" echo -e " Passed: ${GREEN}${PASS}${NC} Failed: ${RED}${FAIL}${NC}" diff --git a/install.sh b/install.sh index a3a12d5..89903e4 100755 --- a/install.sh +++ b/install.sh @@ -126,6 +126,7 @@ DEPLOY_TERRAFORM=3 INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)}" INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$SCRIPT_DIR" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" REPO_BRANCH="${REPO_BRANCH:-$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}" +[[ "$REPO_BRANCH" == "HEAD" ]] && REPO_BRANCH="main" # Detect AWS CloudShell (limited ~1GB home dir, use /tmp for large files) IS_CLOUDSHELL=false @@ -1677,16 +1678,20 @@ ensure_ssm_session_document() { local doc_content='{"schemaVersion":"1.0","description":"SSM session for Loki - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"loki tui || exec bash --login\""}}}' if aws ssm describe-document --name "$SSM_DOC_NAME" --region "$DEPLOY_REGION" &>/dev/null; then - # Update existing document to latest version - aws ssm update-document \ + # Update existing document and set new version as default + local new_version + new_version=$(aws ssm update-document \ --name "$SSM_DOC_NAME" \ --content "$doc_content" \ --document-version '$LATEST' \ - --region "$DEPLOY_REGION" >/dev/null 2>&1 || true - aws ssm update-document-default-version \ - --name "$SSM_DOC_NAME" \ - --document-version '$LATEST' \ - --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + --region "$DEPLOY_REGION" \ + --query 'DocumentDescription.DocumentVersion' --output text 2>/dev/null) || true + if [[ -n "$new_version" && "$new_version" =~ ^[0-9]+$ ]]; then + aws ssm update-document-default-version \ + --name "$SSM_DOC_NAME" \ + --document-version "$new_version" \ + --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + fi ok "SSM session document: ${SSM_DOC_NAME} (updated)" return 0 fi diff --git a/upstream-fixes.md b/upstream-fixes.md new file mode 100644 index 0000000..8eb1ac1 --- /dev/null +++ b/upstream-fixes.md @@ -0,0 +1,87 @@ +# Upstream Fixes Needed: OpenClaw + pi-coding-agent Bedrock Auth + +OpenClaw + pi-coding-agent fail when using AWS Bedrock with EC2 instance-profile auth (IMDS). +The AWS SDK credential chain works fine, but the auth pre-flight checks reject it before any request is made. + +We currently patch these at install time (`packs/openclaw/resources/patch-pi-agent.py`), but +these patches are overwritten on every OpenClaw update. + +## Issue 1: pi-coding-agent `hasConfiguredAuth()` rejects AWS SDK auth + +**File:** `src/core/model-registry.ts` (dist: `dist/core/model-registry.js`) + +**Problem:** `hasConfiguredAuth()` only checks `authStorage.hasAuth()` and `providerRequestConfigs.apiKey`. Neither is set for AWS SDK auth via instance profile. This causes "No API key found for amazon-bedrock" before any API request is made. + +**Fix:** Return `true` when the provider is `amazon-bedrock` and the config uses `auth: "aws-sdk"`. + +```typescript +// In hasConfiguredAuth(): +if (this.providerId === "amazon-bedrock") return true; +``` + +**Why:** Bedrock auth uses AWS SDK signing (SigV4), not API keys. The SDK resolves credentials from the instance metadata service (IMDS) at request time. There's no key to pre-check. + +--- + +## Issue 2: pi-coding-agent `_getRequiredRequestAuth()` throws on undefined apiKey + +**File:** `src/core/agent-session.ts` (dist: `dist/core/agent-session.js`) + +**Problem:** `_getRequiredRequestAuth()` throws when `getApiKeyAndHeaders()` returns `ok: true` but no `apiKey`. Bedrock uses AWS SDK signing (no API key needed), but this code path doesn't account for it. + +**Fix:** Early return for `amazon-bedrock` provider with `{ apiKey: undefined, headers }`. + +```typescript +// In _getRequiredRequestAuth(): +if (this.providerId === "amazon-bedrock") { + const authResult = await this._getRequestAuth?.() || { ok: true, headers: {} }; + return { apiKey: undefined, headers: authResult.headers || {} }; +} +``` + +**Why:** The Bedrock provider adapter handles auth via the AWS SDK at the HTTP layer (SigV4 signing). It never needs an API key injected by the session. + +--- + +## Issue 3 (Root Cause): OpenClaw auth-controller doesn't inject SDK auth into pi's authStorage + +**File:** `src/agents/pi-embedded-runner/run/auth-controller.ts` + +**Problem:** OpenClaw's auth controller correctly resolves AWS SDK auth (returns `{ mode: "aws-sdk", source: "..." }` from `resolveAwsSdkAuthInfo()`), but does an early return at ~line 329-337 without calling `setRuntimeApiKey()`. This means pi's `authStorage` never learns about Bedrock credentials. + +**Proper fix:** When auth mode is `aws-sdk`, the auth controller should either: +1. Call `setRuntimeApiKey()` with a sentinel value that pi recognizes as "use SDK signing", or +2. Set a flag on the provider config that pi checks in `hasConfiguredAuth()` and `_getRequiredRequestAuth()` + +This would eliminate the need for patches #1 and #2. + +--- + +## Issue 4: `resolveAwsSdkEnvVarName()` doesn't detect IMDS + +**File:** `src/agents/model-auth-runtime-shared.ts` + +**Problem:** `resolveAwsSdkEnvVarName()` checks for `AWS_BEARER_TOKEN_BEDROCK`, `AWS_ACCESS_KEY_ID`+`AWS_SECRET_ACCESS_KEY`, and `AWS_PROFILE`. If none are set, it returns `undefined`. EC2 instances with IAM roles don't need any env vars — the SDK discovers credentials via IMDS automatically. + +**Current workaround:** We set `AWS_PROFILE=default` in `/etc/profile.d/` and the systemd service unit, which triggers the env var check and lets the SDK fall through to IMDS. + +**Proper fix:** The fallback in `resolveAwsSdkAuthInfo()` already returns `{ mode: "aws-sdk", source: "aws-sdk default chain" }` when no env vars are found — this is correct. The issue is upstream in the auth controller (Issue 3) not propagating this to pi. + +--- + +## Environment + +- OpenClaw version: latest (as of 2026-04-06) +- pi-coding-agent version: 0.65.0 +- Platform: Amazon Linux 2023 on EC2 with IAM instance profile +- Auth method: Bedrock via IMDS (no API keys, no env vars) +- Config: `models.providers.amazon-bedrock.auth: "aws-sdk"` + +## Reproduction + +1. Launch EC2 instance with IAM role that has Bedrock access +2. Install OpenClaw: `npm install -g openclaw` +3. Configure with `auth: "aws-sdk"` for amazon-bedrock provider +4. Run `openclaw tui` — fails with "No API key found for amazon-bedrock" +5. Set `AWS_PROFILE=default` — still fails (auth pre-flight in pi rejects it) +6. Apply patches to model-registry.js and agent-session.js — works From 99b20eaffac4d1854271cd4fbb217fcfb3843afa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 19:27:13 +0000 Subject: [PATCH 145/172] ci: bump version to 0.5.64 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 89903e4..3f2369b 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.63" +INSTALLER_VERSION="0.5.64" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 9eec3773779801540ce4a946c4c9bc15fcb2dfc8 Mon Sep 17 00:00:00 2001 From: Loki Agent Date: Tue, 7 Apr 2026 22:43:18 +0300 Subject: [PATCH 146/172] Fix ironclaw pack: pg_hba TCP auth, systemd EnvironmentFile, --no-onboard (#7) * Fix ironclaw pack: pg_hba TCP auth, systemd EnvironmentFile, --no-onboard - Add TCP trust auth in pg_hba.conf for 127.0.0.1 (IronClaw uses DATABASE_URL with TCP) - Write systemd unit with EnvironmentFile pointing to ~/.ironclaw/.env - Add --no-onboard to ExecStart to skip interactive wizard in daemon mode - Enable user lingering for boot-start * Fix section ordering: systemd block before Done marker --------- Co-authored-by: Loki FastStart --- packs/ironclaw/install.sh | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 1221433..9610509 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -157,10 +157,15 @@ fi # Configure pg_hba.conf for local peer authentication (ec2-user can connect) PG_HBA="${PG_DATA}/pg_hba.conf" if ! sudo grep -q "^local.*${IC_DB_NAME}.*${IC_DB_USER}" "${PG_HBA}" 2>/dev/null; then - log "Configuring pg_hba.conf for local peer auth..." + log "Configuring pg_hba.conf for local peer + TCP trust auth..." # Add before the first 'local' line: allow ec2-user to connect to ironclaw db sudo sed -i "/^local/i local ${IC_DB_NAME} ${IC_DB_USER} peer" "${PG_HBA}" 2>/dev/null \ || echo "local ${IC_DB_NAME} ${IC_DB_USER} peer" | sudo tee -a "${PG_HBA}" >/dev/null + # IronClaw connects via TCP (DATABASE_URL=postgresql://...@localhost) — need trust for 127.0.0.1 + if ! sudo grep -q "^host.*${IC_DB_NAME}.*${IC_DB_USER}.*trust" "${PG_HBA}" 2>/dev/null; then + sudo sed -i "/^host.*all.*all.*127.0.0.1/i host ${IC_DB_NAME} ${IC_DB_USER} 127.0.0.1/32 trust" "${PG_HBA}" 2>/dev/null \ + || echo "host ${IC_DB_NAME} ${IC_DB_USER} 127.0.0.1/32 trust" | sudo tee -a "${PG_HBA}" >/dev/null + fi fi # Start PostgreSQL @@ -327,6 +332,37 @@ else warn "pgvector extension not detected — semantic search may not work" fi +# ── Fix systemd service unit ───────────────────────────────────────────────── +step "Configuring systemd service" + +SYSTEMD_USER_DIR="${HOME}/.config/systemd/user" +mkdir -p "${SYSTEMD_USER_DIR}" + +cat > "${SYSTEMD_USER_DIR}/ironclaw.service" </dev/null || true + +# Reload and enable (but dont start — let bootstrap handle that) +systemctl --user daemon-reload 2>/dev/null || true +systemctl --user enable ironclaw 2>/dev/null || true +ok "Systemd service installed with EnvironmentFile + --no-onboard" + # ── Done ───────────────────────────────────────────────────────────────────── write_done_marker "ironclaw" printf "\n[PACK:ironclaw] INSTALLED — ironclaw CLI ready\n" From 7a2e76345e2687aae6935b7bbdf2abf98139db59 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 19:43:28 +0000 Subject: [PATCH 147/172] ci: bump version to 0.5.65 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3f2369b..bb494d4 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.64" +INSTALLER_VERSION="0.5.65" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 442aace18a2ef364fdd3e6b3a5ea59f8efdbbf57 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 22:47:28 +0300 Subject: [PATCH 148/172] feat: default deploy method to CloudFormation CLI in both modes Simple mode and advanced mode now default to CFN CLI instead of Terraform. Advanced mode menu lists CFN CLI first and pre-selects it. Co-Authored-By: Claude Opus 4.6 --- install.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index bb494d4..9818129 100755 --- a/install.sh +++ b/install.sh @@ -728,15 +728,15 @@ choose_deploy_method() { ok "Deploy method pre-selected: ${method_name}" else local method_choice - method_choice=$($GUM choose --header "Deployment method" --selected "Terraform" \ - "CloudFormation Console" \ + method_choice=$($GUM choose --header "Deployment method" --selected "CloudFormation CLI" \ "CloudFormation CLI" \ + "CloudFormation Console" \ "Terraform" < /dev/tty) case "$method_choice" in - "CloudFormation Console") DEPLOY_METHOD="$DEPLOY_CFN_CONSOLE" ;; "CloudFormation CLI") DEPLOY_METHOD="$DEPLOY_CFN_CLI" ;; + "CloudFormation Console") DEPLOY_METHOD="$DEPLOY_CFN_CONSOLE" ;; "Terraform") DEPLOY_METHOD="$DEPLOY_TERRAFORM" ;; - *) DEPLOY_METHOD="$DEPLOY_TERRAFORM" ;; + *) DEPLOY_METHOD="$DEPLOY_CFN_CLI" ;; esac fi @@ -943,7 +943,7 @@ collect_config_simple() { # ---- Auto-configure everything else ---- DEPLOY_REGION="${REGION:-us-east-1}" - DEPLOY_METHOD="$DEPLOY_TERRAFORM" + DEPLOY_METHOD="$DEPLOY_CFN_CLI" # Instance type: profile determines size in simple mode case "$PROFILE_NAME" in @@ -1876,9 +1876,8 @@ show_complete() { # ============================================================================ run_config_and_review() { if [[ "$INSTALL_MODE" == "simple" ]]; then - # Simple mode: pack + profile, then auto-configure, then terraform check + # Simple mode: pack + profile, then auto-configure (defaults to CFN CLI) collect_config_simple - ensure_terraform_available # Auto-detect VPC reuse check_existing_deployments else From d0ff13da8390440d8a5e759740b176adca5d3362 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 19:47:54 +0000 Subject: [PATCH 149/172] ci: bump version to 0.5.66 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9818129..0f31f18 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.65" +INSTALLER_VERSION="0.5.66" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 713b581e83fbc301a895689bb0054f7fa784ad71 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 22:57:15 +0300 Subject: [PATCH 150/172] fix: source IronClaw .env in shell profile for interactive DATABASE_URL The .env file (DATABASE_URL, LLM_BACKEND, etc.) was only loaded by the systemd service via EnvironmentFile. Running `ironclaw` interactively had no DATABASE_URL, causing "No database connection" in the setup wizard. Co-Authored-By: Claude Opus 4.6 --- packs/ironclaw/resources/shell-profile.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packs/ironclaw/resources/shell-profile.sh b/packs/ironclaw/resources/shell-profile.sh index 17c39d8..3ccc118 100644 --- a/packs/ironclaw/resources/shell-profile.sh +++ b/packs/ironclaw/resources/shell-profile.sh @@ -2,6 +2,10 @@ # This file defines aliases and the welcome banner for the IronClaw pack. PACK_ALIASES=' +# Source IronClaw env vars (DATABASE_URL, LLM config) for interactive use +if [ -f "$HOME/.ironclaw/.env" ]; then + set -a; source "$HOME/.ironclaw/.env"; set +a +fi alias ic="ironclaw" ' From 29571be657df0042787ca788753742f44c0e90b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 19:57:38 +0000 Subject: [PATCH 151/172] ci: bump version to 0.5.67 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 0f31f18..e6d18ea 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.66" +INSTALLER_VERSION="0.5.67" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 5f7501381028b6a1bd89030d992e1a9f85ad38fa Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 23:04:08 +0300 Subject: [PATCH 152/172] fix: ironclaw alias uses --no-onboard to skip broken setup wizard The onboard wizard fails with "No database connection" even when PostgreSQL is running. The actual agent works fine with --no-onboard. Alias `ic` now runs `ironclaw run --no-onboard`. Original wizard available as `ironclaw-wizard`. Co-Authored-By: Claude Opus 4.6 --- packs/ironclaw/resources/shell-profile.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packs/ironclaw/resources/shell-profile.sh b/packs/ironclaw/resources/shell-profile.sh index 3ccc118..b869fce 100644 --- a/packs/ironclaw/resources/shell-profile.sh +++ b/packs/ironclaw/resources/shell-profile.sh @@ -6,13 +6,14 @@ PACK_ALIASES=' if [ -f "$HOME/.ironclaw/.env" ]; then set -a; source "$HOME/.ironclaw/.env"; set +a fi -alias ic="ironclaw" +alias ic="ironclaw run --no-onboard" +alias ironclaw-wizard="ironclaw" ' PACK_BANNER_NAME="IronClaw Agent Environment" PACK_BANNER_EMOJI="🦀" PACK_BANNER_COMMANDS=' - ironclaw → Run IronClaw CLI agent + ic → Run IronClaw agent ironclaw --help → Show IronClaw options ironclaw --version → Show installed version ' From 95e46e2b79f924921d4e72aa5974eab7cbf9554e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 20:04:33 +0000 Subject: [PATCH 153/172] ci: bump version to 0.5.68 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e6d18ea..1d8ea85 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.67" +INSTALLER_VERSION="0.5.68" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 916a65aedcc2f9f1e01a85fc06cc8ea74658541b Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 23:15:46 +0300 Subject: [PATCH 154/172] feat: switch IronClaw to native Bedrock backend Use LLM_BACKEND=bedrock instead of openai_compatible through bedrockify. IronClaw's native Bedrock provider uses the AWS SDK Converse API directly via EC2 instance profile credentials, eliminating the bedrockify dependency. --- packs/ironclaw/install.sh | 37 +++++++++++++++--------------------- packs/ironclaw/manifest.yaml | 10 +++------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 9610509..14dd15e 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -1,13 +1,12 @@ #!/usr/bin/env bash -# packs/ironclaw/install.sh — Install IronClaw and configure it to use bedrockify +# packs/ironclaw/install.sh — Install IronClaw with native AWS Bedrock support # # Usage: -# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] # # Assumes: -# - bedrockify is already installed and running (see packs/bedrockify/) # - curl available -# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) +# - IAM role with bedrock:InvokeModel permissions (EC2 instance profile) # # IronClaw is a single static Rust binary — no Rust/Cargo needed at runtime. # We download the pre-built musl binary from GitHub releases. @@ -18,7 +17,7 @@ # without it, `ironclaw onboard` segfaults on headless EC2) # # NEAR AI OAuth is bypassed entirely — we write .env with -# LLM_BACKEND=openai_compatible, pointing at bedrockify. +# LLM_BACKEND=bedrock, using the native AWS SDK credential chain. # # Idempotent: safe to re-run. @@ -32,27 +31,25 @@ source "${SCRIPT_DIR}/../common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── PACK_ARG_REGION="$(pack_config_get region "us-east-1")" PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" -PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" # ── Help ────────────────────────────────────────────────────────────────────── usage() { cat < "${HOME}/.ironclaw/.env" < Date: Tue, 7 Apr 2026 20:16:14 +0000 Subject: [PATCH 155/172] ci: bump version to 0.5.69 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1d8ea85..5e2da75 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.68" +INSTALLER_VERSION="0.5.69" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 1386af17349157eb5b349955767f8beddc622384 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 23:32:25 +0300 Subject: [PATCH 156/172] fix: use correct env vars for IronClaw native Bedrock backend IronClaw expects BEDROCK_MODEL and BEDROCK_REGION, not LLM_MODEL and AWS_DEFAULT_REGION. Also strip cross-region prefix (e.g. "us.") from model ID and set BEDROCK_CROSS_REGION separately. --- packs/ironclaw/install.sh | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 14dd15e..3e0c451 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -287,12 +287,30 @@ step "Configuring IronClaw" mkdir -p "${HOME}/.ironclaw" # Write .env with native Bedrock + PostgreSQL config — bypasses NEAR AI OAuth entirely +# IronClaw's Bedrock backend expects BEDROCK_MODEL (base model ID without cross-region +# prefix) and BEDROCK_REGION. If the model has a cross-region prefix like "us.", strip +# it and set BEDROCK_CROSS_REGION separately. +BEDROCK_MODEL_ID="${MODEL}" +BEDROCK_CROSS="" +if [[ "${MODEL}" =~ ^(us|eu|apac|global)\. ]]; then + BEDROCK_CROSS="${BASH_REMATCH[1]}" + BEDROCK_MODEL_ID="${MODEL#*.}" +fi + cat > "${HOME}/.ironclaw/.env" <> "${HOME}/.ironclaw/.env" +fi + +cat >> "${HOME}/.ironclaw/.env" < Date: Tue, 7 Apr 2026 20:33:00 +0000 Subject: [PATCH 157/172] ci: bump version to 0.5.70 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 5e2da75..3064ef5 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.69" +INSTALLER_VERSION="0.5.70" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From e89ba5ee83e9c6a287ed79e5796068dea9f9ce9a Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 23:38:49 +0300 Subject: [PATCH 158/172] fix: revert IronClaw to openai_compatible via bedrockify The pre-built ironclaw release binary is not compiled with --features bedrock, so native LLM_BACKEND=bedrock fails at runtime. Revert to openai_compatible backend routing through bedrockify until upstream ships a bedrock-enabled binary. --- packs/ironclaw/install.sh | 64 ++++++++++++++++-------------------- packs/ironclaw/manifest.yaml | 13 ++++++-- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 3e0c451..0d176b8 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash -# packs/ironclaw/install.sh — Install IronClaw with native AWS Bedrock support +# packs/ironclaw/install.sh — Install IronClaw and configure it to use bedrockify # # Usage: -# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] +# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] # # Assumes: +# - bedrockify is already installed and running (see packs/bedrockify/) # - curl available -# - IAM role with bedrock:InvokeModel permissions (EC2 instance profile) +# - IAM role with bedrock:InvokeModel permissions (handled by bedrockify) # # IronClaw is a single static Rust binary — no Rust/Cargo needed at runtime. # We download the pre-built musl binary from GitHub releases. @@ -17,7 +18,11 @@ # without it, `ironclaw onboard` segfaults on headless EC2) # # NEAR AI OAuth is bypassed entirely — we write .env with -# LLM_BACKEND=bedrock, using the native AWS SDK credential chain. +# LLM_BACKEND=openai_compatible, pointing at bedrockify. +# +# NOTE: IronClaw supports native LLM_BACKEND=bedrock via AWS SDK, but the +# pre-built release binary is not compiled with --features bedrock. +# Once upstream ships a bedrock-enabled binary, switch to native Bedrock. # # Idempotent: safe to re-run. @@ -31,25 +36,27 @@ source "${SCRIPT_DIR}/../common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── PACK_ARG_REGION="$(pack_config_get region "us-east-1")" PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" # ── Help ────────────────────────────────────────────────────────────────────── usage() { cat < "${HOME}/.ironclaw/.env" <> "${HOME}/.ironclaw/.env" -fi - -cat >> "${HOME}/.ironclaw/.env" < Date: Tue, 7 Apr 2026 20:39:20 +0000 Subject: [PATCH 159/172] ci: bump version to 0.5.71 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3064ef5..96a3643 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.70" +INSTALLER_VERSION="0.5.71" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From dc961f2f5b64aed5b35e2c52f9faf97c7d5fd2b6 Mon Sep 17 00:00:00 2001 From: Roy Osherove Date: Tue, 7 Apr 2026 23:44:31 +0300 Subject: [PATCH 160/172] fix: use correct bedrockify model ID for IronClaw Bedrockify exposes models as 'anthropic.claude-sonnet-4-6' without the cross-region 'us.' prefix or '-v1' suffix. The previous default 'us.anthropic.claude-sonnet-4-6-v1' caused 400 Bad Request errors. --- packs/ironclaw/install.sh | 8 ++++---- packs/ironclaw/manifest.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packs/ironclaw/install.sh b/packs/ironclaw/install.sh index 0d176b8..ef34470 100755 --- a/packs/ironclaw/install.sh +++ b/packs/ironclaw/install.sh @@ -2,7 +2,7 @@ # packs/ironclaw/install.sh — Install IronClaw and configure it to use bedrockify # # Usage: -# ./install.sh [--region us-east-1] [--model us.anthropic.claude-sonnet-4-6-v1] [--bedrockify-port 8090] +# ./install.sh [--region us-east-1] [--model anthropic.claude-sonnet-4-6] [--bedrockify-port 8090] # # Assumes: # - bedrockify is already installed and running (see packs/bedrockify/) @@ -35,7 +35,7 @@ source "${SCRIPT_DIR}/../common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── PACK_ARG_REGION="$(pack_config_get region "us-east-1")" -PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-sonnet-4-6-v1")" +PACK_ARG_MODEL="$(pack_config_get model "anthropic.claude-sonnet-4-6")" PACK_ARG_BEDROCKIFY_PORT="$(pack_config_get bedrockify_port "8090")" # ── Help ────────────────────────────────────────────────────────────────────── @@ -47,7 +47,7 @@ Install IronClaw and configure it to use bedrockify. Options: --region AWS region for Bedrock (default: us-east-1) - --model Model ID for LLM_MODEL (default: us.anthropic.claude-sonnet-4-6-v1) + --model Model ID for LLM_MODEL (default: anthropic.claude-sonnet-4-6) --bedrockify-port Port where bedrockify listens (default: 8090) --help Show this help message @@ -56,7 +56,7 @@ NEAR AI OAuth is bypassed; bedrockify handles all LLM access. Examples: ./install.sh --region us-east-1 - ./install.sh --model us.anthropic.claude-sonnet-4-6-v1 --bedrockify-port 8090 + ./install.sh --model anthropic.claude-sonnet-4-6 --bedrockify-port 8090 EOF } diff --git a/packs/ironclaw/manifest.yaml b/packs/ironclaw/manifest.yaml index 1ed00f8..531c6b7 100644 --- a/packs/ironclaw/manifest.yaml +++ b/packs/ironclaw/manifest.yaml @@ -24,7 +24,7 @@ params: default: us-east-1 - name: model description: "Model ID for LLM_MODEL env var (via bedrockify)" - default: "us.anthropic.claude-sonnet-4-6-v1" + default: "anthropic.claude-sonnet-4-6" - name: bedrockify-port description: "Port where bedrockify is running" default: "8090" From 91b3e4f93c3f164c8f7f3e365fb866d3a73f91f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Apr 2026 20:44:47 +0000 Subject: [PATCH 161/172] ci: bump version to 0.5.72 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 96a3643..b74b09b 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.71" +INSTALLER_VERSION="0.5.72" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From da0dcf2ab09eef7e131dcad3ffcdf4b5ed47a4f7 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Wed, 8 Apr 2026 15:12:55 +0000 Subject: [PATCH 162/172] =?UTF-8?q?fix:=20rename=20loki=20tui=20=E2=86=92?= =?UTF-8?q?=20openclaw=20tui=20in=20SSM=20doc,=20aliases,=20and=20install?= =?UTF-8?q?=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.sh | 6 +++--- packs/openclaw/resources/shell-profile.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index b74b09b..e1a40b2 100755 --- a/install.sh +++ b/install.sh @@ -1327,7 +1327,7 @@ deploy_console() { echo "" echo -e " ${BOLD}Connect:${NC}" echo -e " ${CYAN}$(ssm_connect_cmd '')${NC}" - echo -e " ${CYAN}loki tui${NC}" + echo -e " ${CYAN}openclaw tui${NC}" echo "" echo -e " ${DIM}Docs:${NC} ${DOCS_URL}" echo "" @@ -1675,7 +1675,7 @@ terraform_apply() { # ============================================================================ # Ensure Loki-Session SSM document exists (instance-scoped, not account-wide) ensure_ssm_session_document() { - local doc_content='{"schemaVersion":"1.0","description":"SSM session for Loki - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"loki tui || exec bash --login\""}}}' + local doc_content='{"schemaVersion":"1.0","description":"SSM session for OpenClaw - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"openclaw tui || exec bash --login\""}}}' if aws ssm describe-document --name "$SSM_DOC_NAME" --region "$DEPLOY_REGION" &>/dev/null; then # Update existing document and set new version as default @@ -1830,7 +1830,7 @@ show_complete() { # Load pack-specific commands for the completion screen local pack_profile="${CLONE_DIR}/packs/${PACK_NAME}/resources/shell-profile.sh" - local pack_commands="loki tui" + local pack_commands="openclaw tui" local pack_name_display="Loki" if [[ -f "$pack_profile" ]]; then source "$pack_profile" diff --git a/packs/openclaw/resources/shell-profile.sh b/packs/openclaw/resources/shell-profile.sh index 1a6f7d2..8351b67 100644 --- a/packs/openclaw/resources/shell-profile.sh +++ b/packs/openclaw/resources/shell-profile.sh @@ -3,8 +3,8 @@ PACK_ALIASES=' alias loki="openclaw" -alias lt="loki tui" -alias gr="loki gateway restart" +alias lt="openclaw tui" +alias gr="openclaw gateway restart" ' PACK_BANNER_NAME="OpenClaw Agent Environment" From ce0fbb3a5a4cb00e128de0e232304dd5c2c2f5bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 15:14:04 +0000 Subject: [PATCH 163/172] ci: bump version to 0.5.73 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e1a40b2..9326697 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.72" +INSTALLER_VERSION="0.5.73" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From e66880de8c059c423f2f8735c2996bd239aa770a Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Wed, 8 Apr 2026 15:15:01 +0000 Subject: [PATCH 164/172] fix: make SSM document TUI command dynamic per pack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each pack now declares PACK_TUI_COMMAND in its shell-profile. The SSM session document, completion screen, and bootstrap echo all use this variable instead of hardcoded 'openclaw tui'. Examples: - openclaw pack → 'openclaw tui' - claude-code pack → 'claude' - hermes pack → 'hermes' - kiro-cli pack → 'kiro-cli' --- install.sh | 21 ++++++++++++++++---- packs/claude-code/resources/shell-profile.sh | 1 + packs/hermes/resources/shell-profile.sh | 1 + packs/ironclaw/resources/shell-profile.sh | 1 + packs/kiro-cli/resources/shell-profile.sh | 1 + packs/nemoclaw/resources/shell-profile.sh | 1 + packs/openclaw/resources/shell-profile.sh | 1 + packs/pi/resources/shell-profile.sh | 1 + 8 files changed, 24 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 9326697..de1c8db 100755 --- a/install.sh +++ b/install.sh @@ -1327,7 +1327,7 @@ deploy_console() { echo "" echo -e " ${BOLD}Connect:${NC}" echo -e " ${CYAN}$(ssm_connect_cmd '')${NC}" - echo -e " ${CYAN}openclaw tui${NC}" + echo -e " ${CYAN}${PACK_TUI_COMMAND:-openclaw tui}${NC}" echo "" echo -e " ${DIM}Docs:${NC} ${DOCS_URL}" echo "" @@ -1675,7 +1675,20 @@ terraform_apply() { # ============================================================================ # Ensure Loki-Session SSM document exists (instance-scoped, not account-wide) ensure_ssm_session_document() { - local doc_content='{"schemaVersion":"1.0","description":"SSM session for OpenClaw - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"openclaw tui || exec bash --login\""}}}' + # Source pack profile to get the correct TUI command for this agent + local pack_profile="${CLONE_DIR}/packs/${PACK_NAME}/resources/shell-profile.sh" + local tui_cmd="bash --login" + if [[ -f "$pack_profile" ]]; then + source "$pack_profile" + tui_cmd="${PACK_TUI_COMMAND:-bash --login}" + fi + local escaped_cmd + escaped_cmd="${tui_cmd//\"/\\\"}" + local doc_content + doc_content="$(cat </dev/null; then # Update existing document and set new version as default @@ -1830,8 +1843,8 @@ show_complete() { # Load pack-specific commands for the completion screen local pack_profile="${CLONE_DIR}/packs/${PACK_NAME}/resources/shell-profile.sh" - local pack_commands="openclaw tui" - local pack_name_display="Loki" + local pack_commands="${PACK_TUI_COMMAND:-openclaw tui}" + local pack_name_display="${PACK_BANNER_NAME:-Agent}" if [[ -f "$pack_profile" ]]; then source "$pack_profile" pack_name_display="${PACK_BANNER_NAME:-Loki}" diff --git a/packs/claude-code/resources/shell-profile.sh b/packs/claude-code/resources/shell-profile.sh index 8a4b783..b44754a 100755 --- a/packs/claude-code/resources/shell-profile.sh +++ b/packs/claude-code/resources/shell-profile.sh @@ -1,4 +1,5 @@ # Claude Code shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="claude" # This file defines aliases and the welcome banner for the Claude Code pack. PACK_ALIASES=' diff --git a/packs/hermes/resources/shell-profile.sh b/packs/hermes/resources/shell-profile.sh index 01875bb..1b382ef 100644 --- a/packs/hermes/resources/shell-profile.sh +++ b/packs/hermes/resources/shell-profile.sh @@ -1,4 +1,5 @@ # Hermes shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="hermes" # This file defines aliases and the welcome banner for the Hermes pack. PACK_ALIASES=' diff --git a/packs/ironclaw/resources/shell-profile.sh b/packs/ironclaw/resources/shell-profile.sh index b869fce..ffe4fe7 100644 --- a/packs/ironclaw/resources/shell-profile.sh +++ b/packs/ironclaw/resources/shell-profile.sh @@ -1,4 +1,5 @@ # IronClaw shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="ironclaw run --no-onboard" # This file defines aliases and the welcome banner for the IronClaw pack. PACK_ALIASES=' diff --git a/packs/kiro-cli/resources/shell-profile.sh b/packs/kiro-cli/resources/shell-profile.sh index 73cf2fc..c76a2e7 100644 --- a/packs/kiro-cli/resources/shell-profile.sh +++ b/packs/kiro-cli/resources/shell-profile.sh @@ -1,4 +1,5 @@ # Kiro CLI shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="kiro-cli" # Defines aliases and welcome banner for the kiro-cli pack. # # NOTE: Kiro CLI requires interactive login after install. diff --git a/packs/nemoclaw/resources/shell-profile.sh b/packs/nemoclaw/resources/shell-profile.sh index c4d57c8..6b96022 100644 --- a/packs/nemoclaw/resources/shell-profile.sh +++ b/packs/nemoclaw/resources/shell-profile.sh @@ -1,4 +1,5 @@ # NemoClaw shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="nemoclaw loki-assistant shell" # This file defines aliases and the welcome banner for the NemoClaw pack. PACK_ALIASES=' diff --git a/packs/openclaw/resources/shell-profile.sh b/packs/openclaw/resources/shell-profile.sh index 8351b67..012c63f 100644 --- a/packs/openclaw/resources/shell-profile.sh +++ b/packs/openclaw/resources/shell-profile.sh @@ -1,4 +1,5 @@ # OpenClaw shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="openclaw tui" # This file defines aliases and the welcome banner for the OpenClaw pack. PACK_ALIASES=' diff --git a/packs/pi/resources/shell-profile.sh b/packs/pi/resources/shell-profile.sh index f5376ab..a2ae02e 100644 --- a/packs/pi/resources/shell-profile.sh +++ b/packs/pi/resources/shell-profile.sh @@ -1,4 +1,5 @@ # Pi shell profile — sourced by bootstrap for .bashrc and /etc/profile.d +PACK_TUI_COMMAND="pi" # This file defines aliases and the welcome banner for the Pi pack. PACK_ALIASES=' From 1e1753befca2517e4be53589508d1e03cdd4056d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 15:15:50 +0000 Subject: [PATCH 165/172] ci: bump version to 0.5.74 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index de1c8db..0f47e52 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="Loki-Session" -INSTALLER_VERSION="0.5.73" +INSTALLER_VERSION="0.5.74" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From a81ab478c5d8cd0dc479a61eb904baa1bb0b2856 Mon Sep 17 00:00:00 2001 From: "Loki@FastStart" Date: Wed, 8 Apr 2026 15:23:15 +0000 Subject: [PATCH 166/172] fix: address code review findings for dynamic SSM document - SSM doc name now pack-specific (Loki-Session-{pack}) to avoid cross-pack collisions in same account/region - deploy_console() loads pack profile before displaying TUI command - Profile sourcing uses grep/subshell extraction instead of raw source to avoid executing arbitrary code on installer host - kiro-cli profile guards interactive code with [[ $- == *i* ]] - JSON construction uses jq instead of hand-escaping - Generic fallback (bash --login) instead of hardcoded openclaw tui - Updated docs: README.md and wiki to show openclaw tui --- README.md | 2 +- install.sh | 40 ++++++++++++++--------- packs/kiro-cli/resources/shell-profile.sh | 4 +-- wiki/Deploying-Loki-on-AWS.md | 4 +-- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6240372..bf5902b 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ aws cloudformation create-stack \ aws ssm start-session --target # Talk to your Loki -loki tui +openclaw tui # or use the alias: loki tui ``` Full deployment guide: [Deploying Loki on AWS](https://github.com/inceptionstack/loki-agent/wiki/Deploying-Loki-on-AWS) diff --git a/install.sh b/install.sh index 0f47e52..1dfc622 100755 --- a/install.sh +++ b/install.sh @@ -64,7 +64,7 @@ trap ' REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" -SSM_DOC_NAME="Loki-Session" +SSM_DOC_NAME="" INSTALLER_VERSION="0.5.74" # --non-interactive / --yes / -y: accept all defaults, minimal prompts @@ -419,8 +419,9 @@ run_or_fail() { ssm_connect_cmd() { local target="${1:-\$INSTANCE_ID}" local cmd="aws ssm start-session --target ${target}" - if aws ssm describe-document --name "$SSM_DOC_NAME" --region "$DEPLOY_REGION" &>/dev/null 2>&1; then - cmd+=" --document-name ${SSM_DOC_NAME}" + local doc_name="${SSM_DOC_NAME:-Loki-Session-${PACK_NAME:-openclaw}}" + if [[ -n "$doc_name" ]] && aws ssm describe-document --name "$doc_name" --region "$DEPLOY_REGION" &>/dev/null 2>&1; then + cmd+=" --document-name ${doc_name}" fi cmd+=" --region ${DEPLOY_REGION}" echo "$cmd" @@ -1278,6 +1279,11 @@ clean_stale_terraform() { # Deploy: CloudFormation Console (option 1) # ============================================================================ deploy_console() { + # Load pack profile for TUI command display + local _pack_profile="${CLONE_DIR:-}/packs/${PACK_NAME}/resources/shell-profile.sh" + if [[ -f "$_pack_profile" ]]; then + eval "$(grep -E '^PACK_(TUI_COMMAND|BANNER_NAME|BANNER_EMOJI)=' "$_pack_profile")" + fi echo "" info "Preparing CloudFormation Console launch..." @@ -1327,7 +1333,7 @@ deploy_console() { echo "" echo -e " ${BOLD}Connect:${NC}" echo -e " ${CYAN}$(ssm_connect_cmd '')${NC}" - echo -e " ${CYAN}${PACK_TUI_COMMAND:-openclaw tui}${NC}" + echo -e " ${CYAN}${PACK_TUI_COMMAND:-bash --login}${NC}" echo "" echo -e " ${DIM}Docs:${NC} ${DOCS_URL}" echo "" @@ -1675,20 +1681,23 @@ terraform_apply() { # ============================================================================ # Ensure Loki-Session SSM document exists (instance-scoped, not account-wide) ensure_ssm_session_document() { + # Build pack-specific document name to avoid collisions between different agents + SSM_DOC_NAME="Loki-Session-${PACK_NAME}" + # Source pack profile to get the correct TUI command for this agent local pack_profile="${CLONE_DIR}/packs/${PACK_NAME}/resources/shell-profile.sh" local tui_cmd="bash --login" if [[ -f "$pack_profile" ]]; then - source "$pack_profile" + # Extract only variable assignments — don't execute arbitrary profile code + eval "$(grep -E '^PACK_(TUI_COMMAND|BANNER_NAME|BANNER_EMOJI)=' "$pack_profile")" tui_cmd="${PACK_TUI_COMMAND:-bash --login}" fi - local escaped_cmd - escaped_cmd="${tui_cmd//\"/\\\"}" + local shell_cmd="cd ~ && bash --login -c \"${tui_cmd} || exec bash --login\"" local doc_content - doc_content="$(cat </dev/null; then # Update existing document and set new version as default @@ -1843,12 +1852,13 @@ show_complete() { # Load pack-specific commands for the completion screen local pack_profile="${CLONE_DIR}/packs/${PACK_NAME}/resources/shell-profile.sh" - local pack_commands="${PACK_TUI_COMMAND:-openclaw tui}" + local pack_commands="${PACK_TUI_COMMAND:-bash --login}" local pack_name_display="${PACK_BANNER_NAME:-Agent}" if [[ -f "$pack_profile" ]]; then - source "$pack_profile" - pack_name_display="${PACK_BANNER_NAME:-Loki}" - pack_commands="${PACK_BANNER_COMMANDS}" + # Source in subshell to extract variables safely (PACK_* only) + eval "$(bash -c 'source "$1" 2>/dev/null; for v in PACK_TUI_COMMAND PACK_BANNER_NAME PACK_BANNER_EMOJI PACK_BANNER_COMMANDS; do [[ -n "${!v}" ]] && printf "%s=%q\n" "$v" "${!v}"; done' _ "$pack_profile")" + pack_name_display="${PACK_BANNER_NAME:-Agent}" + pack_commands="${PACK_BANNER_COMMANDS:-${PACK_TUI_COMMAND:-bash --login}}" fi echo "" diff --git a/packs/kiro-cli/resources/shell-profile.sh b/packs/kiro-cli/resources/shell-profile.sh index c76a2e7..fca7289 100644 --- a/packs/kiro-cli/resources/shell-profile.sh +++ b/packs/kiro-cli/resources/shell-profile.sh @@ -20,7 +20,7 @@ PACK_BANNER_COMMANDS=' kiro-cli settings chat.defaultAgent → Show/set default agent ' -# ⚠ Login reminder: check if kiro-cli is installed but remind about auth -if command -v kiro-cli &>/dev/null; then +# ⚠ Login reminder: only in interactive shells (not during install sourcing) +if [[ $- == *i* ]] && command -v kiro-cli &>/dev/null; then printf '\n\033[0;33m⚠ Kiro CLI installed. If not yet authenticated, run: kiro-cli login --use-device-flow\033[0m\n\n' fi diff --git a/wiki/Deploying-Loki-on-AWS.md b/wiki/Deploying-Loki-on-AWS.md index 0c6d0c3..80535f4 100644 --- a/wiki/Deploying-Loki-on-AWS.md +++ b/wiki/Deploying-Loki-on-AWS.md @@ -41,7 +41,7 @@ aws cloudformation create-stack \ aws ssm start-session --target --region us-east-1 # 4. Talk to it -loki tui +openclaw tui ``` **Estimated monthly cost:** $50–150 depending on instance size and model usage. [Set budget alerts first.](#step-6-set-budget-alerts) @@ -212,7 +212,7 @@ You should see the gateway as `running` and the model configured. ### Talk to It ```bash -loki tui +openclaw tui ``` This opens the terminal UI where you can chat directly with your Loki instance. From 81ba7946db48d1aa753e12d57a2270f28f8995b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 15:23:52 +0000 Subject: [PATCH 167/172] ci: bump version to 0.5.75 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1dfc622..3c14a21 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="" -INSTALLER_VERSION="0.5.74" +INSTALLER_VERSION="0.5.75" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From 53e6189fe2e807468672de0493bb24d2f3ba9364 Mon Sep 17 00:00:00 2001 From: Loki Date: Thu, 9 Apr 2026 22:36:29 +0000 Subject: [PATCH 168/172] feat: add BOOTSTRAP-DEVOPS-AGENT for AWS DevOps Agent skill setup Adds optional bootstrap that guides agents through setting up the aws-devops-agent skill for querying AWS DevOps Agent via boto3. Key learnings encoded: - send_message only in boto3, not CLI - list-chats CLI has datetime parsing bug - EventStream response with final_response blocks - Multi-turn via executionId reuse - Monthly quota tracking --- bootstraps/optional/BOOTSTRAP-DEVOPS-AGENT.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 bootstraps/optional/BOOTSTRAP-DEVOPS-AGENT.md diff --git a/bootstraps/optional/BOOTSTRAP-DEVOPS-AGENT.md b/bootstraps/optional/BOOTSTRAP-DEVOPS-AGENT.md new file mode 100644 index 0000000..feb6c56 --- /dev/null +++ b/bootstraps/optional/BOOTSTRAP-DEVOPS-AGENT.md @@ -0,0 +1,119 @@ +# BOOTSTRAP-DEVOPS-AGENT.md — Set Up AWS DevOps Agent Skill + +> **Applies to:** Agents with AWS account access (boto3 + CLI) + +## What This Does + +Installs the `aws-devops-agent` skill so you can query the AWS DevOps Agent for infrastructure health, incident investigation, recommendations, and on-demand SRE tasks — all via boto3 (no browser/console needed). + +## Prerequisites + +- boto3 >= 1.42.87 (must have `devops-agent` service support) +- An existing DevOps Agent space in the account (created via AWS Console initially) +- IAM permissions to call `devops-agent:*` + +## Step 1 — Verify boto3 Version + +```bash +python3 -c "import boto3; print(boto3.__version__)" +``` + +If below 1.42.87: +```bash +pip install --upgrade --break-system-packages boto3 botocore +``` + +## Step 2 — Verify Agent Space Exists + +```bash +aws devops-agent list-agent-spaces --region us-east-1 +``` + +Note the `agentSpaceId` — you'll need it for queries. + +If no agent space exists, one must be created via the AWS Console first (no CLI support for initial setup with associations). + +## Step 3 — Create the Skill + +Create the skill directory at `~/.openclaw/workspace/skills/aws-devops-agent/` with this structure: + +``` +aws-devops-agent/ +├── SKILL.md +├── scripts/ +│ └── devops-agent-chat.py +└── references/ + └── api-reference.md +``` + +### SKILL.md + +The SKILL.md should contain: +- **Frontmatter:** name `aws-devops-agent`, description covering when to use (DevOps Agent queries, incident investigation, recommendations) +- **Body:** Quick start with the chat script, workflow for discover → query → follow-up → management, important notes about CLI limitations + +### scripts/devops-agent-chat.py + +A Python script that: +1. Creates a chat session via `client.create_chat()` +2. Sends a message via `client.send_message()` +3. Iterates the EventStream, collecting `final_response` blocks +4. Supports `--exec-id` for multi-turn, `--raw` for full output +5. Prints `exec_id=` to stderr for reuse + +Key implementation details: +- EventStream has block types: `text` (thinking), `tool_summary`, `final_response` (the answer), `chat_title` +- `send_message` is **boto3 only** — the CLI does not expose it +- `list-chats` CLI has a datetime parsing bug (epoch millis treated as year) + +### references/api-reference.md + +Document the full boto3 API: +- Management operations: `list_agent_spaces`, `get_agent_space`, `list_associations`, `list_goals`, `get_account_usage`, `list_recommendations`, `list_backlog_tasks` +- Chat operations: `create_chat`, `send_message` (streaming EventStream) +- CLI commands (management only) +- Known CLI bugs +- Monthly quotas (200 investigation hrs, 150 evaluation hrs, 40 learning hrs, 200 on-demand hrs) + +## Step 4 — Test + +```bash +# Get agent space ID +SPACE_ID=$(aws devops-agent list-agent-spaces --region us-east-1 --query 'agentSpaces[0].agentSpaceId' --output text) + +# Send a test query +python3 ~/.openclaw/workspace/skills/aws-devops-agent/scripts/devops-agent-chat.py "$SPACE_ID" "How many ECS clusters are in this account?" +``` + +If you get a response listing clusters, the skill is working. + +## Step 5 — Record in Memory + +Add to TOOLS.md or daily memory: +``` +## AWS DevOps Agent +- **Agent Space ID:** +- **Chat script:** skills/aws-devops-agent/scripts/devops-agent-chat.py +- **boto3 only:** send_message not in CLI +- **Quotas:** 200 investigation + 150 evaluation + 40 learning + 200 on-demand hrs/month +``` + +## Usage Examples + +```bash +# Infrastructure health check +python3 scripts/devops-agent-chat.py $SPACE_ID "What unhealthy resources are in this account?" + +# Investigate an incident +python3 scripts/devops-agent-chat.py $SPACE_ID "Investigate why CloudQuiz ECS service keeps crashing" + +# Get recommendations +python3 scripts/devops-agent-chat.py $SPACE_ID "What optimization opportunities do you see?" + +# Multi-turn (reuse exec_id from stderr) +python3 scripts/devops-agent-chat.py $SPACE_ID "Tell me more about the LiteLLM restarts" --exec-id +``` + +## Done + +After completing these steps, delete this file — it's a one-time bootstrap. From 8f292ee8628c2338f6ac22248733f3bb7020bea1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Apr 2026 22:36:44 +0000 Subject: [PATCH 169/172] ci: bump version to 0.5.76 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3c14a21..aa4712d 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="" -INSTALLER_VERSION="0.5.75" +INSTALLER_VERSION="0.5.76" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From a5b5b6e62fbc8663d5357e7e464c6804e7d6dae4 Mon Sep 17 00:00:00 2001 From: Loki Agent Date: Sun, 12 Apr 2026 09:01:30 +0300 Subject: [PATCH 170/172] fix: add explicit contextWindow to BOOTSTRAP-MODEL-CONFIG to prevent 32K cap (#10) Bedrock auto-discovery defaults to 32K contextWindow for discovered models. The bootstrap was only setting agents.defaults.model.primary without registering explicit model entries, so Opus 4.6 (200K context) was silently capped at 32K, causing 'context limit exceeded' errors. Now the config patch includes explicit model entries with contextWindow: 200000 for both Opus and Sonnet, alongside the existing default model + heartbeat config. Closes #9 Co-authored-by: Roy Osherove <575051+royosherove@users.noreply.github.com> --- .../essential/BOOTSTRAP-MODEL-CONFIG.md | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md b/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md index 3a88784..824ed7c 100644 --- a/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md +++ b/bootstraps/essential/BOOTSTRAP-MODEL-CONFIG.md @@ -15,13 +15,39 @@ This gives you full Opus quality when talking to your human, while keeping autom ## OpenClaw-Specific Configuration -### Step 1: Configure Default Model + Heartbeat +### Step 1: Configure Explicit Model Entries + Default Model + Heartbeat -Run this OpenClaw config patch: +Register the models with explicit `contextWindow` and set the default model and heartbeat in a single patch: ```bash openclaw config patch <<'EOF' { + "models": { + "providers": { + "amazon-bedrock": { + "models": [ + { + "id": "global.anthropic.claude-opus-4-6-v1", + "name": "Claude Opus 4.6", + "contextWindow": 200000, + "maxTokens": 16384, + "reasoning": true, + "input": ["text", "image"], + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + }, + { + "id": "global.anthropic.claude-sonnet-4-6", + "name": "Claude Sonnet 4.6", + "contextWindow": 200000, + "maxTokens": 16384, + "reasoning": true, + "input": ["text", "image"], + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + } + ] + } + } + }, "agents": { "defaults": { "model": { @@ -38,6 +64,11 @@ EOF OpenClaw restarts automatically. +> **Why explicit model entries?** Bedrock auto-discovery uses a +> `defaultContextWindow` of 32K for discovered models. Without explicit entries +> that set `contextWindow: 200000`, Opus 4.6 gets capped at 32K — causing +> frequent "context limit exceeded" errors. + ### Step 2: Configure Cron Jobs All cron jobs with `payload.kind: "agentTurn"` should set their model to Sonnet 4.6. @@ -86,6 +117,12 @@ Expected output: amazon-bedrock/global.anthropic.claude-sonnet-4-6 ``` +```bash +openclaw config get models.providers.amazon-bedrock.models +``` + +Verify that both models show `"contextWindow": 200000` (not 32000). + ## Why `global.` prefix? The `global.` inference profile routes across all AWS regions automatically — no need to pick `us.` or `eu.`. Better availability, same price. From f0f9ef48cdbed8097daaab5464ea20237995a295 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Apr 2026 06:01:38 +0000 Subject: [PATCH 171/172] ci: bump version to 0.5.77 [skip ci] --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index aa4712d..5635573 100755 --- a/install.sh +++ b/install.sh @@ -65,7 +65,7 @@ REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="" -INSTALLER_VERSION="0.5.76" +INSTALLER_VERSION="0.5.77" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack From dccd258af2d29a890001b9e57ac32dafd589d312 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:08:09 +0000 Subject: [PATCH 172/172] feat: ship Opus 4.6 + Sonnet 4.6 as default model config with 200K context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config-gen.py: add explicit model entries with contextWindow: 200000 instead of empty models array that relied on Bedrock discovery (32K default) - Switch all defaults from us. to global. prefix for cross-region routing - Opus 4.6 as primary, Sonnet 4.6 as fallback and heartbeat model - Updated: config-gen.py, install.sh, manifest.yaml, template.yaml, bootstrap.sh New deployments get correct 200K context out of the box — no manual bootstrap step needed. --- deploy/bootstrap.sh | 2 +- deploy/cloudformation/template.yaml | 2 +- packs/openclaw/install.sh | 6 +++--- packs/openclaw/manifest.yaml | 2 +- packs/openclaw/resources/config-gen.py | 21 +++++++++++++++++---- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 8cb01d2..8417815 100755 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -99,7 +99,7 @@ All --key value arguments are forwarded to pack install.sh scripts. Packs silently ignore arguments they don't recognise. Examples: - $(basename "$0") --pack openclaw --region us-east-1 --model us.anthropic.claude-opus-4-6-v1 + $(basename "$0") --pack openclaw --region us-east-1 --model global.anthropic.claude-opus-4-6-v1 $(basename "$0") --pack hermes --region eu-west-1 Environment: diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 40780e8..5caa80b 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -213,7 +213,7 @@ Parameters: DefaultModel: Type: String - Default: 'us.anthropic.claude-opus-4-6-v1' + Default: 'global.anthropic.claude-opus-4-6-v1' Description: "The primary AI model. Claude Opus 4.6 is recommended for best performance. Used when ModelMode is 'bedrock'." ModelMode: diff --git a/packs/openclaw/install.sh b/packs/openclaw/install.sh index c957615..064f44c 100755 --- a/packs/openclaw/install.sh +++ b/packs/openclaw/install.sh @@ -23,7 +23,7 @@ source "${SCRIPT_DIR}/../common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── # Defaults from config file (written by bootstrap dispatcher), then CLI overrides PACK_ARG_REGION="$(pack_config_get region "us-east-1")" -PACK_ARG_MODEL="$(pack_config_get model "us.anthropic.claude-opus-4-6-v1")" +PACK_ARG_MODEL="$(pack_config_get model "global.anthropic.claude-opus-4-6-v1")" PACK_ARG_PORT="$(pack_config_get gw_port "3001")" PACK_ARG_TOKEN="$(pack_config_get gw_token "")" PACK_ARG_MODEL_MODE="$(pack_config_get model_mode "bedrock")" @@ -41,7 +41,7 @@ Install OpenClaw and configure the gateway service. Options: --region AWS region for Bedrock (default: us-east-1) - --model Default Bedrock model ID (default: us.anthropic.claude-opus-4-6-v1) + --model Default Bedrock model ID (default: global.anthropic.claude-opus-4-6-v1) --port Gateway port (default: 3001) --token Gateway auth token (default: auto-generated) --model-mode bedrock | litellm | api-key (default: bedrock) @@ -52,7 +52,7 @@ Options: --help Show this help message Examples: - ./install.sh --region us-east-1 --model us.anthropic.claude-opus-4-6-v1 --port 3001 + ./install.sh --region us-east-1 --model global.anthropic.claude-opus-4-6-v1 --port 3001 ./install.sh --model-mode litellm --litellm-url http://proxy:4000 --litellm-key sk-xxx EOF } diff --git a/packs/openclaw/manifest.yaml b/packs/openclaw/manifest.yaml index cb51acf..ba9c349 100644 --- a/packs/openclaw/manifest.yaml +++ b/packs/openclaw/manifest.yaml @@ -21,7 +21,7 @@ params: default: us-east-1 - name: model description: "Default Bedrock model ID" - default: "us.anthropic.claude-opus-4-6-v1" + default: "global.anthropic.claude-opus-4-6-v1" - name: port description: "Gateway port" default: "3001" diff --git a/packs/openclaw/resources/config-gen.py b/packs/openclaw/resources/config-gen.py index 2546197..0d3a25f 100644 --- a/packs/openclaw/resources/config-gen.py +++ b/packs/openclaw/resources/config-gen.py @@ -18,10 +18,23 @@ litellm_model = sys.argv[8] if len(sys.argv) > 8 else "claude-opus-4-6" provider_key = os.environ.get("PROVIDER_KEY_ENV") or (sys.argv[9] if len(sys.argv) > 9 else "") home = os.path.expanduser("~") + +# Explicit model entries with contextWindow to prevent Bedrock discovery 32K default. +# Without these, auto-discovery assigns defaultContextWindow=32000 to all models, +# silently capping Opus/Sonnet 4.6 (200K) and causing "context limit exceeded" errors. +bedrock_models = [ + {"id": "global.anthropic.claude-opus-4-6-v1", "name": "Claude Opus 4.6", "contextWindow": 200000, "maxTokens": 16384, "reasoning": True, "input": ["text", "image"], "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}}, + {"id": "global.anthropic.claude-sonnet-4-6", "name": "Claude Sonnet 4.6", "contextWindow": 200000, "maxTokens": 16384, "reasoning": True, "input": ["text", "image"], "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}}, +] + +# Default to global. prefix for cross-region routing; respect user-provided model ID +default_primary = model if ("." in model and "anthropic" in model) else "global.anthropic.claude-opus-4-6-v1" +default_fallback = "global.anthropic.claude-sonnet-4-6" + cfg = { - "models": {"providers": {"amazon-bedrock": {"baseUrl": f"https://bedrock-runtime.{bedrock_region}.amazonaws.com", "auth": "aws-sdk", "api": "bedrock-converse-stream", "models": []}}}, + "models": {"providers": {"amazon-bedrock": {"baseUrl": f"https://bedrock-runtime.{bedrock_region}.amazonaws.com", "auth": "aws-sdk", "api": "bedrock-converse-stream", "models": bedrock_models}}}, "plugins": {"entries": {"amazon-bedrock": {"config": {"discovery": {"enabled": True, "region": bedrock_region, "providerFilter": ["anthropic"]}}}}}, - "agents": {"defaults": {"model": {"primary": f"amazon-bedrock/{model}", "fallbacks": ["amazon-bedrock/us.anthropic.claude-sonnet-4-6"]}, "workspace": f"{home}/.openclaw/workspace", "compaction": {"mode": "safeguard"}, "heartbeat": {"model": f"amazon-bedrock/us.anthropic.claude-sonnet-4-6", "target": "telegram", "every": "30m", "lightContext": True, "isolatedSession": True}, "maxConcurrent": 4, "subagents": {"maxConcurrent": 8}}}, + "agents": {"defaults": {"model": {"primary": f"amazon-bedrock/{default_primary}", "fallbacks": [f"amazon-bedrock/{default_fallback}"]}, "workspace": f"{home}/.openclaw/workspace", "compaction": {"mode": "safeguard"}, "heartbeat": {"model": f"amazon-bedrock/{default_fallback}", "target": "telegram", "every": "30m", "lightContext": True, "isolatedSession": True}, "maxConcurrent": 4, "subagents": {"maxConcurrent": 8}}}, "tools": {"web": {"search": {"enabled": False}, "fetch": {"enabled": True}}}, "hooks": {"internal": {"enabled": True, "entries": {"boot-md": {"enabled": True}, "bootstrap-extra-files": {"enabled": True}, "command-logger": {"enabled": True}, "session-memory": {"enabled": True}}}}, "gateway": {"port": int(gw_port), "mode": "local", "bind": "loopback", "auth": {"mode": "token", "token": gw_token}} @@ -31,10 +44,10 @@ {"id": "claude-opus-4-6", "name": "Claude Opus 4.6", "reasoning": True, "input": ["text", "image"], "contextWindow": 200000, "maxTokens": 64000}, {"id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6", "reasoning": True, "input": ["text", "image"], "contextWindow": 200000, "maxTokens": 64000}, {"id": "claude-3.5-haiku", "name": "Claude 3.5 Haiku", "reasoning": False, "input": ["text", "image"], "contextWindow": 200000, "maxTokens": 8192}]} - cfg["agents"]["defaults"]["model"] = {"primary": f"litellm/{litellm_model}", "fallbacks": ["litellm/claude-sonnet-4-6", f"amazon-bedrock/{model}"]} + cfg["agents"]["defaults"]["model"] = {"primary": f"litellm/{litellm_model}", "fallbacks": ["litellm/claude-sonnet-4-6", f"amazon-bedrock/{default_primary}"]} elif model_mode == "api-key" and provider_key: cfg["models"]["providers"]["anthropic"] = {"apiKey": provider_key, "models": []} - cfg["agents"]["defaults"]["model"] = {"primary": "anthropic/claude-opus-4-6-20260514", "fallbacks": ["anthropic/claude-sonnet-4-6-20260514", f"amazon-bedrock/{model}"]} + cfg["agents"]["defaults"]["model"] = {"primary": "anthropic/claude-opus-4-6-20260514", "fallbacks": ["anthropic/claude-sonnet-4-6-20260514", f"amazon-bedrock/{default_primary}"]} with open(f"{home}/.openclaw/openclaw.json", "w") as f: json.dump(cfg, f, indent=2) print(f"Config written (mode={model_mode})")