Skip to content

Auth Login Smoke Test #1276

Auth Login Smoke Test

Auth Login Smoke Test #1276

name: Auth Login Smoke Test
# Verifies the auth login contract by building and running the Go backend
# AND kc-agent in dev mode. No external dependencies — everything runs
# against localhost.
#
# #6590 contract (enforced by #8107): the refreshed JWT is delivered
# EXCLUSIVELY via the HttpOnly `kc_auth` cookie. The JSON body carries
# only `{ refreshed: true, onboarded }` — any `token` field in the body
# would be an XSS/extension-readable leak and must fail the smoke test.
#
# #10398 contract: ALL frontend fetch calls to kc-agent (LOCAL_AGENT_URL /
# LOCAL_AGENT_HTTP_URL) must go through agentFetch() or fetchWithRetry()
# which inject the KC_AGENT_TOKEN. Raw fetch() to agent URLs bypasses auth
# and produces silent 401 errors on every kc-agent endpoint.
#
# What it checks:
# 1. /health returns JSON with status and oauth_configured fields
# 2. GET /auth/github (dev mode, no OAuth client) sets a kc_auth cookie
# 3. POST /auth/refresh (cookie + CSRF header, NO Authorization) returns
# - response body with { refreshed: true, onboarded }
# - Set-Cookie: kc_auth=... (refreshed JWT)
# - NO `token` field in the body
# 4. kc-agent /health is reachable (no auth required)
# 5. /api/agent/token returns the shared KC_AGENT_TOKEN
# 6. kc-agent rejects requests without the agent token (401)
# 7. kc-agent accepts requests with the correct agent token (200)
# 8. kc-agent rejects unauthenticated WebSocket upgrades
# 9. /auth/refresh rejects expired/invalid cookies (not silent 200)
# 10. /api/agent/token requires authentication (no token leak)
# 11. No raw fetch() calls to agent URLs bypass agentFetch/fetchWithRetry
#
# The test uses DEV_MODE (no GitHub OAuth credentials) so the backend
# auto-creates a dev-user on /auth/github. This takes ~60 seconds.
on:
schedule:
# Every hour
- cron: "0 * * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: auth-login-smoke
cancel-in-progress: true
jobs:
auth-smoke:
if: github.repository == 'kubestellar/console'
name: Auth Login Smoke
runs-on: ubuntu-latest
timeout-minutes: 5
env:
KC_AGENT_TOKEN: smoke-test-agent-token-do-not-use-in-prod
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
# ── 1. Build the Go backend and kc-agent ─────────────────────
- name: Build backend and kc-agent
run: |
go build -o console-bin ./cmd/console
go build -o kc-agent-bin ./cmd/kc-agent
# ── 2. Start kc-agent with token auth ────────────────────────
- name: Start kc-agent
run: |
DEV_MODE=true \
./kc-agent-bin --port 8585 &
echo $! > /tmp/kc-agent.pid
# ── 3. Wait for kc-agent health ──────────────────────────────
- name: Wait for kc-agent health
run: |
MAX_WAIT_SECONDS=30
for i in $(seq 1 "$MAX_WAIT_SECONDS"); do
HTTP_CODE=$(curl -sS --max-time 2 -o /dev/null -w '%{http_code}' \
http://127.0.0.1:8585/health 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "kc-agent healthy after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::kc-agent did not become healthy within ${MAX_WAIT_SECONDS}s"
exit 1
# ── 4. Test kc-agent rejects unauthenticated requests ────────
- name: Test kc-agent rejects unauthenticated requests
run: |
echo "Testing kc-agent rejects requests without token..."
HTTP_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
http://127.0.0.1:8585/deployments 2>/dev/null)
if [ "$HTTP_CODE" != "401" ]; then
echo "::error::kc-agent /deployments returned $HTTP_CODE without auth — expected 401"
exit 1
fi
echo "kc-agent correctly rejected unauthenticated request (401)"
echo "Testing kc-agent rejects wrong token..."
HTTP_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer wrong-token-value" \
http://127.0.0.1:8585/deployments 2>/dev/null)
if [ "$HTTP_CODE" != "401" ]; then
echo "::error::kc-agent /deployments returned $HTTP_CODE with wrong token — expected 401"
exit 1
fi
echo "kc-agent correctly rejected wrong token (401)"
# ── 5. Test kc-agent accepts correct token ───────────────────
- name: Test kc-agent accepts correct agent token
run: |
echo "Testing kc-agent accepts correct agent token..."
HTTP_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer ${KC_AGENT_TOKEN}" \
http://127.0.0.1:8585/deployments 2>/dev/null)
if [ "$HTTP_CODE" = "401" ]; then
echo "::error::kc-agent /deployments returned 401 with correct token — agent token auth is broken"
exit 1
fi
echo "kc-agent accepted correct token (HTTP $HTTP_CODE)"
# ── 6. Start backend in dev mode ─────────────────────────────
- name: Start backend
run: |
JWT_SECRET="smoke-test-$(openssl rand -hex 16)" \
DEV_MODE=true \
./console-bin --dev --port 8081 &
echo $! > /tmp/backend.pid
# ── 7. Wait for backend health ──────────────────────────────
- name: Wait for backend health
run: |
MAX_WAIT_SECONDS=60
for i in $(seq 1 "$MAX_WAIT_SECONDS"); do
RESP=$(curl -sS --max-time 2 http://localhost:8081/health 2>/dev/null || true)
STATUS=$(echo "$RESP" | jq -r '.status // empty' 2>/dev/null || echo "")
if [ "$STATUS" = "ok" ]; then
echo "Backend healthy (status=ok) after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::Backend did not reach status=ok within ${MAX_WAIT_SECONDS}s"
exit 1
# ── 8. Test /health returns JSON with expected fields ────────
- name: Test /health endpoint
run: |
echo "Checking /health endpoint..."
HEALTH_RESP=$(curl -sS --max-time 5 http://localhost:8081/health)
echo "Response: $HEALTH_RESP"
STATUS=$(echo "$HEALTH_RESP" | jq -r '.status // empty')
if [ -z "$STATUS" ]; then
echo "::error::/health response missing 'status' field"
exit 1
fi
echo "status=$STATUS"
# In dev mode with no OAuth creds, oauth_configured should be false
OAUTH=$(echo "$HEALTH_RESP" | jq -r '.oauth_configured // empty')
echo "oauth_configured=$OAUTH"
echo "/health OK"
# ── 9. Dev-mode login via /auth/github ───────────────────────
- name: Get dev-mode auth cookie
run: |
echo "Calling GET /auth/github (dev mode login)..."
COOKIE_JAR=/tmp/cookies.txt
HTTP_CODE=$(curl -sS --max-time 5 \
-o /dev/null -w '%{http_code}' \
-c "$COOKIE_JAR" \
http://localhost:8081/auth/github)
echo "HTTP status: $HTTP_CODE (expect 307 redirect)"
# Extract the kc_auth cookie value
KC_AUTH=$(grep 'kc_auth' "$COOKIE_JAR" | awk '{print $NF}')
if [ -z "$KC_AUTH" ]; then
echo "::error::Dev-mode login did not set kc_auth cookie"
echo "Cookie jar contents:"
cat "$COOKIE_JAR"
exit 1
fi
echo "Got kc_auth cookie (${#KC_AUTH} chars)"
echo "$KC_AUTH" > /tmp/kc_auth_token.txt
# ── 10. Test /api/agent/token returns the shared secret ──────
# This is the endpoint the frontend calls after login to get the
# kc-agent token. It must return the same KC_AGENT_TOKEN that
# kc-agent was started with. If this breaks, every agentFetch()
# call sends the wrong (or empty) token → 401 on all agent data.
- name: Test /api/agent/token endpoint
run: |
COOKIE_JAR=/tmp/cookies.txt
echo "Testing /api/agent/token..."
AGENT_TOKEN_RESP=$(curl -sS --max-time 5 \
-b "$COOKIE_JAR" \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/api/agent/token)
echo "Response: $AGENT_TOKEN_RESP"
RETURNED_TOKEN=$(echo "$AGENT_TOKEN_RESP" | jq -r '.token // empty')
if [ -z "$RETURNED_TOKEN" ]; then
echo "::error::/api/agent/token returned empty token — frontend will not be able to auth with kc-agent"
exit 1
fi
if [ "$RETURNED_TOKEN" != "$KC_AGENT_TOKEN" ]; then
echo "::error::/api/agent/token returned a different token than KC_AGENT_TOKEN — backend/agent token mismatch"
echo "Expected: ${KC_AGENT_TOKEN:0:8}..."
echo "Got: ${RETURNED_TOKEN:0:8}..."
exit 1
fi
echo "/api/agent/token returns correct KC_AGENT_TOKEN"
# ── 11. End-to-end: token from backend works on kc-agent ─────
# Simulates the full frontend flow: get token from backend, use
# it to call kc-agent. This is the exact path that broke in
# #10398/#10407 — raw fetch() bypassed agentFetch() and sent the
# wrong token (OAuth JWT instead of agent token).
- name: Test end-to-end agent token flow
run: |
COOKIE_JAR=/tmp/cookies.txt
# Get token from backend (like frontend does after login)
E2E_TOKEN=$(curl -sS --max-time 5 \
-b "$COOKIE_JAR" \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/api/agent/token | jq -r '.token')
# Use that token to call kc-agent (like agentFetch does)
echo "Testing kc-agent with token from /api/agent/token..."
HTTP_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer ${E2E_TOKEN}" \
http://127.0.0.1:8585/deployments 2>/dev/null)
if [ "$HTTP_CODE" = "401" ]; then
echo "::error::END-TO-END FAILURE: token from /api/agent/token was rejected by kc-agent (401)"
echo "This means the backend and kc-agent are using different tokens."
exit 1
fi
echo "End-to-end agent token flow PASSED (HTTP $HTTP_CODE)"
# ── 12. Test /auth/refresh contract (#6590) ──────────────────
- name: Test /auth/refresh contract
run: |
COOKIE_JAR=/tmp/cookies.txt
REFRESH_COOKIE_JAR=/tmp/cookies-after-refresh.txt
echo "Testing POST /auth/refresh..."
REFRESH_RESP=$(curl -sS --max-time 5 \
-X POST \
-b "$COOKIE_JAR" \
-c "$REFRESH_COOKIE_JAR" \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/auth/refresh)
echo "Response: $REFRESH_RESP"
# #6590 CONTRACT: response body MUST NOT contain a `token` field.
HAS_TOKEN=$(echo "$REFRESH_RESP" | jq 'has("token")')
if [ "$HAS_TOKEN" != "false" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response includes 'token' field — this is the #6590 XSS leak that #8107 fixed. Refreshed JWT must live in the HttpOnly kc_auth cookie only."
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "token field absent from body (correct)"
# #6590 CONTRACT: response body MUST contain `onboarded` (boolean)
HAS_ONBOARDED=$(echo "$REFRESH_RESP" | jq 'has("onboarded")')
if [ "$HAS_ONBOARDED" != "true" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response missing 'onboarded' field"
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "onboarded field present"
# #6590 CONTRACT: response body MUST contain `refreshed: true`
HAS_REFRESHED=$(echo "$REFRESH_RESP" | jq -r '.refreshed // empty')
if [ "$HAS_REFRESHED" != "true" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response missing 'refreshed' field"
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "refreshed field present"
# #6590 CONTRACT: Set-Cookie must refresh the kc_auth cookie.
REFRESHED_KC_AUTH=$(grep -E $'(^|\t)kc_auth\t' "$REFRESH_COOKIE_JAR" 2>/dev/null | awk -F'\t' '{print $NF}' | tail -n1)
if [ -z "$REFRESHED_KC_AUTH" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh did not set a new kc_auth cookie. The refreshed JWT must be delivered exclusively via the HttpOnly cookie."
echo "Cookie jar contents:"
cat "$REFRESH_COOKIE_JAR" || true
exit 1
fi
echo "kc_auth cookie refreshed (${#REFRESHED_KC_AUTH} chars)"
# #6590 CONTRACT: the refreshed kc_auth cookie must be HttpOnly.
if ! grep -E '^#HttpOnly_.*[[:space:]]kc_auth[[:space:]]' "$REFRESH_COOKIE_JAR" >/dev/null 2>&1; then
echo "::error::CONTRACT VIOLATION: refreshed kc_auth cookie is NOT HttpOnly. The #8107 XSS fix requires Set-Cookie: kc_auth=...; HttpOnly."
echo "Cookie jar contents:"
cat "$REFRESH_COOKIE_JAR" || true
exit 1
fi
echo "kc_auth cookie is HttpOnly (correct)"
echo ""
echo "Auth login smoke test PASSED"
# ── 13. Test kc-agent WebSocket rejects without token ─────────
# Covers gap #3: getWsAuthParams returns no auth subprotocol when
# the token is missing from localStorage. Without the bearer
# subprotocol, the WebSocket upgrade must be rejected. If it's
# accepted, unauthenticated users can stream live data.
- name: Test kc-agent WebSocket rejects unauthenticated upgrade
run: |
echo "Testing WebSocket upgrade without token..."
# kc-agent listens for WS upgrades on /ws. Without the auth
# subprotocol, the upgrade handshake should be rejected
# (HTTP 401 or 403).
WS_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
http://127.0.0.1:8585/ws 2>/dev/null)
if [ "$WS_CODE" = "101" ]; then
echo "::error::kc-agent accepted WebSocket upgrade WITHOUT auth token — unauthenticated WS connections should be rejected"
exit 1
fi
echo "kc-agent correctly rejected unauthenticated WebSocket upgrade (HTTP $WS_CODE)"
echo "Testing WebSocket upgrade with correct token..."
# A successful WS upgrade (101) keeps the connection open, so curl
# will hit --max-time and exit with code 28 (timeout). Temporarily
# disable errexit so the non-zero curl exit doesn't kill the step.
# The `|| true` pattern inside command substitution is unreliable
# under bash -e in some CI environments.
set +e
WS_CODE_AUTH=$(curl -sS --max-time 5 --connect-timeout 3 \
-o /dev/null -w '%{http_code}' \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Protocol: bearer.${KC_AGENT_TOKEN}" \
http://127.0.0.1:8585/ws 2>/dev/null)
WS_CURL_RC=$?
set -e
echo "curl exit=$WS_CURL_RC http_code=$WS_CODE_AUTH"
# 101 = upgrade accepted, or any non-401 = token was accepted.
# curl exit 28 (timeout) is expected for a successful WS upgrade
# because the connection stays open until --max-time fires.
if [ "$WS_CODE_AUTH" = "401" ]; then
echo "::error::kc-agent rejected WebSocket upgrade WITH correct token — WS token auth is broken"
exit 1
fi
echo "kc-agent accepted WebSocket upgrade with correct token (HTTP $WS_CODE_AUTH)"
# ── 14. Test /auth/refresh rejects expired/invalid cookies ────
# Covers gap #4: If /auth/refresh accepts garbage cookies and
# returns 200, the silent-catch in the "Refresh Now" handler
# masks a real auth failure. The endpoint must return 401 for
# invalid sessions so the frontend knows the refresh failed.
- name: Test /auth/refresh rejects invalid cookie
run: |
echo "Testing /auth/refresh with no cookie..."
NO_COOKIE_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-X POST \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/auth/refresh)
if [ "$NO_COOKIE_CODE" = "200" ]; then
echo "::error::/auth/refresh returned 200 with NO cookie — should require valid session"
exit 1
fi
echo "/auth/refresh correctly rejected no-cookie request (HTTP $NO_COOKIE_CODE)"
echo "Testing /auth/refresh with garbage cookie..."
GARBAGE_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-X POST \
-b "kc_auth=garbage-invalid-jwt-token-value" \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/auth/refresh)
if [ "$GARBAGE_CODE" = "200" ]; then
echo "::error::/auth/refresh returned 200 with garbage cookie — invalid JWTs must be rejected"
exit 1
fi
echo "/auth/refresh correctly rejected garbage cookie (HTTP $GARBAGE_CODE)"
# ── 15. Test /api/agent/token requires authentication ─────────
# Covers gap #1: If /api/agent/token returns a token to
# unauthenticated requests, anyone can steal the agent shared
# secret. It must require a valid session cookie.
- name: Test /api/agent/token requires auth
run: |
echo "Testing /api/agent/token without cookie..."
NO_AUTH_CODE=$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/api/agent/token)
if [ "$NO_AUTH_CODE" = "200" ]; then
echo "::error::/api/agent/token returned 200 without auth — agent token would leak to unauthenticated users"
exit 1
fi
echo "/api/agent/token correctly requires auth (HTTP $NO_AUTH_CODE)"
# ── 16. Static analysis: no raw fetch() to agent URLs ──────────
# Catches the exact regression from #10398: someone adds a fetch()
# call to LOCAL_AGENT_HTTP_URL or LOCAL_AGENT_URL without going
# through agentFetch() or fetchWithRetry(). These calls silently
# skip the KC_AGENT_TOKEN header and produce 401s on every
# kc-agent endpoint.
- name: Check for raw fetch to agent URLs
run: |
echo "Scanning for raw fetch() calls to kc-agent URLs..."
# Match: fetch(`${LOCAL_AGENT_HTTP_URL} or fetch(`${LOCAL_AGENT_URL}
# Match: fetch(LOCAL_AGENT_HTTP_URL or fetch(LOCAL_AGENT_URL
# Match: fetch(`http://127.0.0.1:8585
# Exclude: agentFetch, fetchWithRetry, test files, comments
# /health is intentionally public (no auth required, used for
# agent discovery) so raw fetch() to /health is allowed.
VIOLATIONS=$(grep -rn \
-e 'fetch(`${LOCAL_AGENT_HTTP_URL}' \
-e 'fetch(`${LOCAL_AGENT_URL}' \
-e 'fetch(LOCAL_AGENT_HTTP_URL' \
-e 'fetch(LOCAL_AGENT_URL' \
-e 'fetch(`http://127\.0\.0\.1:8585' \
web/src/ \
--include='*.ts' --include='*.tsx' \
| grep -v 'agentFetch' \
| grep -v 'fetchWithRetry' \
| grep -v '__tests__' \
| grep -v '\.test\.' \
| grep -v '\.spec\.' \
| grep -v '// ' \
| grep -v 'shared\.ts' \
| grep -v '/health' \
|| true)
if [ -n "$VIOLATIONS" ]; then
echo "::error::Found raw fetch() calls to kc-agent URLs that bypass agentFetch()."
echo "These will produce 401 Unauthorized errors because they don't include the KC_AGENT_TOKEN header."
echo ""
echo "Violations:"
echo "$VIOLATIONS"
echo ""
echo "Fix: replace fetch() with agentFetch() from hooks/mcp/shared.ts"
exit 1
fi
echo "No raw fetch() calls to agent URLs found (correct)"
# ── 17. Static analysis: no raw WebSocket to agent URL ─────
# Catches WebSocket connections that bypass getWsAuthParams.
# Without the auth subprotocol, kc-agent rejects the upgrade.
- name: Check for raw WebSocket to agent URLs
run: |
echo "Scanning for WebSocket connections that bypass getWsAuthParams..."
# Match: new WebSocket( with agent URL patterns, excluding
# getWsAuthParams calls and test files.
# Filter on the code portion only (not the file path), otherwise
# filenames like localAgentChat.ts trigger false positives.
WS_VIOLATIONS=$(grep -rn \
-e 'new WebSocket(' \
web/src/ \
--include='*.ts' --include='*.tsx' \
| awk 'BEGIN{IGNORECASE=1} { code=$0; sub(/^[^:]+:[0-9]+:/, "", code); if (code ~ /(LOCAL_AGENT|agent|8585)/) print $0 }' \
| grep -v 'getWsAuthParams' \
| grep -v '__tests__' \
| grep -v '\.test\.' \
| grep -v '\.spec\.' \
| grep -v '// ' \
| grep -v 'useActiveUsers' \
|| true)
if [ -n "$WS_VIOLATIONS" ]; then
echo "::error::Found WebSocket connections to kc-agent that bypass getWsAuthParams()."
echo "These will be rejected by kc-agent because they don't include the auth subprotocol."
echo ""
echo "Violations:"
echo "$WS_VIOLATIONS"
echo ""
echo "Fix: call getWsAuthParams() from lib/utils/wsAuth.ts and pass its protocols to new WebSocket()"
exit 1
fi
echo "No raw WebSocket connections to agent URLs found (correct)"
# ── 19. Cleanup ──────────────────────────────────────────────
- name: Stop backend and kc-agent
if: always()
run: |
if [ -f /tmp/backend.pid ]; then
kill "$(cat /tmp/backend.pid)" 2>/dev/null || true
fi
if [ -f /tmp/kc-agent.pid ]; then
kill "$(cat /tmp/kc-agent.pid)" 2>/dev/null || true
fi
# ── 20. Alert on failure ─────────────────────────────────────
- name: Create issue on failure
if: failure()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const title = `Auth smoke test failed — login or agent token contract may be broken`;
const body = [
'## Auth Smoke Test Failure',
'',
`**Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
`**Time:** ${new Date().toISOString()}`,
'',
'The auth smoke test failed against a local backend + kc-agent in dev mode.',
'',
'### What this test covers',
'',
'**OAuth login contract (#6590, enforced by #8107):**',
'- `/auth/refresh` body contains `{ refreshed: true, onboarded }` only',
'- Refreshed JWT is in the HttpOnly `kc_auth` cookie, NOT in the response body',
'',
'**Agent token contract (#10398, #10407):**',
'- kc-agent rejects requests without `KC_AGENT_TOKEN` (401)',
'- kc-agent accepts requests with the correct token',
'- `/api/agent/token` returns the same token kc-agent was started with',
'- `/api/agent/token` requires authentication (no token leak)',
'- End-to-end: token from backend → kc-agent auth succeeds',
'- kc-agent rejects unauthenticated WebSocket upgrades',
'- `/auth/refresh` rejects expired/invalid cookies',
'- No raw `fetch()` calls to agent URLs bypass `agentFetch()`',
'',
'### Action required',
'1. Check the workflow run logs for the specific failure step',
'2. If **agent token**: verify `KC_AGENT_TOKEN` env var flows from startup-oauth.sh → backend → `/api/agent/token` → frontend → kc-agent',
'3. If **raw fetch**: replace `fetch()` with `agentFetch()` from `hooks/mcp/shared.ts`',
'4. If **OAuth login**: verify `/auth/refresh` contract (see #6590)',
'5. If **WebSocket auth**: verify `getWsAuthParams()` is called and kc-agent validates the bearer subprotocol',
'6. If **agent token leak**: verify `/api/agent/token` middleware requires a valid session cookie',
].join('\n');
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'auth-smoke-failure',
per_page: 1,
});
if (existing.data.length > 0) {
console.log(`Issue already open: #${existing.data[0].number}`);
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ['auth-smoke-failure', 'priority/critical'],
});