diff --git a/deploy/test-templates.sh b/deploy/test-templates.sh index bc3b7d7..c2eff39 100644 --- a/deploy/test-templates.sh +++ b/deploy/test-templates.sh @@ -95,6 +95,73 @@ check_contains "$INSTALL_SH" 'PackName' "install.sh: PackName in PARAM_CFN_NAMES check_contains "$INSTALL_SH" 'pack_name' "install.sh: pack_name in PARAM_TF_NAMES" check_contains "$INSTALL_SH" 't4g.medium' "install.sh: hermes default size logic present" +# ── Branch detection & SSM doc version ────────────────────────────────────── +echo -e "${BOLD}Branch & SSM fixes${NC}" +check_contains "$INSTALL_SH" '[[ "$REPO_BRANCH" == "HEAD" ]]' "install.sh: detached HEAD falls back to main" +check_contains "$INSTALL_SH" 'REPO_BRANCH=' "install.sh: REPO_BRANCH is set" +check_contains "$TF_VARS" 'variable "repo_branch"' "TF: repo_branch variable defined" +check_contains "$TF_MAIN" "repo_branch" "TF main: repo_branch passed to userdata template" +check_contains "$TF_USERDATA" 'repo_branch' "TF userdata: uses repo_branch for git clone" +check_contains "$CFN_TEMPLATE" "RepoBranch" "CFN: RepoBranch parameter defined" +check_contains "$INSTALL_SH" "DocumentDescription.DocumentVersion" "install.sh: SSM update-document captures numeric version" +check_contains "$INSTALL_SH" 'new_version' "install.sh: SSM update-document-default-version uses captured version" + +echo "" + +# ── Branch detection unit tests ───────────────────────────────────────────── +echo -e "${BOLD}Branch detection (unit)${NC}" + +# Test: detached HEAD → main +_test_branch="HEAD" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "main" ]]; then + pass "Detached HEAD resolves to main" +else + fail "Detached HEAD should resolve to main, got: $_test_branch" +fi + +# Test: normal branch → unchanged +_test_branch="installer-ux-overhaul" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "installer-ux-overhaul" ]]; then + pass "Normal branch name preserved" +else + fail "Normal branch should be preserved, got: $_test_branch" +fi + +# Test: main → unchanged +_test_branch="main" +[[ "$_test_branch" == "HEAD" ]] && _test_branch="main" +if [[ "$_test_branch" == "main" ]]; then + pass "main branch preserved" +else + fail "main should be preserved, got: $_test_branch" +fi + +# Test: SSM version regex accepts numeric +_test_version="5" +if [[ "$_test_version" =~ ^[0-9]+$ ]]; then + pass "SSM version regex accepts numeric version" +else + fail "SSM version regex should accept '5'" +fi + +# Test: SSM version regex rejects $LATEST +_test_version='$LATEST' +if [[ "$_test_version" =~ ^[0-9]+$ ]]; then + fail "SSM version regex should reject '\$LATEST'" +else + pass "SSM version regex rejects \$LATEST" +fi + +# Test: SSM version regex rejects empty +_test_version="" +if [[ -n "$_test_version" && "$_test_version" =~ ^[0-9]+$ ]]; then + fail "SSM version regex should reject empty string" +else + pass "SSM version regex rejects empty string" +fi + echo "" echo -e "${BOLD}─────────────────────────────────────────────────${NC}" echo -e " Passed: ${GREEN}${PASS}${NC} Failed: ${RED}${FAIL}${NC}" diff --git a/install.sh b/install.sh index a3a12d5..89903e4 100755 --- a/install.sh +++ b/install.sh @@ -126,6 +126,7 @@ DEPLOY_TERRAFORM=3 INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)}" INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$SCRIPT_DIR" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" REPO_BRANCH="${REPO_BRANCH:-$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}" +[[ "$REPO_BRANCH" == "HEAD" ]] && REPO_BRANCH="main" # Detect AWS CloudShell (limited ~1GB home dir, use /tmp for large files) IS_CLOUDSHELL=false @@ -1677,16 +1678,20 @@ ensure_ssm_session_document() { local doc_content='{"schemaVersion":"1.0","description":"SSM session for Loki - starts as ec2-user and launches TUI","sessionType":"Standard_Stream","inputs":{"runAsEnabled":true,"runAsDefaultUser":"ec2-user","shellProfile":{"linux":"cd ~ && bash --login -c \"loki tui || exec bash --login\""}}}' if aws ssm describe-document --name "$SSM_DOC_NAME" --region "$DEPLOY_REGION" &>/dev/null; then - # Update existing document to latest version - aws ssm update-document \ + # Update existing document and set new version as default + local new_version + new_version=$(aws ssm update-document \ --name "$SSM_DOC_NAME" \ --content "$doc_content" \ --document-version '$LATEST' \ - --region "$DEPLOY_REGION" >/dev/null 2>&1 || true - aws ssm update-document-default-version \ - --name "$SSM_DOC_NAME" \ - --document-version '$LATEST' \ - --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + --region "$DEPLOY_REGION" \ + --query 'DocumentDescription.DocumentVersion' --output text 2>/dev/null) || true + if [[ -n "$new_version" && "$new_version" =~ ^[0-9]+$ ]]; then + aws ssm update-document-default-version \ + --name "$SSM_DOC_NAME" \ + --document-version "$new_version" \ + --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + fi ok "SSM session document: ${SSM_DOC_NAME} (updated)" return 0 fi diff --git a/upstream-fixes.md b/upstream-fixes.md new file mode 100644 index 0000000..8eb1ac1 --- /dev/null +++ b/upstream-fixes.md @@ -0,0 +1,87 @@ +# Upstream Fixes Needed: OpenClaw + pi-coding-agent Bedrock Auth + +OpenClaw + pi-coding-agent fail when using AWS Bedrock with EC2 instance-profile auth (IMDS). +The AWS SDK credential chain works fine, but the auth pre-flight checks reject it before any request is made. + +We currently patch these at install time (`packs/openclaw/resources/patch-pi-agent.py`), but +these patches are overwritten on every OpenClaw update. + +## Issue 1: pi-coding-agent `hasConfiguredAuth()` rejects AWS SDK auth + +**File:** `src/core/model-registry.ts` (dist: `dist/core/model-registry.js`) + +**Problem:** `hasConfiguredAuth()` only checks `authStorage.hasAuth()` and `providerRequestConfigs.apiKey`. Neither is set for AWS SDK auth via instance profile. This causes "No API key found for amazon-bedrock" before any API request is made. + +**Fix:** Return `true` when the provider is `amazon-bedrock` and the config uses `auth: "aws-sdk"`. + +```typescript +// In hasConfiguredAuth(): +if (this.providerId === "amazon-bedrock") return true; +``` + +**Why:** Bedrock auth uses AWS SDK signing (SigV4), not API keys. The SDK resolves credentials from the instance metadata service (IMDS) at request time. There's no key to pre-check. + +--- + +## Issue 2: pi-coding-agent `_getRequiredRequestAuth()` throws on undefined apiKey + +**File:** `src/core/agent-session.ts` (dist: `dist/core/agent-session.js`) + +**Problem:** `_getRequiredRequestAuth()` throws when `getApiKeyAndHeaders()` returns `ok: true` but no `apiKey`. Bedrock uses AWS SDK signing (no API key needed), but this code path doesn't account for it. + +**Fix:** Early return for `amazon-bedrock` provider with `{ apiKey: undefined, headers }`. + +```typescript +// In _getRequiredRequestAuth(): +if (this.providerId === "amazon-bedrock") { + const authResult = await this._getRequestAuth?.() || { ok: true, headers: {} }; + return { apiKey: undefined, headers: authResult.headers || {} }; +} +``` + +**Why:** The Bedrock provider adapter handles auth via the AWS SDK at the HTTP layer (SigV4 signing). It never needs an API key injected by the session. + +--- + +## Issue 3 (Root Cause): OpenClaw auth-controller doesn't inject SDK auth into pi's authStorage + +**File:** `src/agents/pi-embedded-runner/run/auth-controller.ts` + +**Problem:** OpenClaw's auth controller correctly resolves AWS SDK auth (returns `{ mode: "aws-sdk", source: "..." }` from `resolveAwsSdkAuthInfo()`), but does an early return at ~line 329-337 without calling `setRuntimeApiKey()`. This means pi's `authStorage` never learns about Bedrock credentials. + +**Proper fix:** When auth mode is `aws-sdk`, the auth controller should either: +1. Call `setRuntimeApiKey()` with a sentinel value that pi recognizes as "use SDK signing", or +2. Set a flag on the provider config that pi checks in `hasConfiguredAuth()` and `_getRequiredRequestAuth()` + +This would eliminate the need for patches #1 and #2. + +--- + +## Issue 4: `resolveAwsSdkEnvVarName()` doesn't detect IMDS + +**File:** `src/agents/model-auth-runtime-shared.ts` + +**Problem:** `resolveAwsSdkEnvVarName()` checks for `AWS_BEARER_TOKEN_BEDROCK`, `AWS_ACCESS_KEY_ID`+`AWS_SECRET_ACCESS_KEY`, and `AWS_PROFILE`. If none are set, it returns `undefined`. EC2 instances with IAM roles don't need any env vars — the SDK discovers credentials via IMDS automatically. + +**Current workaround:** We set `AWS_PROFILE=default` in `/etc/profile.d/` and the systemd service unit, which triggers the env var check and lets the SDK fall through to IMDS. + +**Proper fix:** The fallback in `resolveAwsSdkAuthInfo()` already returns `{ mode: "aws-sdk", source: "aws-sdk default chain" }` when no env vars are found — this is correct. The issue is upstream in the auth controller (Issue 3) not propagating this to pi. + +--- + +## Environment + +- OpenClaw version: latest (as of 2026-04-06) +- pi-coding-agent version: 0.65.0 +- Platform: Amazon Linux 2023 on EC2 with IAM instance profile +- Auth method: Bedrock via IMDS (no API keys, no env vars) +- Config: `models.providers.amazon-bedrock.auth: "aws-sdk"` + +## Reproduction + +1. Launch EC2 instance with IAM role that has Bedrock access +2. Install OpenClaw: `npm install -g openclaw` +3. Configure with `auth: "aws-sdk"` for amazon-bedrock provider +4. Run `openclaw tui` — fails with "No API key found for amazon-bedrock" +5. Set `AWS_PROFILE=default` — still fails (auth pre-flight in pi rejects it) +6. Apply patches to model-registry.js and agent-session.js — works