# Pre-Flight Audit — v0.22.0 Kickoff **Date:** 2026-03-01 **Current version:** 0.21.6 **Test instance:** `switchboard-be-test` in `gobha-ai-chat` namespace **Schema:** `012_v0214_git.sql` — up to date --- ## 1. Bug: Channel List Corruption (SafeJSON 500) ### Symptoms ``` 23:24:44 ⚠ SafeJSON: marshal failed: json: error calling MarshalJSON for type json.RawMessage: invalid character 'i' looking for beginning of value 23:24:44 | 500 | GET "/test/api/v1/channels?page=1&per_page=100&type=direct" ``` One channel in the test database has invalid JSON in its `settings` column. The content starts with the character `i` (likely a bare word like `invalid`, `inactive`, or `in_progress` — not valid JSON). This kills the entire paginated channel list with a 500. ### Root Cause Analysis **Defense layer 1 — `scanJSON`**: The `ListChannels` scan path at `channels.go:236` correctly uses `scanJSON(&ch.Settings)`, which calls `json.Valid()` and defaults to `{}` on failure. This should catch corrupt data. **Defense layer 2 — `SafeJSON`**: Pre-marshals the entire response and returns a clean 500 instead of truncated output. This layer IS firing, meaning layer 1 did NOT catch the problem. **Inconsistent scan paths**: The Postgres `CreateChannel` RETURNING path at `channels.go:323` scans `&ch.Settings` directly WITHOUT `scanJSON`: ```go // line 323 — Postgres CreateChannel pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt, ``` Compare to the SQLite path at line 306 which correctly uses `scanJSON`. **How the data got corrupted**: Unknown without DB access. Postgres JSONB columns reject invalid JSON at write time (`::jsonb` cast), so the corruption likely predates the current schema — possibly from a migration, manual DB edit, or an older code version that used TEXT instead of JSONB. Run this to find it: ```sql SELECT id, title, settings FROM channels WHERE user_id = '664c71e8-cd8b-47fb-b421-9c48abb815ba' AND settings::text NOT LIKE '{%' AND settings::text NOT LIKE '[%' AND settings::text NOT LIKE '"null"'; ``` ### Fixes Required **P0 — Fix the data** (immediate): ```sql UPDATE channels SET settings = '{}'::jsonb WHERE settings IS NULL OR NOT (settings::text ~ '^[\[{"]'); ``` **P0 — Consistent scanJSON usage** (`channels.go:323`): ```go // Before (Postgres CreateChannel): pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt, // After: pq.Array(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt, ``` **P1 — Graceful list degradation**: When `SafeJSON` encounters a corrupt item in a paginated list, the current behavior drops the entire response. Instead, add a `SafeMarshalList` helper that: 1. Marshals items individually 2. Skips corrupt items with a warning log (include the row ID) 3. Returns the healthy items with a `warnings` field in the response **P1 — Input validation on settings write** (`channels.go:487`): ```go if req.Settings != nil { if !json.Valid([]byte(*req.Settings)) { c.JSON(http.StatusBadRequest, gin.H{"error": "settings must be valid JSON"}) return } // ... existing merge logic } ``` **P2 — Audit all RawMessage scan sites**: Search for `&ch.Settings`, `&*.Settings`, or any `json.RawMessage` scan that doesn't go through `scanJSON`. Every JSONB column scanned into `json.RawMessage` must use the wrapper. ### Additional Log Noise The git endpoints are returning 400s for workspaces without git repos: ``` 23:23:40 | 400 | GET ".../git/status" 23:23:56 | 400 | GET ".../git/branches" ``` These are expected (workspace has no `.git` directory), but the frontend should suppress these calls when the workspace has no `git_remote_url` configured. Frontend fix: check `workspace.git_remote_url` before calling git endpoints. --- ## 2. CI Variables → Deployment Audit Cross-referencing CI secrets/variables (from Gitea) against `backend.yaml`, `config.go`, and runtime logs. ### Secrets (Gitea Secrets → K8s Secrets) | CI Secret | K8s Secret | Key | Status | |-----------|------------|-----|--------| | ENCRYPTION_KEY | switchboard-encryption | ENCRYPTION_KEY | ✅ Active | | POSTGRES_USER | switchboard-db-credentials | POSTGRES_USER | ✅ Active | | POSTGRES_PASSWORD | switchboard-db-credentials | POSTGRES_PASSWORD | ✅ Active | | POSTGRES_ADMIN_USER | — | — | ⚠ Unused by app — likely for CI DB bootstrap | | POSTGRES_ADMIN_PASSWORD | — | — | ⚠ Unused by app — likely for CI DB bootstrap | | S3_ACCESS_KEY | switchboard-s3 | S3_ACCESS_KEY | ✅ Active | | S3_SECRET_KEY | switchboard-s3 | S3_SECRET_KEY | ✅ Active | | SWITCHBOARD_ADMIN_EMAIL | switchboard-admin | email | ✅ Active | | SWITCHBOARD_ADMIN_PASSWORD | switchboard-admin | password | ✅ Active | | SWITCHBOARD_ADMIN_USERNAME | switchboard-admin | username | ✅ Active | ### Variables (Gitea Variables → envsubst / K8s env) | CI Variable | Used In | Value | Status | |-------------|---------|-------|--------| | DOMAIN | ingress.yaml | gobha.ai | ✅ | | NAMESPACE | all manifests | gobha-ai-chat | ✅ | | POSTGRES_HOST | backend.yaml env | postgres-primary.postgres.svc.cluster.local | ✅ | | PROVIDER | ? | venice | ⚠ **Orphan** — not referenced in any manifest or config.go | | S3_BUCKET | switchboard-s3 secret | integration-test | ✅ | | S3_ENDPOINT | switchboard-s3 secret | http://192.168.10.29:7480 | ✅ | | SEED_PROVIDERS | backend.yaml env (direct) | venice:...:micro | ✅ New (Mar 1) | | SEED_USERS | switchboard-seed-users secret | darthvader:...,lukeskywalker:... | ✅ | | STORAGE_BACKEND | backend.yaml env | s3 | ✅ | | STORAGE_CLASS | storage-pvc.yaml | cephfs | ✅ | | STORAGE_SIZE | storage-pvc.yaml | (visible, value cut off) | ✅ | ### Variables Expected by backend.yaml but NOT in CI Screenshots These are likely set in the CI pipeline YAML directly (not as repo variables): | Variable | Used In | Expected Source | |----------|---------|-----------------| | ENVIRONMENT | backend.yaml env | Pipeline per-branch | | GIN_MODE | backend.yaml env | Pipeline (release/debug) | | BASE_PATH | backend.yaml env | Pipeline per-deploy (/test, /prod, etc.) | | POSTGRES_PORT | backend.yaml env | Default 5432 (config.go fallback) | | DB_NAME | backend.yaml env | Pipeline per-deploy | | DEPLOY_SUFFIX | all manifests | Pipeline (-test, -prod, etc.) | | BE_IMAGE | backend.yaml image | Pipeline / registry | | IMAGE_TAG | backend.yaml image | Pipeline / git SHA | | BE_REPLICAS | backend.yaml replicas | Pipeline | | BE_MEMORY_REQUEST | backend.yaml resources | Pipeline | | BE_MEMORY_LIMIT | backend.yaml resources | Pipeline | | BE_CPU_REQUEST | backend.yaml resources | Pipeline | | BE_CPU_LIMIT | backend.yaml resources | Pipeline | | EXTRACTION_CONCURRENCY | backend.yaml env | Pipeline (default 3) | ### Issues Found 1. **`PROVIDER` variable is orphaned.** Value is "venice" but nothing in backend.yaml or config.go references it. Likely a legacy variable from before `SEED_PROVIDERS` was added. Clean up or document. 2. **`SEED_PROVIDERS` contains an API key in a CI Variable (not Secret).** The screenshot shows the Venice API key visible in plaintext in the Variables section. This should be moved to Gitea Secrets for proper masking. The backend.yaml needs a corresponding mapping (currently SEED_PROVIDERS may be injected via envsubst or direct env, verify the CI pipeline). 3. **`SEED_USERS` passwords are plaintext in CI.** `password1234` for both test users. Acceptable for test environment but should be documented as test-only. Production pipeline should NOT have SEED_USERS set at all (and `SeedProviders` already blocks production). 4. **Missing `SEED_PROVIDERS` in backend.yaml env block.** The K8s manifest doesn't have a `SEED_PROVIDERS` env var entry. It works because the CI pipeline likely injects it via envsubst into a direct env entry, but this should be made explicit in backend.yaml for documentation parity. 5. **No `JWT_SECRET` in CI secrets.** Config.go defaults to `"dev-secret-change-me"`. The test instance may be running with this default. Add a proper secret for test environments. 6. **No `S3_REGION` or `S3_PREFIX` in CI.** Config.go defaults to `us-east-1` and `""` respectively. Fine for Ceph RGW but should be documented. ### Recommendations - Move `SEED_PROVIDERS` from CI Variable to CI Secret - Remove or document the orphaned `PROVIDER` variable - Add `JWT_SECRET` to CI Secrets and wire through backend.yaml - Add `SEED_PROVIDERS` env entry to backend.yaml (with secretKeyRef) - Add a CI pipeline comment documenting which variables are set inline vs. in repo settings --- ## 3. Test Hardening — Unhappy Paths Current test coverage is strong on the happy path (164+ integration tests). The corruption bug exposes gaps in unhappy-path coverage. ### Missing Test Categories **A. Data corruption resilience** (integration_test.go): - [ ] **Corrupt JSONB in channels**: INSERT a channel with valid settings, then UPDATE settings directly via SQL with invalid text. Verify GET /channels still returns 200 with the corrupt channel included (settings defaulted to `{}`). - [ ] **Corrupt JSONB in projects**: Same pattern for project settings. - [ ] **Corrupt JSONB in teams**: Same for team settings/policies. - [ ] **NULL settings column**: Verify channels with NULL settings serialize correctly (should be `{}` or `null`, not crash). - [ ] **Corrupt tags column**: Insert channel with malformed tags array. Verify list doesn't 500. **B. Settings write validation**: - [ ] **Invalid JSON in channel settings update**: PUT /channels/:id with `{"settings": "not-json"}` — should 400, not write to DB. - [ ] **Oversized settings**: PUT with a 10MB settings blob — should be rejected. - [ ] **SQL injection via settings merge**: PUT with settings containing SQL metacharacters — verify parameterized query prevents injection. **C. Scan alignment tests** (safe_json_test.go): - [ ] **Column count mismatch**: Mock a query result with wrong column count, verify clean error instead of panic. - [ ] **Type mismatch**: Scan a TEXT column into json.RawMessage without scanJSON — verify SafeJSON catches it. - [ ] **Driver buffer reuse**: Scan multiple rows with RawMessage, verify each row's data is independent (copy, not reference). **D. Startup resilience** (config/seed tests): - [ ] **SEED_PROVIDERS with invalid format**: `"openai"` (no key), `":"`, `":::"`, empty string — verify graceful skip. - [ ] **SEED_PROVIDERS with unknown provider**: `"unknown:key123"` — verify warning log, not crash. - [ ] **SEED_USERS with invalid format**: Malformed CSV — verify skip. - [ ] **DATABASE_URL with unreachable host**: Verify startup logs error and serves 503 on API endpoints. **E. WebSocket reconnection**: - [ ] **Rapid disconnect/reconnect**: The logs show multiple ws connect/ disconnect cycles in quick succession. Verify no goroutine leaks, no duplicate event delivery. - [ ] **Auth token expiry during active connection**: Verify clean disconnect with appropriate error, not silent data loss. **F. Frontend error handling** (manual or e2e): - [ ] **500 on channel list**: Frontend should show error state, not blank screen. Currently unknown behavior. - [ ] **400 on git endpoints for non-git workspace**: Frontend should not call git endpoints when workspace has no git_remote_url. - [ ] **Network timeout during streaming completion**: Verify partial response is preserved, user can retry. ### Test Infrastructure Needed 1. **SQL fixture injection**: Helper function to INSERT corrupt data directly via SQL, bypassing store layer validation. For corruption resilience tests. 2. **SafeJSON audit script**: `grep -rn '&.*\.Settings\|json.RawMessage'` to find all RawMessage scan sites. Run as CI lint check to catch regressions. 3. **Integration test for each migration**: Both fresh-install path AND upgrade path from previous version. Currently tested manually — automate. --- ## 4. Quick Wins Before v0.22.0 Work Starts Priority-ordered items that should land as v0.21.7 patches: 1. **Fix corrupt data in test DB** — SQL UPDATE (5 min) 2. **Add scanJSON to CreateChannel Postgres path** — one-line fix (5 min) 3. **Add json.Valid check on settings write** — channels.go UpdateChannel (10 min) 4. **Move SEED_PROVIDERS to CI Secret** — CI UI change (5 min) 5. **Suppress git endpoint calls for non-git workspaces** — frontend (15 min) 6. **Add corruption resilience integration tests** — 2-3 tests (1 hr) 7. **Add JWT_SECRET to CI Secrets** — CI + backend.yaml (15 min)