From c541220ea4e121c3a6745eca89fa9cc5a5e24fbd Mon Sep 17 00:00:00 2001 From: Henry Park Date: Mon, 9 Mar 2026 16:17:20 -0700 Subject: [PATCH 1/2] feat(ci): chained promotion PRs with multi-agent Claude review (#776) * feat(ci): chained promotion PRs with multi-agent Claude review [skip-regression-check] Staging CI workflow with batched promotion PRs: - Creates staging-promote/ branches per batch - Chains PRs onto previous promotion branch (incremental diffs) - Claude Code reviews only the incremental changes per batch - Blocked PRs stay open as records of findings - staging-tested tag advances regardless of gate outcome - Runs every 60 min on cron + manual dispatch Multi-agent Claude review (Sonnet orchestrator + Haiku agents): - 4 parallel Sonnet review agents (security, architecture, bugs, performance) - Haiku agents for severity/confidence scoring - [SEVERITY:CONFIDENCE] output format - Severity/confidence matrix for issue creation and gate blocking: CRITICAL: always create issue, block if confidence >=80 HIGH: create issue if confidence >=50 MEDIUM/LOW: create issue if confidence >=80 --- .github/workflows/claude-review.yml | 99 ++++++ .github/workflows/e2e.yml | 1 + .github/workflows/staging-ci.yml | 473 ++++++++++++++++++++++++++++ .github/workflows/test.yml | 37 ++- 4 files changed, 603 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/claude-review.yml create mode 100644 .github/workflows/staging-ci.yml diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 00000000..86d1bb2f --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,99 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, labeled] + +permissions: + contents: read + pull-requests: write + issues: write + id-token: write + +concurrency: + group: claude-review-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + review: + name: Claude Code Review + if: contains(github.event.pull_request.labels.*.name, 'staging-promotion') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Run Claude Code review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_args: "--max-turns 50 --model claude-haiku-4-5-20251001 --allowedTools 'Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(git blame:*),Bash(git log:*),Bash(git diff:*)'" + prompt: | + Code review this pull request. Follow these steps precisely: + + 1. Use a Haiku agent to find relevant CLAUDE.md files: the root CLAUDE.md + and any CLAUDE.md files in directories whose files this PR modifies. + + 2. Use a Haiku agent to summarize the PR change (use `gh pr diff`). + + 3. Launch 4 parallel agents to review the change independently. Each agent should + read the PR diff with `gh pr diff` and the full source files for changed + code, then return a list of issues found: + + Agent 1 — Security & Safety + Check for: command injection, path traversal, SSRF, XSS, auth bypass, + secrets in logs, .unwrap()/.expect() in production code (not tests), + race conditions, TOCTOU, unsafe blocks, panics in async, unbounded allocations. + + Agent 2 — Architecture & Patterns + Check for: extensible design (traits/enums over nested conditionals), + clean abstractions, proper error types (thiserror), CLAUDE.md compliance, + type-driven design over stringly-typed code, DRY violations. + + Agent 3 — Bug Scan + Shallow diff-only scan for obvious bugs: logic errors, off-by-one, + missing error handling, division by zero, incorrect return values. + Ignore nitpicks and likely false positives. Do NOT read extra context + beyond the diff — focus only on the changes. + + Agent 4 — Performance & Production + Check for: blocking in async, N+1 queries, unbounded loops, missing + timeouts, resource leaks (file handles, connections), large allocations + in hot paths. + + 4. For each issue found, launch a parallel Haiku agent to: + a. Assign a severity: + - CRITICAL: security vulns, panics in prod (.unwrap/.expect), data exfiltration, race conditions + - HIGH: logic bugs, missing error handling, breaking API/schema changes + - MEDIUM: missing tests, unnecessary complexity, performance issues + - LOW: documentation gaps, naming suggestions + b. Score confidence 0-100 (give this rubric verbatim): + 0: False positive, doesn't stand up to scrutiny, or pre-existing issue. + 25: Might be real, but may be false positive. Stylistic issues not in CLAUDE.md. + 50: Real issue but nitpick or rare in practice. Not very important. + 75: Verified real issue, will be hit in practice. Directly impacts functionality + or explicitly mentioned in CLAUDE.md. + 100: Certain, confirmed, will happen frequently. Evidence directly confirms. + + 5. Post a single comment on the PR using `gh pr comment` with this format. + If no issues were found, post "No issues found." instead: + + ### Code review + + Found N issues: + + 1. [SEVERITY:CONFIDENCE] + + + + Example: [CRITICAL:92] `.unwrap()` can panic in production when config is missing + + You MUST use the full git SHA in links (not HEAD or branch name). + Provide 1 line of context before and after each linked range. + + Notes: + - Use `gh` for all GitHub interactions, not web fetch + - Do NOT check build signal or attempt to build/test the code + - Ignore pre-existing issues not introduced by this PR + - Ignore issues a linter/compiler would catch (formatting, imports, types) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3dc95a2d..fea70b87 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,5 +1,6 @@ name: E2E Tests on: + workflow_call: schedule: - cron: "0 6 * * 1" # Weekly Monday 6 AM UTC workflow_dispatch: diff --git a/.github/workflows/staging-ci.yml b/.github/workflows/staging-ci.yml new file mode 100644 index 00000000..9e887436 --- /dev/null +++ b/.github/workflows/staging-ci.yml @@ -0,0 +1,473 @@ +name: Staging CI (Batched) + +on: + schedule: + - cron: "0 * * * *" # Every 60 minutes + workflow_dispatch: + inputs: + force: + description: "Force run even if no new commits" + type: boolean + default: false + skip_claude_gate: + description: "Skip Claude review gate (bypass blocking findings)" + type: boolean + default: false + +permissions: + contents: write + issues: write + pull-requests: write + checks: read + +concurrency: + group: staging-ci + cancel-in-progress: false # Let running suites finish + +jobs: + # ── Check for new commits ────────────────────────────────────── + check-changes: + name: Check for new commits + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + current_head: ${{ steps.check.outputs.current_head }} + diff_range: ${{ steps.check.outputs.diff_range }} + steps: + - uses: actions/checkout@v6 + with: + ref: staging + fetch-depth: 0 + fetch-tags: true + + - name: Check for changes since last tested + id: check + env: + FORCE_RUN: ${{ inputs.force }} + run: | + CURRENT_HEAD=$(git rev-parse HEAD) + echo "current_head=${CURRENT_HEAD}" >> "$GITHUB_OUTPUT" + + if git rev-parse staging-tested >/dev/null 2>&1; then + LAST_TESTED=$(git rev-parse staging-tested) + else + LAST_TESTED="" + fi + + DIFF_RANGE="" + if [ -n "$LAST_TESTED" ] && [ "$LAST_TESTED" = "$CURRENT_HEAD" ]; then + echo "No new commits since last tested (${CURRENT_HEAD})" + HAS_CHANGES=false + else + HAS_CHANGES=true + if [ -n "$LAST_TESTED" ]; then + COMMIT_COUNT=$(git rev-list --count "${LAST_TESTED}..HEAD") + echo "Found ${COMMIT_COUNT} new commit(s) since last tested" + DIFF_RANGE="${LAST_TESTED}..${CURRENT_HEAD}" + else + git fetch origin main + MERGE_BASE=$(git merge-base origin/main HEAD) + echo "First run -- reviewing from merge-base ${MERGE_BASE}" + DIFF_RANGE="${MERGE_BASE}..${CURRENT_HEAD}" + fi + fi + + # Force override from workflow_dispatch + if [ "$FORCE_RUN" = "true" ]; then + echo "Force run requested" + HAS_CHANGES=true + if [ -z "$DIFF_RANGE" ]; then + DIFF_RANGE="${CURRENT_HEAD}..${CURRENT_HEAD}" + fi + fi + + echo "has_changes=${HAS_CHANGES}" >> "$GITHUB_OUTPUT" + echo "diff_range=${DIFF_RANGE}" >> "$GITHUB_OUTPUT" + + # ── Run full test suite ────────────────────────────────────────── + tests: + name: Test Suite + needs: check-changes + if: needs.check-changes.outputs.has_changes == 'true' + uses: ./.github/workflows/test.yml + + # ── Run E2E browser tests ──────────────────────────────────────── + e2e: + name: E2E Browser Tests + needs: check-changes + if: needs.check-changes.outputs.has_changes == 'true' + uses: ./.github/workflows/e2e.yml + + # ── Create promotion PR (triggers claude-review.yml on the PR) ── + create-promotion-pr: + name: Create Promotion PR + needs: check-changes + if: needs.check-changes.outputs.has_changes == 'true' + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.create-pr.outputs.pr_number }} + promotion_branch: ${{ steps.branch.outputs.branch }} + steps: + - uses: actions/checkout@v6 + with: + ref: staging + fetch-depth: 0 + + - name: Generate GitHub App token + id: app-token + if: ${{ secrets.GH_RELEASES_MANAGER_APP_ID != '' }} + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} + private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }} + + - name: Set token + id: token + run: | + if [ -n "${{ steps.app-token.outputs.token }}" ]; then + echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT" + else + echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT" + fi + + - name: Check if staging is ahead of main + id: ahead-check + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + run: | + git fetch origin main + AHEAD=$(git rev-list --count origin/main..origin/staging) + echo "commits_ahead=${AHEAD}" >> "$GITHUB_OUTPUT" + if [ "$AHEAD" -eq 0 ]; then + echo "Staging is not ahead of main. Nothing to promote." + else + echo "Staging is ${AHEAD} commits ahead of main." + fi + + - name: Create promotion branch + id: branch + if: steps.ahead-check.outputs.commits_ahead != '0' + run: | + SHORT_SHA=$(echo "${{ needs.check-changes.outputs.current_head }}" | cut -c1-8) + BRANCH="staging-promote/${SHORT_SHA}-${{ github.run_id }}" + git checkout -b "$BRANCH" + git push origin "$BRANCH" + echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" + echo "Created promotion branch: ${BRANCH}" + + - name: Find base branch + id: find-base + if: steps.ahead-check.outputs.commits_ahead != '0' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + run: | + # Find the newest open promotion PR with a staging-promote/* head branch + LATEST=$(gh pr list --label staging-promotion --state open \ + --json headRefName,createdAt \ + --jq '[.[] | select(.headRefName | startswith("staging-promote/"))] | sort_by(.createdAt) | last | .headRefName // empty') + if [ -n "$LATEST" ]; then + echo "base=${LATEST}" >> "$GITHUB_OUTPUT" + echo "Chaining onto existing promotion branch: ${LATEST}" + else + echo "base=main" >> "$GITHUB_OUTPUT" + echo "No existing promotion PR — targeting main" + fi + + - name: Create promotion PR + id: create-pr + if: steps.ahead-check.outputs.commits_ahead != '0' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + run: | + RANGE="${{ needs.check-changes.outputs.diff_range }}" + TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC") + BRANCH="${{ steps.branch.outputs.branch }}" + BASE="${{ steps.find-base.outputs.base }}" + + PR_URL=$(gh pr create \ + --base "$BASE" \ + --head "$BRANCH" \ + --title "chore: promote staging to main (${TIMESTAMP})" \ + --body "## Auto-promotion from staging CI + + **Batch range:** \`${RANGE}\` + **Promotion branch:** \`${BRANCH}\` + **Base:** \`${BASE}\` + **Triggered by:** Staging CI batch at ${TIMESTAMP} + + Waiting for gates: + - Tests: pending + - E2E: pending + - Claude Code review: pending (will post comments on this PR) + + --- + *Auto-created by staging-ci workflow*" \ + --label "staging-promotion") + + PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$') + echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT" + echo "Created promotion PR #${PR_NUM}" + + # ── Gate: wait for review, process findings, merge or block ───── + gate: + name: Staging Gate + needs: [check-changes, tests, e2e, create-promotion-pr] + if: > + always() && + needs.check-changes.outputs.has_changes == 'true' && + needs.tests.result == 'success' && + needs.e2e.result == 'success' && + needs.create-promotion-pr.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 25 + outputs: + gate_passed: ${{ steps.evaluate.outputs.passed }} + steps: + - uses: actions/checkout@v6 + with: + ref: staging + fetch-depth: 1 + + - name: Generate GitHub App token + id: app-token + if: ${{ secrets.GH_RELEASES_MANAGER_APP_ID != '' }} + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} + private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }} + + - name: Set token + id: token + run: | + if [ -n "${{ steps.app-token.outputs.token }}" ]; then + echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT" + else + echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT" + fi + + - name: Wait for Claude review job + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }} + REPO: ${{ github.repository }} + run: | + if [ -z "$PR_NUMBER" ]; then + echo "No PR number — skipping wait" + exit 0 + fi + + PR_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid' || echo "") + if [ -z "$PR_SHA" ]; then + echo "::warning::Could not get PR head SHA" + exit 0 + fi + + echo "Polling for Claude Code Review job on PR #${PR_NUMBER} (SHA: ${PR_SHA})..." + TIMEOUT=1200 # 20 minutes + ELAPSED=0 + INTERVAL=30 + + while [ "$ELAPSED" -lt "$TIMEOUT" ]; do + STATUS=$(gh api "repos/${REPO}/commits/${PR_SHA}/check-runs" \ + --jq '[.check_runs[] | select(.name == "Claude Code Review") | .conclusion // .status] | first // "pending"' 2>/dev/null || echo "pending") + + if [ "$STATUS" = "success" ] || [ "$STATUS" = "failure" ] || [ "$STATUS" = "cancelled" ]; then + echo "Claude review job completed with status: ${STATUS} (${ELAPSED}s)" + exit 0 + fi + + echo "Claude review status: ${STATUS} (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + ELAPSED=$((ELAPSED + INTERVAL)) + done + + echo "::warning::Claude review job not completed after ${TIMEOUT}s" + + - name: Process Claude review comments and create issues + id: process-findings + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }} + REPO: ${{ github.repository }} + run: | + HAS_BLOCKING=false + ISSUES_CREATED=0 + + if [ -z "$PR_NUMBER" ]; then + echo "No PR — skipping finding processing" + echo "has_blocking=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Check for "No issues found" first (clean pass) + NO_ISSUES=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq '[.[] | select(.user.login == "claude[bot]") | select(.body | test("No issues found"))] | length' 2>/dev/null || echo "0") + if [ "$NO_ISSUES" -gt 0 ]; then + echo "Claude review found no issues — gate passes" + echo "has_blocking=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Get the last Claude comment that contains findings + JQ_FILTER='[.[] | select(.user.login == "claude[bot]") | select(.body | test("Found [0-9]+ issue"))] | last' + BODY=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq "${JQ_FILTER} | .body // empty" 2>/dev/null || echo "") + COMMENT_URL=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq "${JQ_FILTER} | .html_url // empty" 2>/dev/null || echo "") + + if [ -z "$BODY" ]; then + echo "::warning::No Claude review comment found for PR #${PR_NUMBER} — treating as blocking" + echo "has_blocking=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Parse [SEVERITY:CONFIDENCE] tags from each numbered finding + # Matrix: CRITICAL always→issue, ≥80→block. HIGH ≥50→issue. MEDIUM ≥80→issue. LOW ≥80→issue. + # Use process substitution so variables propagate to parent shell + while read -r line; do + TAG=$(echo "$line" | grep -oE '^\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\]') + SEVERITY=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\1/') + CONFIDENCE=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\2/') + DESC=$(echo "$line" | sed "s/\[${SEVERITY}:${CONFIDENCE}\] *//" | head -1) + + echo "Found: [${SEVERITY}:${CONFIDENCE}] ${DESC}" + + # Check if blocking (CRITICAL ≥80) + if [ "$SEVERITY" = "CRITICAL" ] && [ "$CONFIDENCE" -ge 80 ]; then + HAS_BLOCKING=true + fi + + # Determine if this should create an issue + CREATE_ISSUE=false + case "$SEVERITY" in + CRITICAL) CREATE_ISSUE=true ;; + HIGH) [ "$CONFIDENCE" -ge 50 ] && CREATE_ISSUE=true ;; + MEDIUM) [ "$CONFIDENCE" -ge 80 ] && CREATE_ISSUE=true ;; + LOW) [ "$CONFIDENCE" -ge 80 ] && CREATE_ISSUE=true ;; + esac + + if [ "$CREATE_ISSUE" = "true" ]; then + case "$SEVERITY" in + CRITICAL) LABELS="bug,risk: high,staging-ci-review" ;; + HIGH) LABELS="bug,risk: medium,staging-ci-review" ;; + MEDIUM) LABELS="risk: medium,staging-ci-review" ;; + LOW) LABELS="risk: low,staging-ci-review" ;; + esac + + TITLE=$(echo "$DESC" | cut -c1-80) + { + echo "## [${SEVERITY}:${CONFIDENCE}] Issue Found by Staging CI Review" + echo "" + echo "**Severity:** ${SEVERITY}" + echo "**Confidence:** ${CONFIDENCE}/100" + echo "**PR comment:** ${COMMENT_URL}" + echo "" + echo "### Description" + echo "$DESC" + echo "" + echo "---" + echo "*Auto-created by staging-ci Claude Code review*" + } > /tmp/issue-body.md + + if gh issue create \ + --title "[${SEVERITY}] ${TITLE}" \ + --body-file /tmp/issue-body.md \ + --label "${LABELS}"; then + ISSUES_CREATED=$((ISSUES_CREATED + 1)) + else + echo "::warning::Failed to create issue for ${SEVERITY} finding" + fi + fi + done < <(echo "$BODY" | grep -oE '\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\].*') + + echo "Created ${ISSUES_CREATED} issues" + echo "has_blocking=${HAS_BLOCKING}" >> "$GITHUB_OUTPUT" + + - name: Evaluate gate + id: evaluate + env: + PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }} + SKIP_GATE: ${{ inputs.skip_claude_gate }} + HAS_BLOCKING: ${{ steps.process-findings.outputs.has_blocking }} + run: | + SKIP_INPUT="$SKIP_GATE" + + if [ "$HAS_BLOCKING" = "true" ]; then + echo "::warning::Claude review found blocking issues (CRITICAL ≥80 confidence)" + if [ "$SKIP_INPUT" = "true" ]; then + echo "::warning::Gate overridden by skip_claude_gate workflow input" + echo "passed=true" >> "$GITHUB_OUTPUT" + else + echo "::error::Blocking promotion due to CRITICAL findings (≥80 confidence)" + echo "::error::PR #${PR_NUMBER} left open with review comments" + echo "passed=false" >> "$GITHUB_OUTPUT" + exit 1 + fi + else + echo "No blocking findings. Gate passed." + echo "passed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Merge promotion PR + id: merge + if: steps.evaluate.outputs.passed == 'true' + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }} + run: | + if [ -n "$PR_NUMBER" ]; then + echo "Merging promotion PR #${PR_NUMBER}" + # Do NOT use --delete-branch: deleting a promotion branch closes + # any chained PRs that use it as their base (verified in ironclaw-ci-test). + # Stale promotion branches are cleaned up separately. + gh pr merge "$PR_NUMBER" --merge + echo "merged=true" >> "$GITHUB_OUTPUT" + fi + + # ── Update tested tag (always, so next batch covers only new commits) ── + update-tag: + name: Update staging-tested tag + needs: [check-changes, tests, e2e, create-promotion-pr, gate] + if: > + always() && + needs.check-changes.outputs.has_changes == 'true' && + needs.tests.result == 'success' && + needs.e2e.result == 'success' && + needs.create-promotion-pr.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: staging + fetch-depth: 1 + + - name: Update staging-tested tag + run: | + git tag -f staging-tested "${{ needs.check-changes.outputs.current_head }}" + git push origin staging-tested --force + echo "Updated staging-tested tag to ${{ needs.check-changes.outputs.current_head }}" + + # ── Report ─────────────────────────────────────────────────────── + report: + name: Staging CI Summary + needs: [check-changes, tests, e2e, create-promotion-pr, gate, update-tag] + if: always() && needs.check-changes.outputs.has_changes == 'true' + runs-on: ubuntu-latest + steps: + - name: Summary + run: | + echo "## Staging CI Batch Results" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Check | Result |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Tests | ${{ needs.tests.result }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| E2E | ${{ needs.e2e.result }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Promotion PR | ${{ needs.create-promotion-pr.result }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Gate | ${{ needs.gate.result }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Tag Updated | ${{ needs.update-tag.result }} |" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Range: ${{ needs.check-changes.outputs.diff_range }}" >> "$GITHUB_STEP_SUMMARY" + PR_NUM="${{ needs.create-promotion-pr.outputs.pr_number }}" + if [ -n "$PR_NUM" ]; then + echo "Promotion PR: #${PR_NUM}" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f0fd2bb..efa28648 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,6 @@ name: Run Tests on: + workflow_call: pull_request: push: branches: @@ -38,6 +39,9 @@ jobs: telegram-tests: name: Telegram Channel Tests + if: > + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.base_ref != 'staging') runs-on: ubuntu-latest steps: - name: Checkout repository @@ -50,6 +54,9 @@ jobs: windows-build: name: Windows Build (${{ matrix.name }}) + if: > + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.base_ref != 'staging') runs-on: windows-latest strategy: fail-fast: false @@ -74,6 +81,9 @@ jobs: wasm-wit-compat: name: WASM WIT Compatibility + if: > + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.base_ref != 'staging') runs-on: ubuntu-latest steps: - name: Checkout repository @@ -94,6 +104,9 @@ jobs: docker-build: name: Docker Build + if: > + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.base_ref != 'staging') runs-on: ubuntu-latest steps: - name: Checkout repository @@ -123,12 +136,22 @@ jobs: needs: [tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check] steps: - run: | - if [[ "${{ needs.tests.result }}" != "success" || "${{ needs.telegram-tests.result }}" != "success" || "${{ needs.wasm-wit-compat.result }}" != "success" || "${{ needs.docker-build.result }}" != "success" || "${{ needs.windows-build.result }}" != "success" ]]; then - echo "One or more jobs failed" - exit 1 - fi - # version-check only runs on PRs, so skip/success are both acceptable - if [[ "${{ needs.version-check.result }}" == "failure" ]]; then - echo "Version bump check failed" + # Unit tests must always pass + if [[ "${{ needs.tests.result }}" != "success" ]]; then + echo "Unit tests failed" exit 1 fi + # Gated jobs: must pass on promotion PRs / push, skipped on developer PRs + for job in telegram-tests wasm-wit-compat docker-build windows-build version-check; do + case "$job" in + telegram-tests) result="${{ needs.telegram-tests.result }}" ;; + wasm-wit-compat) result="${{ needs.wasm-wit-compat.result }}" ;; + docker-build) result="${{ needs.docker-build.result }}" ;; + windows-build) result="${{ needs.windows-build.result }}" ;; + version-check) result="${{ needs.version-check.result }}" ;; + esac + if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then + echo "$job failed" + exit 1 + fi + done From bcbdc273a53351dd2e7f85fbb0a7324e496a7f38 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Mon, 9 Mar 2026 23:19:25 +0000 Subject: [PATCH 2/2] Restructure CLAUDE.md into modular rules + add pr-shepherd command (#750) * refactor: restructure CLAUDE.md into modular rules and add pr-shepherd command Trim CLAUDE.md from 710 lines to 92 by moving detailed guidance into path-scoped `.claude/rules/` files that load on demand. Add a new `/pr-shepherd` command that consolidates the full PR lifecycle (review, fix, quality gate, CI fix loop, merge) into one workflow. Changes: - CLAUDE.md: keep only essentials (build commands, code style, architecture, module specs, config reference, debugging) - .claude/rules/review-discipline.md: 15+ review rules, scoped to src/**/*.rs - .claude/rules/database.md: dual-backend rules with SQL dialect translation table, scoped to src/db/** and migrations/** - .claude/rules/safety-and-sandbox.md: safety layer and sandbox rules, scoped to src/safety/**, src/sandbox/**, src/secrets/** - .claude/rules/testing.md: test tiers and patterns, scoped to src/** and tests/** - .claude/rules/tools.md: tool architecture and implementation pattern, scoped to src/tools/** and tools-src/** - .claude/commands/pr-shepherd.md: 7-phase PR lifecycle command that subsumes review-pr, respond-pr, ship, and manual CI fix loops [skip-regression-check] Co-Authored-By: Claude Opus 4.6 * fix: address review feedback on CLAUDE.md restructure - Restore project structure tree in CLAUDE.md (zmanian blocking) - Create .claude/rules/skills.md with trust model, SKILL.md format, selection pipeline, and skill tools (zmanian blocking) - Restore configuration section with key env vars (zmanian medium) - Restore "Adding a New Channel" guide (zmanian medium) - Add heartbeat mention to Workspace & Memory section (zmanian low) - Fix pr-shepherd: replace `git add -A` with specific file staging (zmanian) - Fix pr-shepherd: ask user for merge strategy instead of hardcoding --squash (zmanian) - Fix pr-shepherd: replace `--watch` with polling + 10min timeout (zmanian) - Fix testing.md: "skipped if DB is unreachable" not "expected to fail" (Copilot) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 * fix: address review comments on PR #750 - Narrow `crate::` import rule: `super::` is fine in tests and intra-module refs - Fix capabilities file naming: `.capabilities.json` sidecar, not bare `capabilities.json` - Update mechanical verification checklist to match narrowed import rule Co-Authored-By: Claude Opus 4.6 * refactor: move Bedrock docs from CLAUDE.md to src/llm/CLAUDE.md Bedrock provider details (auth, config, feature flag) belong in the LLM module spec, not the top-level guide. Added file map entry, provider table row, and dedicated section in src/llm/CLAUDE.md. Co-Authored-By: Claude Opus 4.6 * refactor: move env var config block out of CLAUDE.md Replace 20-line config block with one-liner pointing to .env.example and src/llm/CLAUDE.md. Config details are only needed during deployment, not everyday coding. Co-Authored-By: Claude Opus 4.6 * fix: use gh pr checkout for fork-safe PR checkout in pr-shepherd Replaces git fetch/checkout with gh pr checkout {number} which handles both same-repo and fork-based PRs automatically. Co-Authored-By: Claude Opus 4.6 * fix: address Copilot review round 5 on PR #750 - Add gh pr list and gh pr checkout to pr-shepherd allowed-tools - Align crate:: import rule in pr-shepherd with updated CLAUDE.md guidance - Fix vector type in database.md: BLOB (flexible dims), not F32_BLOB(1536) - Update MCP limitation: stdio/HTTP/Unix transports exist, no streaming Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .claude/commands/pr-shepherd.md | 303 +++++++++++++ .claude/rules/database.md | 63 +++ .claude/rules/review-discipline.md | 48 ++ .claude/rules/safety-and-sandbox.md | 34 ++ .claude/rules/skills.md | 56 +++ .claude/rules/testing.md | 25 ++ .claude/rules/tools.md | 39 ++ CLAUDE.md | 674 ++++------------------------ src/llm/CLAUDE.md | 13 + 9 files changed, 662 insertions(+), 593 deletions(-) create mode 100644 .claude/commands/pr-shepherd.md create mode 100644 .claude/rules/database.md create mode 100644 .claude/rules/review-discipline.md create mode 100644 .claude/rules/safety-and-sandbox.md create mode 100644 .claude/rules/skills.md create mode 100644 .claude/rules/testing.md create mode 100644 .claude/rules/tools.md diff --git a/.claude/commands/pr-shepherd.md b/.claude/commands/pr-shepherd.md new file mode 100644 index 00000000..c6dc87a1 --- /dev/null +++ b/.claude/commands/pr-shepherd.md @@ -0,0 +1,303 @@ +--- +description: Full PR lifecycle — review, fix findings, address comments, quality gate, push, CI fix loop, merge +disable-model-invocation: true +allowed-tools: Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh pr comment:*), Bash(gh pr merge:*), Bash(gh pr checks:*), Bash(gh pr edit:*), Bash(gh pr list:*), Bash(gh pr checkout:*), Bash(gh api:*), Bash(gh repo view:*), Bash(gh run view:*), Bash(gh run watch:*), Bash(git diff:*), Bash(git log:*), Bash(git fetch:*), Bash(git checkout:*), Bash(git status:*), Bash(git branch:*), Bash(git add:*), Bash(git commit:*), Bash(git push:*), Bash(git merge:*), Bash(git rebase:*), Bash(cargo fmt:*), Bash(cargo clippy:*), Bash(cargo test:*), Bash(cargo check:*), Read, Edit, Write, Grep, Glob, Agent +argument-hint: " [--fix] [--merge] [--review-only]" +--- + +# PR Shepherd + +Full PR lifecycle: review → fix → quality gate → push → CI → merge. + +Parse `$ARGUMENTS`: +- Extract PR number from bare number or `https://github.com/owner/repo/pull/123` URL. +- Flags: `--fix` (auto-fix without asking), `--merge` (merge when CI green), `--review-only` (stop after review, don't fix). +- If no PR number, detect from current branch: `gh pr list --head $(git branch --show-current) --json number --jq '.[0].number'` +- If still nothing, stop and ask the user. + +--- + +## Phase 1: Situational Awareness + +Gather everything in parallel: + +**PR metadata:** +``` +gh pr view {number} --json number,title,body,author,baseRefName,headRefName,headRefOid,state,isDraft,mergeable,mergeStateStatus,files,additions,deletions,labels,reviewRequests +``` + +**Diff:** +``` +gh pr diff {number} +gh pr diff {number} --name-only +``` + +**CI status:** +``` +gh pr checks {number} --json name,status,conclusion,detailsUrl +``` + +**Review comments (human + bot):** +``` +gh api --paginate repos/{owner}/{repo}/pulls/{number}/comments +gh api --paginate repos/{owner}/{repo}/pulls/{number}/reviews +``` + +Resolve `{owner}/{repo}`: +``` +gh repo view --json owner,name --jq '"\(.owner.login)/\(.name)"' +``` + +Save `headRefOid` — needed for posting line comments later. + +**Assess the situation and print a status card:** + +``` +PR #{number}: {title} +Author: {author} Base: {base} ← {head} +Size: +{additions} -{deletions} across {file_count} files +CI: {PASS|FAIL|PENDING|NONE} Mergeable: {yes|no|conflict} +Reviews: {N approved, N changes_requested, N comments-only, N bot-only} +Unresolved comments: {N} +Draft: {yes|no} +``` + +**Decide the mode** based on situation: +- **Has unresolved review comments** → Phase 2a (address comments first, then review remaining) +- **No reviews yet / bot-only reviews** → Phase 2b (full deep review) +- **CI failing, no review issues** → Phase 4 (jump to CI fix) +- **Everything green + approved** → Phase 6 (ready to merge) + +--- + +## Phase 2a: Address Existing Review Comments + +For each unresolved review comment or review with CHANGES_REQUESTED: + +1. **Read the referenced code** at the file and line mentioned. Never assess without reading. +2. **Classify each comment:** + - ✅ **Valid & unresolved** — needs a code fix + - ✅ **Already fixed** — a later commit addressed it + - ❌ **False positive** — explain why the code is correct + - 🔧 **Nit** — optional improvement, not blocking + +3. **Deduplicate** — bots (Copilot, Gemini) often post the same finding. Group by actual issue. + +Present a table: + +| # | Source | File:Line | Issue | Status | Planned Fix | +|---|--------|-----------|-------|--------|-------------| + +Wait for user confirmation (unless `--fix` flag set), then proceed to Phase 3. + +--- + +## Phase 2b: Deep Review (6 Lenses) + +Read EVERY changed file in full (not just diff hunks). For PRs touching >20 files, prioritize: service logic > handlers > types > tests > docs. Batch reads in parallel via Agent tool. + +### IronClaw-specific checks (always) +- No `.unwrap()` or `.expect()` in production code +- Prefer `crate::` for cross-module imports (`super::` OK in tests/intra-module) +- Error types use `thiserror` +- If persistence touched, both backends updated (postgres.rs AND libsql/) +- New tools implement `Tool` trait correctly and registered +- External tool output passes through safety layer +- Tool parameters redacted before logging/SSE +- No byte-index slicing on external strings +- Case-insensitive comparisons where needed + +### Correctness +Off-by-one, wrong operators, inverted conditions, unreachable code, type confusion, error propagation, broken invariants, TOCTOU races. + +### Edge cases & failure handling +Empty/None/zero-length input, external service failures, integer boundaries, malformed/adversarial input, partial failure handling. + +### Security (assume adversarial actors) +Auth/authz bypass, IDOR, injection (SQL/command/log/header), data leakage in logs/errors/API responses, resource exhaustion, replay/race conditions. + +### Test coverage +New public functions tested? Error paths tested? Edge cases covered? Existing tests still valid? + +### Architecture +Follows existing patterns? Unnecessary abstractions? Duplicated logic? Clean module dependencies? + +**Present findings as a table:** + +| # | Severity | Category | File:Line | Finding | Suggested Fix | +|---|----------|----------|-----------|---------|---------------| + +Severity: Critical > High > Medium > Low > Nit + +If `--review-only` flag is set, post findings as GitHub comments (see Phase 2c) and STOP. + +Otherwise, ask which findings to fix (default: all Critical + High + Medium). Then proceed to Phase 3. + +--- + +## Phase 2c: Post Review Comments on GitHub + +For each finding the user approved (or all Critical/High/Medium if `--fix`): + +**Line-specific findings** — post as PR review comments: +``` +gh api repos/{owner}/{repo}/pulls/{number}/comments \ + -f body="**{Severity}**: {finding}\n\n{explanation}\n\n**Suggested fix:** {suggestion}" \ + -f path="{file}" \ + -f commit_id="{headRefOid}" \ + -F line={line} \ + -f side="RIGHT" +``` + +**Cross-cutting/architectural findings** — post as regular PR comment: +``` +gh pr comment {number} --body "..." +``` + +--- + +## Phase 3: Fix + +Checkout the PR branch if not already on it (handles fork PRs automatically): +``` +gh pr checkout {number} +``` + +**Implement fixes** for: +1. All approved review comment fixes (from Phase 2a) +2. All approved review findings (from Phase 2b) + +Follow IronClaw conventions: +- `thiserror` for errors +- `crate::` imports +- No `.unwrap()` in production +- Both DB backends if persistence touched +- Regression test for every bug fix (enforced by commit-msg hook; bypass only with `[skip-regression-check]` if genuinely not feasible) + +After all fixes implemented, proceed to Phase 4. + +--- + +## Phase 4: Quality Gate + +Run the full IronClaw shipping checklist: + +```bash +cargo fmt +``` + +```bash +cargo clippy --all --benches --tests --examples --all-features +``` + +```bash +cargo test --lib +``` + +If persistence changes are present, also verify feature isolation: +```bash +cargo check --no-default-features --features libsql +cargo check --all-features +``` + +**If any step fails:** fix the issue and re-run. Do NOT proceed past a failing step. Loop up to 3 times per step. If still failing after 3 attempts, report the failure and stop. + +--- + +## Phase 5: Commit & Push + +Stage changed files by name (never `git add -A` — it can include unintended files): +```bash +git add path/to/changed/file1 path/to/changed/file2 +git commit -m "{message}" +``` + +Commit message format: +- For review fixes: `fix: address review findings on PR #{number}` +- For comment responses: `fix: address review comments on PR #{number}` +- For CI fixes: `fix: resolve CI failures on PR #{number}` +- Include specifics in the body (which findings/comments were addressed) + +Push: +```bash +git push origin {headRefName} +``` + +**Reply to addressed review comments on GitHub.** For each comment that was fixed, reply with the commit SHA and a brief description of what was done. For false positives, reply explaining why no change was needed. + +--- + +## Phase 6: CI Monitor & Fix Loop + +Wait briefly for CI to start, then poll (do NOT use `--watch` as it can hang indefinitely): +``` +gh pr checks {number} --json name,status,conclusion +``` + +Re-check every 30 seconds, up to 10 minutes. If still pending after 10 minutes, report status and ask the user whether to keep waiting. + +**If CI passes** → proceed to Phase 7. + +**If CI fails** (up to 3 fix attempts): + +1. Identify the failing check: + ``` + gh run view {run_id} --log-failed + ``` + If `--log-failed` shows nothing useful: + ``` + gh run view {run_id} --log | tail -100 + ``` + +2. Diagnose and fix the failure. +3. Re-run Phase 4 (quality gate). +4. Commit and push (Phase 5). +5. Go back to top of Phase 6. + +**After 3 failed CI fix attempts:** Report what's failing and why, then stop. Don't keep looping. + +--- + +## Phase 7: Merge Decision + +Print final status: +``` +PR #{number}: {title} +CI: ✅ PASS +Reviews: {summary} +Findings fixed: {N} +Comments addressed: {N} +Commits added: {N} +``` + +**Auto-merge conditions** (if `--merge` flag or user confirms): +- CI is passing +- No unresolved CHANGES_REQUESTED reviews +- PR is not draft +- PR is mergeable (no conflicts) + +If all conditions met, ask the user for merge strategy: + +"CI is green. Merge this PR? [squash/rebase/merge/no]" + +Then execute: +``` +gh pr merge {number} --{strategy} --delete-branch +``` + +If any condition NOT met, report what's blocking and let the user decide. + +--- + +## Rules + +- **Read before judging.** Never comment on code you haven't read in full. Verify line numbers. +- **Be specific.** "Line 42 returns 404 but should return 400 because X" not "this might have issues." +- **Fix the pattern, not just the instance.** When fixing a bug, grep for the same pattern across `src/`. +- **Respect the commit-msg hook.** Bug fixes need regression tests. Use `[skip-regression-check]` only if genuinely not feasible. +- **Don't over-fix.** Only change what was flagged. Don't refactor surrounding code or add improvements beyond the review scope. +- **Credit original authors.** If taking over someone else's PR, credit them in commits and comments. +- **No secrets in comments.** Never include customer data, credentials, or PII in GitHub comments. +- **Distinguish certainty.** "This IS a bug" vs "This COULD be a bug if X." Be honest. +- **Round up severity when uncertain.** Cheaper to dismiss a false alarm than miss a real bug. +- **Parallel where possible.** Use Agent tool for parallel file reads on large PRs. Batch `gh api` calls. diff --git a/.claude/rules/database.md b/.claude/rules/database.md new file mode 100644 index 00000000..07accf07 --- /dev/null +++ b/.claude/rules/database.md @@ -0,0 +1,63 @@ +--- +paths: + - "src/db/**" + - "src/history/**" + - "migrations/**" +--- +# Database Rules + +Dual-backend persistence: PostgreSQL + libSQL/Turso. **All new persistence features must support both backends.** + +See `src/db/CLAUDE.md` for full schema, dialect differences, and libSQL limitations. + +## Adding a New Operation + +1. Decide which sub-trait it belongs to (`ConversationStore`, `JobStore`, `SandboxStore`, `RoutineStore`, `ToolFailureStore`, `SettingsStore`, `WorkspaceStore`) or create a new one +2. Add the async method signature to that sub-trait in `src/db/mod.rs` +3. Implement in `src/db/postgres.rs` (delegate to `Store`/`Repository`) +4. Implement in `src/db/libsql/.rs` (use `self.connect().await?` per operation) +5. Add migration if needed: + - PostgreSQL: new `migrations/VN__description.sql` + - libSQL: add `CREATE TABLE IF NOT EXISTS` to `libsql_migrations.rs` +6. Test feature isolation: + ```bash + cargo check # postgres (default) + cargo check --no-default-features --features libsql # libsql only + cargo check --all-features # both + ``` + +## SQL Dialect Translation Checklist + +When writing SQL for both backends, translate these types: + +| PostgreSQL | libSQL | +|-----------|--------| +| `UUID` | `TEXT` | +| `TIMESTAMPTZ` | `TEXT` (ISO-8601, write with `fmt_ts()`, read with `get_ts()`) | +| `JSONB` | `TEXT` (JSON string) | +| `BOOLEAN` | `INTEGER` (0/1 -- use `get_i64(row, idx) != 0` to read) | +| `NUMERIC` | `TEXT` (preserves `rust_decimal` precision) | +| `TEXT[]` | `TEXT` (JSON-encoded array) | +| `VECTOR` | `BLOB` (flexible dimensions; vector index dropped, brute-force search fallback) | +| `jsonb_set(col, '{key}', val)` | `json_patch(col, '{"key": val}')` -- replaces top-level keys entirely, cannot do partial nested updates | +| `DEFAULT NOW()` | `DEFAULT (datetime('now'))` | +| `tsvector` + `ts_rank_cd` | FTS5 virtual table + sync triggers | + +## Schema Translation Beyond DDL + +Don't just translate `CREATE TABLE`. Also check: +- **Indexes** -- diff `CREATE INDEX` statements between backends +- **Seed data** -- check for `INSERT INTO` in migrations (e.g., `leak_detection_patterns`) +- **Triggers** -- PostgreSQL functions vs SQLite triggers (no stored procs in SQLite) + +## Transaction Safety + +Multi-step operations (INSERT+INSERT, UPDATE+DELETE, read-modify-write) MUST be wrapped in a transaction. Ask: "If this crashes between step N and N+1, is the database consistent?" If not, wrap in a transaction. Applies to both backends. + +## libSQL Connection Model + +`LibSqlBackend::connect()` creates a fresh connection per operation with `PRAGMA busy_timeout = 5000`. This is intentional -- no pool exists. Never hold connections open across `await` points. Satellite stores (`LibSqlSecretsStore`, `LibSqlWasmToolStore`) receive `Arc` via `shared_db()` and call `.connect()` themselves -- never pass a live `Connection`. + +## Fix the Pattern, Not the Instance + +When fixing a bug in one backend's SQL, always grep for the same pattern in the other. A fix to `postgres.rs` that doesn't also fix `libsql/jobs.rs` is half a fix. Same applies to satellite stores. diff --git a/.claude/rules/review-discipline.md b/.claude/rules/review-discipline.md new file mode 100644 index 00000000..74ace30a --- /dev/null +++ b/.claude/rules/review-discipline.md @@ -0,0 +1,48 @@ +--- +paths: + - "src/**/*.rs" +--- +# Review & Fix Discipline + +Hard-won lessons from code review -- follow these when fixing bugs or addressing review feedback. + +**Fix the pattern, not just the instance:** When a reviewer flags a bug (e.g., TOCTOU race in INSERT + SELECT-back), search the entire codebase for all instances of that same pattern. A fix in `SecretsStore::create()` that doesn't also fix `WasmToolStore::store()` is half a fix. + +**Propagate architectural fixes to satellite types:** If a core type changes its concurrency model (e.g., `LibSqlBackend` switches to connection-per-operation), every type that was handed a resource from the old model must also be updated. Grep for the old type across the codebase. + +**Schema translation is more than DDL:** When translating a database schema between backends (PostgreSQL to libSQL, etc.), check for: +- **Indexes** -- diff `CREATE INDEX` statements between the two schemas +- **Seed data** -- check for `INSERT INTO` in migrations (e.g., `leak_detection_patterns`) +- **Semantic differences** -- document where SQL functions behave differently (e.g., `json_patch` vs `jsonb_set`) + +**Feature flag testing:** When adding feature-gated code, test compilation with each feature in isolation: +```bash +cargo check # default features +cargo check --no-default-features --features libsql # libsql only +cargo check --all-features # all features +``` + +**Regression test with every fix:** Every bug fix must include a test that would have caught the bug. Add a `#[test]` or `#[tokio::test]` that reproduces the original failure. Exempt: changes limited to `src/channels/web/static/` or `.md` files. Use `[skip-regression-check]` in commit message or PR label if genuinely not feasible. The `commit-msg` hook and CI workflow enforce this automatically. + +**Zero clippy warnings policy:** Fix ALL clippy warnings before committing, including pre-existing ones in files you didn't change. Never leave warnings behind. + +**Transaction safety:** Multi-step database operations (INSERT+INSERT, UPDATE+DELETE, read-then-write) MUST be wrapped in a transaction. Never assume sequential calls are atomic. This applies to both postgres and libsql backends. + +**UTF-8 string safety:** Never use byte-index slicing (`&s[..n]`) on user-supplied or external strings -- it panics on multi-byte characters. Use `is_char_boundary()` or `char_indices()`. Grep for `[..` in changed files. + +**Case-insensitive comparisons:** When comparing user-supplied strings (file paths, media types, extension names), normalize to lowercase with `.to_ascii_lowercase()`. Path comparisons must be case-insensitive on macOS/Windows. + +**Decorator/wrapper trait delegation:** When adding a new method to `LlmProvider` (or any trait with decorator wrappers), update ALL wrapper types to delegate. Grep for `impl LlmProvider for` to find all implementations. Test through the full provider chain. + +**Sensitive data in logs & events:** Tool parameters and outputs MUST be redacted before logging or broadcasting via SSE/WebSocket. Use `redact_params()` before any `tracing::info!`, `JobEvent`, or SSE emission that includes tool call data. + +**Test temporary files:** Use the `tempfile` crate. Never hardcode `/tmp/...` paths. + +**Trust boundaries in multi-process architecture:** Data from worker containers is untrusted. The orchestrator MUST validate: tool domain, nesting depth (server-side tracking), and parameter sensitivity. + +**Mechanical verification before committing:** +- `cargo clippy --all --benches --tests --examples --all-features` -- zero warnings +- `grep -rnE '\.unwrap\(|\.expect\(' ` -- no panics in production +- `grep -rn 'super::' ` -- prefer `crate::` for cross-module imports (`super::` OK in tests/intra-module) +- If you fixed a pattern bug, `grep` for other instances across `src/` +- Run `scripts/pre-commit-safety.sh` to catch UTF-8, case-sensitivity, hardcoded /tmp, and logging issues diff --git a/.claude/rules/safety-and-sandbox.md b/.claude/rules/safety-and-sandbox.md new file mode 100644 index 00000000..50e1135e --- /dev/null +++ b/.claude/rules/safety-and-sandbox.md @@ -0,0 +1,34 @@ +--- +paths: + - "src/safety/**" + - "src/sandbox/**" + - "src/secrets/**" + - "src/tools/wasm/**" +--- +# Safety Layer & Sandbox Rules + +## Safety Layer + +All external tool output passes through `SafetyLayer`: +1. **Sanitizer** - Detects injection patterns, escapes dangerous content +2. **Validator** - Checks length, encoding, forbidden patterns +3. **Policy** - Rules with severity (Critical/High/Medium/Low) and actions (Block/Warn/Review/Sanitize) +4. **Leak Detector** - Scans for 15+ secret patterns at two points: tool output before LLM, and LLM responses before user + +Tool outputs are wrapped in `` XML before reaching the LLM. + +## Shell Environment Scrubbing + +The shell tool scrubs sensitive env vars before executing commands. The sanitizer detects command injection patterns (chained commands, subshells, path traversal). + +## Sandbox Policies + +| Policy | Filesystem | Network | +|--------|-----------|---------| +| ReadOnly | Read-only workspace | Allowlisted domains | +| WorkspaceWrite | Read-write workspace | Allowlisted domains | +| FullAccess | Full filesystem | Unrestricted | + +## Zero-Exposure Credential Model + +Secrets are stored encrypted on the host and injected into HTTP requests by the proxy at transit time. Container processes never see raw credential values. diff --git a/.claude/rules/skills.md b/.claude/rules/skills.md new file mode 100644 index 00000000..ded26de9 --- /dev/null +++ b/.claude/rules/skills.md @@ -0,0 +1,56 @@ +--- +paths: + - "src/skills/**" + - "skills/**" +--- +# Skills System + +SKILL.md files extend the agent's prompt with domain-specific instructions. Each skill is a YAML frontmatter block (metadata, activation criteria, required tools) followed by a markdown body injected into the LLM context. + +## Trust Model + +| Trust Level | Source | Tool Access | +|-------------|--------|-------------| +| **Trusted** | User-placed in `~/.ironclaw/skills/` or workspace `skills/` | All tools available to the agent | +| **Installed** | Downloaded from ClawHub registry (`~/.ironclaw/installed_skills/`) | Read-only tools only (no shell, file write, HTTP) | + +## SKILL.md Format + +```yaml +--- +name: my-skill +version: 0.1.0 +description: Does something useful +activation: + patterns: + - "deploy to.*production" + keywords: + - "deployment" + exclude_keywords: + - "rollback" + tags: + - "devops" + max_context_tokens: 2000 +metadata: + openclaw: + requires: + bins: [docker, kubectl] + env: [KUBECONFIG] +--- + +# Skill instructions here... +``` + +## Selection Pipeline + +1. **Gating** -- Check binary/env/config requirements; skip skills whose prerequisites are missing +2. **Scoring** -- Deterministic scoring: keywords (10/5 pts, cap 30) + patterns (20 pts, cap 40) + tags (3 pts, cap 15). `exclude_keywords` veto (score = 0 if any present) +3. **Budget** -- Select top-scoring skills within `SKILLS_MAX_TOKENS` prompt budget +4. **Attenuation** -- Minimum trust across active skills determines tool ceiling; installed skills lose dangerous tools + +## Skill Tools + +- `skill_list` -- List all discovered skills with trust level and status +- `skill_search` -- Search ClawHub registry for available skills +- `skill_install` -- Download and install a skill from ClawHub +- `skill_remove` -- Remove an installed skill diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 00000000..3d50b3ea --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,25 @@ +--- +paths: + - "src/**/*.rs" + - "tests/**" +--- +# Testing Rules + +## Test Tiers + +| Tier | Command | External deps | +|------|---------|---------------| +| Unit | `cargo test` | None | +| Integration | `cargo test --features integration` | Running PostgreSQL | +| Live | `cargo test --features integration -- --ignored` | PostgreSQL + LLM API keys | + +Run `bash scripts/check-boundaries.sh` to verify test tier gating. + +## Key Patterns + +- Unit tests in `mod tests {}` at the bottom of each file +- Async tests with `#[tokio::test]` +- No mocks, prefer real implementations or stubs +- Use `tempfile` crate for test directories, never hardcode `/tmp/` +- Regression test with every bug fix (enforced by commit-msg hook) +- Integration tests (`--test workspace_integration`) require PostgreSQL; skipped if DB is unreachable diff --git a/.claude/rules/tools.md b/.claude/rules/tools.md new file mode 100644 index 00000000..a35d9e23 --- /dev/null +++ b/.claude/rules/tools.md @@ -0,0 +1,39 @@ +--- +paths: + - "src/tools/**" + - "tools-src/**" +--- +# Tool Architecture + +**Keep tool-specific logic out of the main agent codebase.** The main agent provides generic infrastructure; tools are self-contained units that declare requirements through `.capabilities.json` sidecar files (in dev mode: `tools-src//-tool.capabilities.json`). + +Tools can be WASM (sandboxed, credential-injected, single binary) or MCP servers (ecosystem, any language, no sandbox). Both are first-class via `ironclaw tool install`. + +See `src/tools/README.md` for full architecture, adding new tools, auth JSON examples, and WASM vs MCP decision guide. + +## Tool Implementation Pattern + +```rust +#[async_trait] +impl Tool for MyTool { + fn name(&self) -> &str { "my_tool" } + fn description(&self) -> &str { "Does something useful" } + fn parameters_schema(&self) -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "param": { "type": "string", "description": "A parameter" } + }, + "required": ["param"] + }) + } + async fn execute(&self, params: serde_json::Value, ctx: &JobContext) + -> Result + { + let start = std::time::Instant::now(); + // ... do work ... + Ok(ToolOutput::text("result", start.elapsed())) + } + fn requires_sanitization(&self) -> bool { true } // External data +} +``` diff --git a/CLAUDE.md b/CLAUDE.md index e51177cb..dbdf6289 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,73 +1,37 @@ # IronClaw Development Guide -## Project Overview - -**IronClaw** is a secure personal AI assistant that protects your data and expands its capabilities on the fly. - -### Core Philosophy -- **User-first security** - Your data stays yours, encrypted and local -- **Self-expanding** - Build new tools dynamically without vendor dependency -- **Defense in depth** - Multiple security layers against prompt injection and data exfiltration -- **Always available** - Multi-channel access with proactive background execution - -### Features -- **Multi-channel input**: TUI (Ratatui), HTTP webhooks, WASM channels (Telegram, Slack), web gateway -- **Parallel job execution** with state machine and self-repair for stuck jobs -- **Sandbox execution**: Docker container isolation with network proxy and credential injection -- **Claude Code mode**: Delegate jobs to Claude CLI inside containers -- **Skills system**: SKILL.md prompt extensions with trust model, tool attenuation, and ClawHub registry -- **Routines**: Scheduled (cron) and reactive (event, webhook) task execution -- **Web gateway**: Browser UI with SSE/WebSocket real-time streaming -- **Extension management**: Install, auth, activate MCP/WASM extensions -- **Extensible tools**: Built-in tools, WASM sandbox, MCP client, dynamic builder -- **Persistent memory**: Workspace with hybrid search (FTS + vector via RRF) -- **Prompt injection defense**: Sanitizer, validator, policy rules, leak detection, shell env scrubbing -- **Multi-provider LLM**: NEAR AI, OpenAI, Anthropic, Ollama, OpenAI-compatible, Tinfoil private inference -- **Setup wizard**: 7-step interactive onboarding for first-run configuration -- **Heartbeat system**: Proactive periodic execution with checklist +**IronClaw** is a secure personal AI assistant — user-first security, self-expanding tools, defense in depth, multi-channel access with proactive background execution. ## Build & Test ```bash -# Format code -cargo fmt - -# Lint (fix ALL warnings before committing, including pre-existing ones) -cargo clippy --all --benches --tests --examples --all-features - -# Run all tests -cargo test - -# Run specific test -cargo test test_name - -# Run with logging -RUST_LOG=ironclaw=debug cargo run - -# Run integration tests (may require running services/DB) -cargo test --test workspace_integration -cargo test --test ws_gateway_integration -cargo test --test heartbeat_integration - -# Run E2E tests (Python/Playwright — requires a running ironclaw instance) -# See tests/e2e/CLAUDE.md for full setup instructions -cd tests/e2e -python -m venv .venv && source .venv/bin/activate # On Windows: .venv\Scripts\activate -pip install -e . -playwright install chromium -pytest scenarios/ # all scenarios -pytest scenarios/test_chat.py # specific scenario +cargo fmt # format +cargo clippy --all --benches --tests --examples --all-features # lint (zero warnings) +cargo test # unit tests +cargo test --features integration # + PostgreSQL tests +RUST_LOG=ironclaw=debug cargo run # run with logging ``` -### Test Tiers +E2E tests: see `tests/e2e/CLAUDE.md`. -| Tier | Command | What runs | External deps | -|------|---------|-----------|---------------| -| Unit | `cargo test` | All `mod tests` + self-contained integration tests | None | -| Integration | `cargo test --features integration` | + PostgreSQL-dependent tests | Running PostgreSQL | -| Live | `cargo test --features integration -- --ignored` | + LLM-dependent tests | PostgreSQL + LLM API keys | +## Code Style -Run `bash scripts/check-boundaries.sh` to verify test tier gating and other architecture rules. +- Prefer `crate::` for cross-module imports; `super::` is fine in tests and intra-module refs +- No `pub use` re-exports unless exposing to downstream consumers +- No `.unwrap()` or `.expect()` in production code (tests are fine) +- Use `thiserror` for error types in `error.rs` +- Map errors with context: `.map_err(|e| SomeError::Variant { reason: e.to_string() })?` +- Prefer strong types over strings (enums, newtypes) +- Keep functions focused, extract helpers when logic is reused +- Comments for non-obvious logic only + +## Architecture + +Prefer generic/extensible architectures over hardcoding specific integrations. Ask clarifying questions about the desired abstraction level before implementing. + +Key traits for extensibility: `Database`, `Channel`, `Tool`, `LlmProvider`, `SuccessEvaluator`, `EmbeddingProvider`, `NetworkPolicyDecider`, `Hook`, `Observer`, `Tunnel`. + +All I/O is async with tokio. Use `Arc` for shared state, `RwLock` for concurrent access. ## Project Structure @@ -95,78 +59,35 @@ src/ │ ├── channel.rs # Channel trait, IncomingMessage, OutgoingResponse │ ├── manager.rs # ChannelManager merges streams │ ├── cli/ # Full TUI with Ratatui -│ │ ├── mod.rs # TuiChannel implementation -│ │ ├── app.rs # Application state -│ │ ├── render.rs # UI rendering -│ │ ├── events.rs # Input handling -│ │ ├── overlay.rs # Approval overlays -│ │ └── composer.rs # Message composition │ ├── http.rs # HTTP webhook (axum) with secret validation │ ├── webhook_server.rs # Unified HTTP server composing all webhook routes │ ├── repl.rs # Simple REPL (for testing) │ ├── web/ # Web gateway (browser UI) — see src/channels/web/CLAUDE.md │ └── wasm/ # WASM channel runtime -│ ├── mod.rs -│ ├── bundled.rs # Bundled channel discovery -│ ├── capabilities.rs # Channel-specific capabilities (HTTP endpoint, emit rate) -│ ├── error.rs # WASM channel error types -│ ├── runtime.rs # WASM channel execution runtime -│ └── wrapper.rs # Channel trait wrapper for WASM modules │ ├── cli/ # CLI subcommands (clap) │ ├── mod.rs # Cli struct, Command enum (run/onboard/config/tool/registry/mcp/memory/pairing/service/doctor/status/completion) -│ ├── config.rs # config list/get/set subcommands -│ ├── tool.rs # tool install/list/remove subcommands -│ ├── registry.rs # registry list/install subcommands -│ ├── mcp.rs # mcp add/auth/list/test subcommands -│ ├── memory.rs # memory search/read/write subcommands -│ ├── pairing.rs # pairing list/approve subcommands -│ ├── service.rs # service install/start/stop subcommands -│ ├── doctor.rs # Active health diagnostics -│ ├── status.rs # System health/status display -│ ├── completion.rs # Shell completion script generation -│ └── oauth_defaults.rs # Default OAuth redirect URIs +│ └── config.rs, tool.rs, registry.rs, mcp.rs, memory.rs, pairing.rs, service.rs, doctor.rs, status.rs, completion.rs │ ├── registry/ # Extension registry catalog -│ ├── mod.rs # Public API; re-exports RegistryCatalog, RegistryInstaller, manifest types │ ├── manifest.rs # ExtensionManifest, ArtifactSpec, BundleDefinition types │ ├── catalog.rs # RegistryCatalog: load from filesystem and embedded JSON -│ ├── installer.rs # RegistryInstaller: download, verify, install WASM artifacts -│ ├── artifacts.rs # Artifact download and caching -│ └── embedded.rs # Catalog compiled into binary at build time (via build.rs) +│ └── installer.rs # RegistryInstaller: download, verify, install WASM artifacts │ -├── hooks/ # Lifecycle hooks for intercepting agent operations -│ ├── mod.rs # 6 HookPoints: BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse -│ ├── hook.rs # Hook trait, HookContext, HookEvent, HookOutcome, HookFailureMode -│ ├── registry.rs # HookRegistry: register, prioritize, execute hooks -│ └── bundled.rs # Built-in hooks: rule-based filters, webhook forwarders, HookBundleConfig +├── hooks/ # Lifecycle hooks (6 points: BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse) │ -├── tunnel/ # Tunnel abstraction for public internet exposure -│ ├── mod.rs # Tunnel trait, TunnelProviderConfig, create_tunnel() factory -│ ├── cloudflare.rs # CloudflareTunnel (cloudflared binary) -│ ├── ngrok.rs # NgrokTunnel -│ ├── tailscale.rs # TailscaleTunnel (serve/funnel modes) -│ ├── custom.rs # CustomTunnel (arbitrary command with {host}/{port}) -│ └── none.rs # NoneTunnel (local-only, no exposure) +├── tunnel/ # Tunnel abstraction (cloudflare, ngrok, tailscale, custom, none) │ -├── observability/ # Pluggable event/metric recording -│ ├── mod.rs # create_observer() factory, ObservabilityConfig -│ ├── traits.rs # Observer trait, ObserverEvent, ObserverMetric -│ ├── noop.rs # NoopObserver (zero overhead, default) -│ ├── log.rs # LogObserver (tracing-based) -│ └── multi.rs # MultiObserver (fan-out to multiple backends) +├── observability/ # Pluggable event/metric recording (noop, log, multi) │ ├── orchestrator/ # Internal HTTP API for sandbox containers -│ ├── mod.rs │ ├── api.rs # Axum endpoints (LLM proxy, events, prompts) │ ├── auth.rs # Per-job bearer token store │ └── job_manager.rs # Container lifecycle (create, stop, cleanup) │ ├── worker/ # Runs inside Docker containers -│ ├── mod.rs │ ├── runtime.rs # Worker execution loop (tool calls, LLM) │ ├── claude_bridge.rs # Claude Code bridge (spawns claude CLI) -│ ├── api.rs # HTTP client to orchestrator │ └── proxy_llm.rs # LlmProvider that proxies through orchestrator │ ├── safety/ # Prompt injection defense @@ -174,179 +95,68 @@ src/ │ ├── validator.rs # Input validation (length, encoding, patterns) │ ├── policy.rs # PolicyRule system with severity/actions │ ├── leak_detector.rs # Secret detection (API keys, tokens, etc.) -│ └── credential_detect.rs # HTTP request credential detection (headers, URL params) +│ └── credential_detect.rs # HTTP request credential detection │ ├── llm/ # Multi-provider LLM integration — see src/llm/CLAUDE.md │ ├── tools/ # Extensible tool system │ ├── tool.rs # Tool trait, ToolOutput, ToolError │ ├── registry.rs # ToolRegistry for discovery -│ ├── sandbox.rs # Process-based sandbox (stub, superseded by wasm/) -│ ├── rate_limiter.rs # Shared sliding-window rate limiter for built-in and WASM tools -│ ├── builtin/ # Built-in tools -│ │ ├── echo.rs, time.rs, json.rs, http.rs -│ │ ├── web_fetch.rs # GET URL → clean Markdown (readability + html-to-md conversion) -│ │ ├── file.rs # ReadFile, WriteFile, ListDir, ApplyPatch -│ │ ├── shell.rs # Shell command execution -│ │ ├── memory.rs # Memory tools (search, write, read, tree) -│ │ ├── message.rs # MessageTool: agent proactively messages users on any channel -│ │ ├── job.rs # CreateJob, ListJobs, JobStatus, CancelJob -│ │ ├── routine.rs # routine_create/list/update/delete/history -│ │ ├── extension_tools.rs # Extension install/auth/activate/remove -│ │ ├── skill_tools.rs # skill_list/search/install/remove tools -│ │ ├── secrets_tools.rs # secret_list/secret_delete (zero-exposure: no values exposed) -│ │ ├── html_converter.rs # HTML→Markdown via readability + html-to-markdown-rs -│ │ ├── path_utils.rs # Shared path validation/canonicalization helpers -│ │ └── marketplace.rs, ecommerce.rs, taskrabbit.rs, restaurant.rs (stubs) +│ ├── rate_limiter.rs # Shared sliding-window rate limiter +│ ├── builtin/ # Built-in tools (echo, time, json, http, web_fetch, file, shell, memory, message, job, routine, extension_tools, skill_tools, secrets_tools) │ ├── builder/ # Dynamic tool building -│ │ ├── core.rs # BuildRequirement, SoftwareType, Language -│ │ ├── templates.rs # Project scaffolding -│ │ ├── testing.rs # Test harness integration -│ │ └── validation.rs # WASM validation -│ ├── mcp/ # Model Context Protocol -│ │ ├── client.rs # MCP client over HTTP -│ │ ├── protocol.rs # JSON-RPC types -│ │ └── session.rs # MCP session management (Mcp-Session-Id header, per-server state) -│ └── wasm/ # Full WASM sandbox (wasmtime) -│ ├── runtime.rs # Module compilation and caching -│ ├── wrapper.rs # Tool trait wrapper for WASM modules -│ ├── host.rs # Host functions (logging, time, workspace) -│ ├── limits.rs # Fuel metering and memory limiting -│ ├── allowlist.rs # Network endpoint allowlisting -│ ├── credential_injector.rs # Safe credential injection -│ ├── loader.rs # WASM tool discovery from filesystem -│ ├── rate_limiter.rs # Per-tool rate limiting -│ ├── error.rs # WASM-specific error types -│ └── storage.rs # Linear memory persistence +│ ├── mcp/ # Model Context Protocol client +│ └── wasm/ # Full WASM sandbox (wasmtime) — runtime, host functions, fuel metering, allowlist, credential injection │ ├── db/ # Dual-backend persistence (PostgreSQL + libSQL) — see src/db/CLAUDE.md │ -├── workspace/ # Persistent memory system (OpenClaw-inspired) -│ ├── mod.rs # Workspace struct, memory operations -│ ├── document.rs # MemoryDocument, MemoryChunk, WorkspaceEntry -│ ├── chunker.rs # Document chunking (800 tokens, 15% overlap) -│ ├── embeddings.rs # EmbeddingProvider trait, OpenAI implementation -│ ├── search.rs # Hybrid search with RRF algorithm -│ └── repository.rs # PostgreSQL CRUD and search operations +├── workspace/ # Persistent memory system — see src/workspace/README.md │ -├── context/ # Job context isolation -│ ├── state.rs # JobState enum, JobContext, state machine -│ ├── memory.rs # ActionRecord, ConversationMemory -│ └── manager.rs # ContextManager for concurrent jobs -│ -├── estimation/ # Cost/time/value estimation -│ ├── cost.rs # CostEstimator -│ ├── time.rs # TimeEstimator -│ ├── value.rs # ValueEstimator (profit margins) -│ └── learner.rs # Exponential moving average learning -│ -├── evaluation/ # Success evaluation -│ ├── success.rs # SuccessEvaluator trait, RuleBasedEvaluator, LlmEvaluator -│ └── metrics.rs # MetricsCollector, QualityMetrics +├── context/ # Job context isolation (JobState, JobContext, ContextManager) +├── estimation/ # Cost/time/value estimation with EMA learning +├── evaluation/ # Success evaluation (rule-based, LLM-based) │ ├── sandbox/ # Docker execution sandbox -│ ├── mod.rs # Public API, default allowlist -│ ├── config.rs # SandboxConfig, SandboxPolicy enum +│ ├── config.rs # SandboxConfig, SandboxPolicy enum (ReadOnly/WorkspaceWrite/FullAccess) │ ├── manager.rs # SandboxManager orchestration │ ├── container.rs # ContainerRunner, Docker lifecycle -│ ├── error.rs # SandboxError types -│ └── proxy/ # Network proxy for containers -│ ├── mod.rs # NetworkProxyBuilder -│ ├── http.rs # HttpProxy, CredentialResolver trait -│ ├── policy.rs # NetworkPolicyDecider trait -│ └── allowlist.rs # DomainAllowlist validation +│ └── proxy/ # Network proxy: domain allowlist, credential injection, CONNECT tunnel │ -├── secrets/ # Secrets management -│ ├── mod.rs # SecretsStore trait, public API -│ ├── types.rs # Core types (Secret, SecretRef, SecretMetadata) -│ ├── crypto.rs # AES-256-GCM encryption -│ ├── keychain.rs # OS keychain integration (macOS Keychain, GNOME Keyring) for master key -│ └── store.rs # Encrypted secret storage +├── secrets/ # Secrets management (AES-256-GCM, OS keychain for master key) │ -├── setup/ # Onboarding wizard (spec: src/setup/README.md) -│ ├── mod.rs # Entry point, check_onboard_needed() -│ ├── wizard.rs # 7-step interactive wizard -│ ├── channels.rs # Channel setup helpers -│ └── prompts.rs # Terminal prompts (select, confirm, secret) +├── setup/ # 7-step onboarding wizard — see src/setup/README.md │ -├── skills/ # SKILL.md prompt extension system -│ ├── mod.rs # Core types (SkillTrust, LoadedSkill) -│ ├── registry.rs # SkillRegistry: discover, install, remove -│ ├── selector.rs # Deterministic scoring prefilter -│ ├── attenuation.rs # Trust-based tool ceiling -│ ├── gating.rs # Requirement checks (bins, env, config) -│ ├── parser.rs # SKILL.md frontmatter + markdown parser -│ └── catalog.rs # ClawHub registry client +├── skills/ # SKILL.md prompt extension system — see .claude/rules/skills.md │ -└── history/ # Persistence - ├── store.rs # PostgreSQL repositories - └── analytics.rs # Aggregation queries (JobStats, ToolStats) +└── history/ # Persistence (PostgreSQL repositories, analytics) tests/ ├── *.rs # Integration tests (workspace, heartbeat, WS gateway, pairing, etc.) -├── test-pages/ # HTML→Markdown conversion fixtures (CNN, Medium, Yahoo) +├── test-pages/ # HTML→Markdown conversion fixtures └── e2e/ # Python/Playwright E2E scenarios (see tests/e2e/CLAUDE.md) ``` -## Key Patterns +## Database -### Architecture +Dual-backend: PostgreSQL + libSQL/Turso. **All new persistence features must support both backends.** See `src/db/CLAUDE.md` and `.claude/rules/database.md`. -When designing new features or systems, always prefer generic/extensible architectures over hardcoding specific integrations. Ask clarifying questions about the desired abstraction level before implementing. +## Module Specs -### Error Handling -- Use `thiserror` for error types in `error.rs` -- Never use `.unwrap()` or `.expect()` in production code (tests are fine) -- Map errors with context: `.map_err(|e| SomeError::Variant { reason: e.to_string() })?` -- Before committing, grep for `.unwrap()` and `.expect(` in changed files to catch violations mechanically +When modifying a module with a spec, read the spec first. Code follows spec; spec is the tiebreaker. -### Async -- All I/O is async with tokio -- Use `Arc` for shared state across tasks -- Use `RwLock` for concurrent read/write access +| Module | Spec | +|--------|------| +| `src/agent/` | `src/agent/CLAUDE.md` | +| `src/channels/web/` | `src/channels/web/CLAUDE.md` | +| `src/db/` | `src/db/CLAUDE.md` | +| `src/llm/` | `src/llm/CLAUDE.md` | +| `src/setup/` | `src/setup/README.md` | +| `src/tools/` | `src/tools/README.md` | +| `src/workspace/` | `src/workspace/README.md` | +| `tests/e2e/` | `tests/e2e/CLAUDE.md` | -### Traits for Extensibility -- `Database` - Add new database backends (must implement all ~78 methods) -- `Channel` - Add new input sources -- `Tool` - Add new capabilities -- `LlmProvider` - Add new LLM backends -- `SuccessEvaluator` - Custom evaluation logic -- `EmbeddingProvider` - Add embedding backends (workspace search) -- `NetworkPolicyDecider` - Custom network access policies for sandbox containers -- `Hook` - Lifecycle hook at 6 interception points (BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse) -- `Observer` - Observability backend (noop/log/multi; future: OpenTelemetry, Prometheus) -- `Tunnel` - Tunnel provider for public internet exposure +## Job State Machine -### Tool Implementation -```rust -#[async_trait] -impl Tool for MyTool { - fn name(&self) -> &str { "my_tool" } - fn description(&self) -> &str { "Does something useful" } - fn parameters_schema(&self) -> serde_json::Value { - serde_json::json!({ - "type": "object", - "properties": { - "param": { "type": "string", "description": "A parameter" } - }, - "required": ["param"] - }) - } - - async fn execute(&self, params: serde_json::Value, ctx: &JobContext) - -> Result - { - let start = std::time::Instant::now(); - // ... do work ... - Ok(ToolOutput::text("result", start.elapsed())) - } - - fn requires_sanitization(&self) -> bool { true } // External data -} -``` - -### State Transitions -Job states follow a defined state machine in `context/state.rs`: ``` Pending -> InProgress -> Completed -> Submitted -> Accepted \-> Failed @@ -354,315 +164,17 @@ Pending -> InProgress -> Completed -> Submitted -> Accepted \-> Failed ``` -### Code Style +## Skills System -- Use `crate::` imports, not `super::` -- No `pub use` re-exports unless exposing to downstream consumers -- Prefer strong types over strings (enums, newtypes) -- Keep functions focused, extract helpers when logic is reused -- Comments for non-obvious logic only +SKILL.md files extend the agent's prompt with domain-specific instructions. See `.claude/rules/skills.md` for full details. -### Review & Fix Discipline - -Hard-won lessons from code review -- follow these when fixing bugs or addressing review feedback. - -**Fix the pattern, not just the instance:** When a reviewer flags a bug (e.g., TOCTOU race in INSERT + SELECT-back), search the entire codebase for all instances of that same pattern. A fix in `SecretsStore::create()` that doesn't also fix `WasmToolStore::store()` is half a fix. - -**Propagate architectural fixes to satellite types:** If a core type changes its concurrency model (e.g., `LibSqlBackend` switches to connection-per-operation), every type that was handed a resource from the old model (e.g., `LibSqlSecretsStore`, `LibSqlWasmToolStore` holding a single `Connection`) must also be updated. Grep for the old type across the codebase. - -**Schema translation is more than DDL:** When translating a database schema between backends (PostgreSQL to libSQL, etc.), check for: -- **Indexes** -- diff `CREATE INDEX` statements between the two schemas -- **Seed data** -- check for `INSERT INTO` in migrations (e.g., `leak_detection_patterns`) -- **Semantic differences** -- document where SQL functions behave differently (e.g., `json_patch` vs `jsonb_set`) - -**Feature flag testing:** When adding feature-gated code, test compilation with each feature in isolation: -```bash -cargo check # default features -cargo check --no-default-features --features libsql # libsql only -cargo check --all-features # all features -``` -Dead code behind the wrong `#[cfg]` gate will only show up when building with a single feature. - -**Regression test with every fix:** Every bug fix must include a test that would have caught the bug. Add a `#[test]` or `#[tokio::test]` that reproduces the original failure. Exempt: changes limited to `src/channels/web/static/` or `.md` files. Use `[skip-regression-check]` in commit message or PR label if genuinely not feasible. The `commit-msg` hook and CI workflow enforce this automatically. - -**Zero clippy warnings policy:** Fix ALL clippy warnings before committing, including pre-existing ones in files you didn't change. Never leave warnings behind — treat `cargo clippy` output as a zero-tolerance gate. - -**Transaction safety:** Multi-step database operations (INSERT+INSERT, UPDATE+DELETE, read-then-write) MUST be wrapped in a transaction. Never assume sequential calls are atomic. Before committing DB code, ask: "If this crashes between step N and N+1, is the database consistent?" If not, wrap in a transaction. This applies to both postgres and libsql backends. - -**UTF-8 string safety:** Never use byte-index slicing (`&s[..n]`) on user-supplied or external strings — it panics on multi-byte characters. Use `is_char_boundary()` to walk backwards from the desired length, or iterate with `char_indices()`. Grep for `[..` in changed files to catch violations. - -**Case-insensitive comparisons:** When comparing user-supplied strings (file paths, media types, extension names), always normalize to lowercase first with `.to_ascii_lowercase()`. On case-insensitive filesystems (macOS, Windows), path comparisons must be case-insensitive. File extension checks (`.png`, `.jpg`) and media type checks (`image/jpeg`) are common offenders. - -**Decorator/wrapper trait delegation:** When adding a new method to `LlmProvider` (or any trait with decorator wrappers), you MUST update ALL wrapper types to delegate to their inner provider. Grep for `impl LlmProvider for` to find all implementations. Add a test that exercises the method through the full provider chain (`build_provider_chain()`), not just the base impl. - -**Sensitive data in logs & events:** Tool parameters and outputs MUST be redacted before logging or broadcasting via SSE/WebSocket. Use `redact_params()` before any `tracing::info!`, `JobEvent`, or SSE emission that includes tool call data. Never log raw parameters from tool calls. - -**Test temporary files:** Use the `tempfile` crate for test directories/files. Never hardcode `/tmp/...` paths — they collide in parallel test runs and break on non-Unix platforms. - -**Trust boundaries in multi-process architecture:** Data from worker containers is untrusted. The orchestrator MUST validate: tool domain (never execute `Container`-domain tools on the host), nesting depth (server-side tracking, not client-supplied), and parameter sensitivity (redact before logging/broadcasting). - -**Mechanical verification before committing:** Run these checks on changed files before committing: -- `cargo clippy --all --benches --tests --examples --all-features` -- zero warnings -- `grep -rnE '\.unwrap\(|\.expect\(' ` -- no panics in production -- `grep -rn 'super::' ` -- use `crate::` imports -- If you fixed a pattern bug, `grep` for other instances of that pattern across `src/` -- Fix commits must include regression tests (enforced by `commit-msg` hook; bypass with `[skip-regression-check]`) -- Run `scripts/pre-commit-safety.sh` to catch UTF-8, case-sensitivity, hardcoded /tmp, and logging issues +- **Trust model**: Trusted (user-placed in `~/.ironclaw/skills/` or workspace `skills/`, full tool access) vs Installed (registry, read-only tools) +- **Selection pipeline**: gating (check bin/env/config requirements) -> scoring (keywords/patterns/tags) -> budget (fit within `SKILLS_MAX_TOKENS`) -> attenuation (trust-based tool ceiling) +- **Skill tools**: `skill_list`, `skill_search`, `skill_install`, `skill_remove` ## Configuration -Environment variables (see `.env.example`): -```bash -# Database backend (default: postgres) -DATABASE_BACKEND=postgres # or "libsql" / "turso" -DATABASE_URL=postgres://user:pass@localhost/ironclaw -LIBSQL_PATH=~/.ironclaw/ironclaw.db # libSQL local path (default) -# LIBSQL_URL=libsql://xxx.turso.io # Turso cloud (optional) -# LIBSQL_AUTH_TOKEN=xxx # Required with LIBSQL_URL - -# NEAR AI (when LLM_BACKEND=nearai, the default) -# Two auth modes: session token (default) or API key -# Session token auth (default): uses browser OAuth on first run -NEARAI_SESSION_TOKEN=sess_... # hosting providers: set this -NEARAI_BASE_URL=https://private.near.ai -# API key auth: set NEARAI_API_KEY, base URL defaults to cloud-api.near.ai -# NEARAI_API_KEY=... # API key from cloud.near.ai -NEARAI_MODEL=claude-3-5-sonnet-20241022 - -# Agent settings -AGENT_NAME=ironclaw -MAX_PARALLEL_JOBS=5 - -# Embeddings (for semantic memory search) -OPENAI_API_KEY=sk-... # For OpenAI embeddings -# Or use NEAR AI embeddings: -# EMBEDDING_PROVIDER=nearai -# EMBEDDING_ENABLED=true -EMBEDDING_MODEL=text-embedding-3-small # or text-embedding-3-large - -# Heartbeat (proactive periodic execution) -HEARTBEAT_ENABLED=true -HEARTBEAT_INTERVAL_SECS=1800 # 30 minutes -HEARTBEAT_NOTIFY_CHANNEL=tui -HEARTBEAT_NOTIFY_USER=default - -# Web gateway -GATEWAY_ENABLED=true -GATEWAY_HOST=127.0.0.1 -GATEWAY_PORT=3001 -GATEWAY_AUTH_TOKEN=changeme # Required for API access -GATEWAY_USER_ID=default - -# Docker sandbox -SANDBOX_ENABLED=true -SANDBOX_IMAGE=ironclaw-worker:latest -SANDBOX_MEMORY_LIMIT_MB=512 -SANDBOX_TIMEOUT_SECS=1800 -SANDBOX_CPU_LIMIT=1.0 # CPU cores per container -SANDBOX_NETWORK_PROXY=true # Enable network proxy for containers -SANDBOX_PROXY_PORT=8080 # Proxy listener port -SANDBOX_DEFAULT_POLICY=workspace_write # ReadOnly, WorkspaceWrite, FullAccess - -# Claude Code mode (runs inside sandbox containers) -CLAUDE_CODE_ENABLED=false -CLAUDE_CODE_MODEL=claude-sonnet-4-20250514 -CLAUDE_CODE_MAX_TURNS=50 -CLAUDE_CODE_CONFIG_DIR=/home/worker/.claude - -# Routines (scheduled/reactive execution) -ROUTINES_ENABLED=true -ROUTINES_CRON_INTERVAL=60 # Tick interval in seconds -ROUTINES_MAX_CONCURRENT=3 - -# Skills system -SKILLS_ENABLED=true -SKILLS_MAX_TOKENS=4000 # Max prompt budget per turn -SKILLS_CATALOG_URL=https://clawhub.dev # ClawHub registry URL -SKILLS_AUTO_DISCOVER=true # Scan skill directories on startup - -# Tinfoil private inference -TINFOIL_API_KEY=... # Required when LLM_BACKEND=tinfoil -TINFOIL_MODEL=kimi-k2-5 # Default model - -# AWS Bedrock (native Converse API, requires --features bedrock) -# LLM_BACKEND=bedrock -# BEDROCK_REGION=us-east-1 # AWS region -# BEDROCK_MODEL=anthropic.claude-opus-4-6-v1 # Required model ID -# BEDROCK_CROSS_REGION=us # Cross-region prefix (us/eu/apac/global) -# AWS_PROFILE=my-profile # Named profile (SSO/assume-role) - -# Tunnel (public internet exposure for webhooks) -TUNNEL_URL=https://abc123.ngrok.io # Static public URL (manual tunnel) -# Or use a managed tunnel provider: -TUNNEL_PROVIDER=none # none (default), cloudflare, tailscale, ngrok, custom -TUNNEL_CF_TOKEN=... # Required for TUNNEL_PROVIDER=cloudflare -TUNNEL_NGROK_TOKEN=... # Required for TUNNEL_PROVIDER=ngrok -# TUNNEL_NGROK_DOMAIN=... # Custom domain (paid ngrok plan) -# TUNNEL_TS_FUNNEL=true # Use tailscale funnel (public) vs serve (tailnet) -TUNNEL_CUSTOM_COMMAND=... # Command with {host}/{port} for custom providers - -# Observability backend -OBSERVABILITY_BACKEND=none # none/noop (default) or log -``` - -### LLM Providers - -Backends: `nearai` (default), `openai`, `anthropic`, `ollama`, `openai_compatible`, `tinfoil`, `bedrock` (requires `--features bedrock`) — set via `LLM_BACKEND`. See [src/llm/CLAUDE.md](src/llm/CLAUDE.md) for per-provider auth and configuration details. - -**AWS Bedrock** -- Uses the native Converse API via `aws-sdk-bedrockruntime`. Requires `--features bedrock` at build time (not included in default features due to heavy AWS SDK dependencies). Supports standard AWS auth methods: IAM credentials (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`), SSO profiles (`AWS_PROFILE`), and instance roles. Configure with `BEDROCK_REGION` (default: `us-east-1`), `BEDROCK_MODEL` (required, e.g., `anthropic.claude-opus-4-6-v1`), and `BEDROCK_CROSS_REGION` (optional: `us`, `eu`, `apac`, `global` for cross-region inference profiles). The SDK credential chain resolves auth automatically from the environment. - -## Database - -Dual-backend persistence (PostgreSQL + libSQL/Turso). **All new persistence features must support both backends** — see [src/db/CLAUDE.md](src/db/CLAUDE.md) for schema, SQL dialect differences, adding operations, and libSQL limitations. - -Implement every new operation in both `src/db/postgres.rs` and `src/db/libsql/mod.rs`. Test in isolation: -```bash -cargo check # postgres (default) -cargo check --no-default-features --features libsql # libsql only -cargo check --all-features # both -``` - -Database configuration: see Configuration section above. - -## Safety Layer - -All external tool output passes through `SafetyLayer`: -1. **Sanitizer** - Detects injection patterns, escapes dangerous content -2. **Validator** - Checks length, encoding, forbidden patterns -3. **Policy** - Rules with severity (Critical/High/Medium/Low) and actions (Block/Warn/Review/Sanitize) -4. **Leak Detector** - Scans for 15+ secret patterns (API keys, tokens, private keys, connection strings) at two points: tool output before it reaches the LLM, and LLM responses before they reach the user. Actions per pattern: Block (reject entirely), Redact (mask the secret), or Warn (flag but allow) - -Tool outputs are wrapped before reaching LLM: -```xml - -[escaped content] - -``` - -### Shell Environment Scrubbing - -The shell tool (`src/tools/builtin/shell.rs`) scrubs sensitive environment variables before executing commands, preventing secrets from leaking through `env`, `printenv`, or `$VAR` expansion. The sanitizer (`src/safety/sanitizer.rs`) also detects command injection patterns (chained commands, subshells, path traversal) and blocks or escapes them based on policy rules. - -## Skills System - -Skills are SKILL.md files that extend the agent's prompt with domain-specific instructions. Each skill is a YAML frontmatter block (metadata, activation criteria, required tools) followed by a markdown body that gets injected into the LLM context when the skill activates. - -### Trust Model - -| Trust Level | Source | Tool Access | -|-------------|--------|-------------| -| **Trusted** | User-placed in `~/.ironclaw/skills/` or workspace `skills/` | All tools available to the agent | -| **Installed** | Downloaded from ClawHub registry | Read-only tools only (no shell, file write, HTTP) | - -### SKILL.md Format - -```yaml ---- -name: my-skill -version: 0.1.0 -description: Does something useful -activation: - patterns: - - "deploy to.*production" - keywords: - - "deployment" - max_context_tokens: 2000 -metadata: - openclaw: - requires: - bins: [docker, kubectl] - env: [KUBECONFIG] ---- - -# Deployment Skill - -Instructions for the agent when this skill activates... -``` - -### Selection Pipeline - -1. **Gating** -- Check binary/env/config requirements; skip skills whose prerequisites are missing -2. **Scoring** -- Deterministic scoring against message content using keywords, tags, and regex patterns -3. **Budget** -- Select top-scoring skills that fit within `SKILLS_MAX_TOKENS` prompt budget -4. **Attenuation** -- Apply trust-based tool ceiling; installed skills lose access to dangerous tools - -### Skill Tools - -Four built-in tools for managing skills at runtime: -- **`skill_list`** -- List all discovered skills with trust level and status -- **`skill_search`** -- Search ClawHub registry for available skills -- **`skill_install`** -- Download and install a skill from ClawHub -- **`skill_remove`** -- Remove an installed skill - -### Skill Directories - -- `~/.ironclaw/skills/` -- User's global skills (trusted) -- `/skills/` -- Per-workspace skills (trusted) -- `~/.ironclaw/installed_skills/` -- Registry-installed skills (installed trust) - -### Testing Skills - -- `skills/web-ui-test/` -- Manual test checklist for the web gateway UI via Claude for Chrome extension. Covers connection, chat, skills search/install/remove, and other tabs. - -Skills configuration: see Configuration section above. - -## Docker Sandbox - -The `src/sandbox/` module provides Docker-based isolation for job execution with a network proxy that controls outbound access and injects credentials. - -### Sandbox Policies - -| Policy | Filesystem | Network | Use Case | -|--------|-----------|---------|----------| -| **ReadOnly** | Read-only workspace mount | Allowlisted domains only | Analysis, code review | -| **WorkspaceWrite** | Read-write workspace mount | Allowlisted domains only | Code generation, file edits | -| **FullAccess** | Full filesystem | Unrestricted | Trusted admin tasks | - -### Network Proxy - -Containers route all HTTP/HTTPS traffic through a host-side proxy (`src/sandbox/proxy/`): -- **Domain allowlist** -- Only allowlisted domains are reachable (default: package registries, docs sites, GitHub, common APIs) -- **Credential injection** -- The `CredentialResolver` trait injects auth headers into proxied requests so secrets never enter the container environment -- **CONNECT tunnel** -- HTTPS traffic uses CONNECT method; the proxy validates the target domain against the allowlist before establishing the tunnel -- **Policy decisions** -- The `NetworkPolicyDecider` trait allows custom logic for allow/deny/inject decisions per request - -### Zero-Exposure Credential Model - -Secrets (API keys, tokens) are stored encrypted on the host and injected into HTTP requests by the proxy at transit time. Container processes never have access to raw credential values, preventing exfiltration even if container code is compromised. - -Sandbox configuration: see Configuration section above. - -## Testing - -Tests are in `mod tests {}` blocks at the bottom of each file. Run specific module tests: -```bash -cargo test safety::sanitizer::tests -cargo test tools::registry::tests -``` - -Key test patterns: -- Unit tests for pure functions -- Async tests with `#[tokio::test]` -- No mocks, prefer real implementations or stubs - -## Current Limitations / TODOs - -1. **Domain-specific tools** - `marketplace.rs`, `restaurant.rs`, `taskrabbit.rs`, `ecommerce.rs` return placeholder responses; need real API integrations -2. **Integration tests** - Need testcontainers setup for PostgreSQL -3. **MCP stdio transport** - Only HTTP transport implemented -4. **WIT bindgen integration** - Auto-extract tool description/schema from WASM modules (stubbed) -5. **Capability granting after tool build** - Built tools get empty capabilities; need UX for granting HTTP/secrets access -6. **Tool versioning workflow** - No version tracking or rollback for dynamically built tools -7. **Full channel status view** - Gateway status widget exists, but no per-channel connection dashboard -8. **Observability backends** - Only `log` and `noop` implemented; OpenTelemetry/Prometheus not yet supported - -## Tool Architecture - -**Keep tool-specific logic out of the main agent codebase.** The main agent provides generic infrastructure; tools are self-contained units that declare their requirements through `capabilities.json` files (API endpoints, credentials, rate limits, auth setup). Service-specific auth flows, CLI commands, and configuration do not belong in the main agent. - -Tools can be built as **WASM** (sandboxed, credential-injected, single binary) or **MCP servers** (ecosystem of pre-built servers, any language, but no sandbox). Both are first-class via `ironclaw tool install`. Auth is declared in capabilities files with OAuth and manual token entry support. - -See `src/tools/README.md` for full tool architecture, adding new tools (built-in Rust and WASM), auth JSON examples, and WASM vs MCP decision guide. +See `.env.example` for all environment variables. LLM backends (`nearai`, `openai`, `anthropic`, `ollama`, `openai_compatible`, `tinfoil`, `bedrock`) documented in `src/llm/CLAUDE.md`. ## Adding a New Channel @@ -671,48 +183,24 @@ See `src/tools/README.md` for full tool architecture, adding new tools (built-in 3. Add config in `src/config/channels.rs` 4. Wire up in `src/app.rs` channel setup section +## Workspace & Memory + +Persistent memory with hybrid search (FTS + vector via RRF). Four tools: `memory_search`, `memory_write`, `memory_read`, `memory_tree`. Identity files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md) injected into system prompt. Heartbeat system runs proactive periodic execution (default: 30 minutes), reading `HEARTBEAT.md` and notifying via channel if findings. See `src/workspace/README.md`. + ## Debugging ```bash -# Verbose logging -RUST_LOG=ironclaw=trace cargo run - -# Just the agent module -RUST_LOG=ironclaw::agent=debug cargo run - -# With HTTP request logging -RUST_LOG=ironclaw=debug,tower_http=debug cargo run +RUST_LOG=ironclaw=trace cargo run # verbose +RUST_LOG=ironclaw::agent=debug cargo run # agent module only +RUST_LOG=ironclaw=debug,tower_http=debug cargo run # + HTTP request logging ``` -## Module Specifications +## Current Limitations -Some modules have a `README.md` that serves as the authoritative specification -for that module's behavior. When modifying code in a module that has a spec: - -1. **Read the spec first** before making changes -2. **Code follows spec**: if the spec says X, the code must do X -3. **Update both sides**: if you change behavior, update the spec to match; - if you're implementing a spec change, update the code to match -4. **Spec is the tiebreaker**: when code and spec disagree, the spec is correct - (unless the spec is clearly outdated, in which case fix the spec first) - -| Module | Spec File | -|--------|-----------| -| `src/setup/` | `src/setup/README.md` | -| `src/workspace/` | `src/workspace/README.md` | -| `src/tools/` | `src/tools/README.md` | -| `src/agent/` | `src/agent/CLAUDE.md` | -| `src/channels/web/` | `src/channels/web/CLAUDE.md` | -| `src/db/` | `src/db/CLAUDE.md` | -| `src/llm/` | `src/llm/CLAUDE.md` | -| `tests/e2e/` | `tests/e2e/CLAUDE.md` | - -## Workspace & Memory System - -OpenClaw-inspired persistent memory with a flexible filesystem-like structure. Principle: "Memory is database, not RAM" -- if you want to remember something, write it explicitly. Uses hybrid search combining FTS (keyword) + vector (semantic) via Reciprocal Rank Fusion. - -Four memory tools for LLM use: `memory_search` (hybrid search -- call before answering questions about prior work), `memory_write`, `memory_read`, `memory_tree`. Identity files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md) are injected into the LLM system prompt. - -The heartbeat system runs proactive periodic execution (default: 30 minutes), reading `HEARTBEAT.md` and notifying via channel if findings are detected. - -See `src/workspace/README.md` for full API documentation, filesystem structure, hybrid search details, chunking strategy, and heartbeat system. +1. Domain-specific tools (`marketplace.rs`, `restaurant.rs`, etc.) are stubs +2. Integration tests need testcontainers for PostgreSQL +3. MCP: no streaming support; stdio/HTTP/Unix transports all use request-response +4. WIT bindgen: auto-extract tool schema from WASM is stubbed +5. Built tools get empty capabilities; need UX for granting access +6. No tool versioning or rollback +7. Observability: only `log` and `noop` backends (no OpenTelemetry) diff --git a/src/llm/CLAUDE.md b/src/llm/CLAUDE.md index a1eb72be..d1b9eea2 100644 --- a/src/llm/CLAUDE.md +++ b/src/llm/CLAUDE.md @@ -19,6 +19,7 @@ Multi-provider LLM integration with circuit breaker, retry, failover, and respon | `rig_adapter.rs` | Adapter bridging rig-core `CompletionModel` → `LlmProvider`; used by OpenAI, Anthropic, Ollama, Tinfoil | | `smart_routing.rs` | `SmartRoutingProvider` — 13-dimension complexity scorer routes cheap vs primary model | | `recording.rs` | `RecordingLlm` — trace capture for E2E replay testing (`IRONCLAW_RECORD_TRACE`) | +| `bedrock.rs` | AWS Bedrock provider via native Converse API (feature-gated: `--features bedrock`) | ## Provider Selection @@ -32,6 +33,18 @@ Set via `LLM_BACKEND` env var: | `ollama` | Ollama local | `OLLAMA_BASE_URL` | | `openai_compatible` | Any OpenAI-compatible endpoint | `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` | | `tinfoil` | Tinfoil TEE inference | `TINFOIL_API_KEY`, `TINFOIL_MODEL` | +| `bedrock` | AWS Bedrock (requires `--features bedrock`) | `BEDROCK_REGION`, `BEDROCK_MODEL`, `AWS_PROFILE` | + +## AWS Bedrock Provider + +Uses the native Converse API via `aws-sdk-bedrockruntime` (`bedrock.rs`). Requires `--features bedrock` at build time — not in default features due to heavy AWS SDK dependencies. + +**Auth:** Standard AWS credential chain — IAM credentials (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`), SSO profiles (`AWS_PROFILE`), or instance roles. The SDK resolves auth automatically from the environment. + +**Config:** +- `BEDROCK_REGION` — AWS region (default: `us-east-1`) +- `BEDROCK_MODEL` — Required model ID (e.g., `anthropic.claude-opus-4-6-v1`) +- `BEDROCK_CROSS_REGION` — Optional cross-region inference prefix (`us`, `eu`, `apac`, `global`) ## NEAR AI Provider Gotchas