---
phase: 1.5
plan: 2
subsystem: governance-runtime-bugfix
tags: [rbac, auth, admin, member, role, bootstrap, approvals, BUG-03]
requirements: [BUG-03]
wave: 2
depends_on: ["01.5-01"]
files_modified:
  - TBD-after-diagnosis
autonomous: false
---

# Phase 1.5 Plan 2: Founder Admin Role Bugfix

<objective>
Fix BUG-03: the founder (Wes) is viewing his own DashClaw instance as `role: member` instead of `role: admin`, and the `/approvals` page is showing him a *"READ-ONLY ACCESS — You are currently viewing as a member"* banner. This bug was caught during the same 2026-04-11 dogfood session as BUG-01 and BUG-02 — after Wes disabled all policies to work around the semantic-check block, he refreshed the Approvals page and the read-only banner flashed visible for a moment before disappearing. A screenshot confirmed the state.

The implication is that **the owner of a self-hosted DashClaw instance cannot approve or deny actions in his own UI**, which means:
- The Phase 1 / Plan 01-03 dogfood ritual ("≥5/7 days of approval events in Wes's personal instance") is structurally broken — even if approvals are created, Wes has no way to act on them
- The Phase 2 Claude Code beachhead demo ("Discord approval in ≤10 seconds") cannot be demonstrated end-to-end — Wes would hit the same read-only wall when trying to demo it
- The `dashclaw_pretool.py` hook's error message *"Run 'dashclaw approvals' to review or override"* is a dead-end instruction for the founder, because he doesn't have override permission in his own instance
- Any new developer deploying DashClaw via the Vercel 1-click path will hit the same bug on day one, meaning the **first-user activation experience is fundamentally broken for the admin use case**

BUG-03 is therefore not an edge case. It is a P0 for the Phase 2 launch and was explicitly sequenced as Wave 2 of Phase 1.5 *after* Plan 01.5-01 so that the diagnosis work benefits from a working audit trail (fixing BUG-02 first means blocked actions will finally appear in the decisions ledger, which makes it easier to reason about what the UI should be showing to each role).
</objective>

<requirements>
- **BUG-03**: The first/founding user of a DashClaw instance must have `role: admin` (or equivalent) by default and must be able to approve and deny actions in the `/approvals` UI without seeing a read-only banner. The fix must also handle the general case — not just Wes's specific user — so that any new developer who deploys DashClaw via the Vercel 1-click path becomes admin of their own instance automatically, without needing to run a SQL query against the database.
</requirements>

<tasks>

<task id="01.5-02-T1">
<name>Diagnose Bug 3: find where the "viewing as a member" state comes from</name>
<action>
Locate the code path that decides whether the current user has admin or member privileges on the `/approvals` page. This is a multi-step investigation — do each step in order, writing findings to `.planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md` as you go:

**Step A: Find the page component.**
The screenshot shows `/approvals` in the breadcrumb (OPERATIONS / APPROVALS). Look for the page component — most likely `app/approvals/page.js` (not `app/approve/page.js`, which was touched in commit `6d2c21bd` and is a separate per-action approval screen). If `app/approvals/page.js` does not exist, check `app/(extensions)/approvals/`, `app/operations/approvals/`, or any other path that matches the route. Use `find app -type d -name "approv*"` and grep for the literal string `"READ-ONLY ACCESS"` or `"viewing as a member"` to find the banner's source:

```bash
grep -rl "READ-ONLY ACCESS\|viewing as a member\|viewing.as.a.member" app/ --include="*.js" --include="*.jsx" --include="*.tsx"
```

**Step B: Trace the role check backward.**
Once the page component is found, find the prop or hook that supplies the user's role. Work backwards:
- What does the component render as `role`?
- Where does the value come from (props, `useSession`, `getUser()`, server action, direct DB query)?
- What does that upstream function return, and what does it read from the DB?

Walk up the chain until you find the SQL (or ORM query) that reads the `role` column. Note the table name, column name, and query shape.

**Step C: Query the actual database state for Wes's user.**
Once you know the table/column, query the DB to see what Wes's user record actually contains. Use the project's existing DB client pattern (likely via Drizzle or via `psql` through the Neon connection string in `.env`). **Read-only query** — do NOT modify the row in this task.

```sql
SELECT id, email, role, org_id, created_at FROM users WHERE email = '<Wes's email>' LIMIT 1;
```

Or equivalent via the project's repository pattern. If the repository doesn't have a direct "get user by email" method, add a temporary query in the diagnosis document, do NOT commit a new repository method for diagnosis-only use.

**Step D: Check the bootstrap flow.**
Find where users are created during instance setup. Most likely in `app/api/setup/*`, `scripts/bootstrap-agent.mjs`, or the `/connect` onboarding flow (`app/connect/page.js`). Look for `INSERT INTO users`, `.insert(users)`, `createUser(`, or similar. Is the first user being inserted with an explicit role, or is it relying on a database default?

If there's a database default, check the schema: is the default `member` or `admin`? If default is `member`, that's the bug — bootstrap is not promoting the first user. If default is `admin`, the bug is elsewhere (maybe a setup route overrides it to `member`).

**Step E: Check for org mismatch.**
DashClaw is multi-tenant. It's possible Wes is logged into an org where he IS a member, while his API key writes to a different org (like `org_default`) where he would be admin. Check:
- What `org_id` is in Wes's user record?
- What `org_id` is in the `DASHCLAW_API_KEY_ORG` env var (from `.env` or the hosted instance)?
- Is the session org matching the API key org?

Commit `3dcb43dc` (part of today's Plan 01-01 Lief-fork port) specifically touched `DASHCLAW_API_KEY_ORG` handling in `app/api/auth/local/route.js` — quote from the commit diff: *"JWT orgId now reads DASHCLAW_API_KEY_ORG env var with org_default fallback"*. **This is a strong suspect for the root cause.** If Wes's user was created in org `org_default` but his session JWT now reads `DASHCLAW_API_KEY_ORG` which is set to something else, the session would land him in an org where he's a non-member (or a brand-new member record created on first login in that org). Check the diff of `3dcb43dc` in `app/api/auth/local/route.js` and trace the behavior change.

**Step F: Write the diagnosis.**
Write all findings to `.planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md` with sections for each step above plus a **root cause hypothesis** (one of):
- (a) DB default for `role` column is `member`, bootstrap doesn't override — simple fix, promote in bootstrap
- (b) Wes's user was created in one org and his session lands in another — fix the session org resolution or the user record
- (c) The bootstrap flow has a conditional that sets `role=admin` only for certain conditions (e.g., first-user detection) and it's broken or never fires — fix the condition
- (d) `3dcb43dc` introduced a regression in JWT org resolution — revert or patch that specific change
- (e) Something else with evidence

**Do not fix anything in this task. Only diagnose.**
</action>
<read_first>
- Previous: `.planning/phases/01.5-governance-bugfix/01.5-01-PLAN.md` (to understand what Plan 01.5-01 is doing in parallel)
- Previous: `.planning/phases/01.5-governance-bugfix/01.5-DIAGNOSIS.md` (from Plan 01.5-01 T1, if complete — contextual information about the server-side auth flow that the semantic check diagnosis may have surfaced)
- `app/` directory listing via `ls app/` (to find the approvals page path)
- `app/api/auth/local/route.js` — specifically the changes from commit `3dcb43dc`, which touched JWT org resolution
- `app/api/setup/` directory listing (setup route handlers)
- `scripts/bootstrap-agent.mjs` (existing bootstrap pattern)
- `.env.example` (to see what `DASHCLAW_API_KEY_ORG` is supposed to be set to)
</read_first>
<acceptance_criteria>
- `.planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md` exists and contains:
  - Exact file path of the `/approvals` page component
  - Exact file paths and line numbers of the role-check chain from UI down to DB query
  - The SQL result (or equivalent) showing Wes's user record with `role`, `org_id`, `email`, `created_at`
  - The bootstrap flow's role assignment logic (inline code quote)
  - Org mismatch check result (what org the session lands in vs what the API key writes to)
  - `3dcb43dc` diff reviewed — either cleared as not the cause, or confirmed as the regression
  - A named root-cause hypothesis from the list (a/b/c/d/e) with evidence
  - A proposed fix description (not the actual fix — that's T2)
- No source files are modified
- No database rows are modified
</acceptance_criteria>
</task>

<task id="01.5-02-T2">
<name>Fix Bug 3: apply the minimal fix identified by T1's diagnosis</name>
<action>
Apply the minimal fix based on T1's root-cause hypothesis. The exact action depends on the hypothesis:

**If hypothesis (a) — DB default is `member`, bootstrap never overrides:**
Modify the bootstrap / setup flow to explicitly set `role='admin'` for the first user created in an instance (or for any user created while no admin exists yet). Follow the project's repository pattern — do NOT add SQL directly to a route file (route-SQL guardrail). Add or modify a `users.repository.js` method like `createFirstAdminUser(email, orgId)` and call it from the setup route. Include a migration or guard so existing member-role first-users are promoted on next login (optional — the user may prefer a one-off DB update instead of a runtime check, decide based on diagnosis).

**If hypothesis (b) — Org mismatch from session vs API key:**
Fix the session org resolution so it reads the org where the user was originally created, or fix the user's org record so it matches `DASHCLAW_API_KEY_ORG`. The specific fix depends on which side is wrong:
- If the session is wrong (should use the user's org, not the env var): fix `app/api/auth/local/route.js` to read `user.org_id` from the DB after authentication, not from `DASHCLAW_API_KEY_ORG`
- If the user is wrong (user is in the wrong org record): fix by either (i) updating the user's `org_id` to match `DASHCLAW_API_KEY_ORG`, or (ii) adding a "join org" path during setup so the user is associated with the right org

**If hypothesis (c) — Broken first-user detection:**
Find the broken conditional, fix it, test that a fresh instance bootstrap now creates the first user as admin.

**If hypothesis (d) — `3dcb43dc` regression:**
**Stop and flag this to Wes.** Do not revert the commit unilaterally — it was part of the Lief LAN/CSP port which is important for FIX-03, and reverting it would regress Phase 1's work. Instead, write a patch that preserves the LAN-fix intent but correctly resolves the org/role. Document the patch in `01.5-BUG03-DIAGNOSIS.md` and request explicit approval before committing.

**If hypothesis (e) — Something else:**
Document the fix plan in the diagnosis doc and request explicit approval before committing.

For any hypothesis, the fix must:
- Use the repository pattern (no direct SQL in routes)
- Not break existing tests
- Include a one-off DB migration or script to promote Wes's **specific user** to admin immediately, so he can verify the fix in his own UI without waiting for a fresh bootstrap (call the script `scripts/promote-founder-to-admin.mjs` following the `migrate-api-keys-compat.mjs` pattern from 01-01, with the email as a CLI arg)

Commit message: `fix(auth): first user of instance is admin by default (BUG-03)` with the root-cause explanation inline and `Co-Authored-By: Wes Sander <ucsandman@users.noreply.github.com>` trailer.
</action>
<read_first>
- `.planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md` (T1 output — source of truth for what to fix)
- Whatever specific file(s) the diagnosis names
- `scripts/migrate-api-keys-compat.mjs` (template for any new migration script)
- `app/lib/repositories/users.repository.js` (or wherever the users repository lives — diagnosis names the exact path)
</read_first>
<acceptance_criteria>
- File(s) identified in diagnosis are modified minimally — no unrelated changes
- Wes's user record has `role='admin'` (verified via SELECT or via successful UI access in T3)
- If a bootstrap fix was made, a new fresh instance bootstrap would create its first user as admin (covered by the regression test in T3 ideally, or manually verified)
- If a one-off promotion script was added (`scripts/promote-founder-to-admin.mjs`), it is idempotent and takes the email as a CLI arg
- `npm run lint` passes
- `npm run route-sql:check` passes
- `npm run openapi:check` passes
- Commit message explicitly names BUG-03 and inlines the root cause
</acceptance_criteria>
</task>

<task id="01.5-02-T3">
<name>Add regression test for first-user-is-admin behavior</name>
<action>
Add a unit test (Vitest, matching the project's existing test patterns in `__tests__/unit/`) at `__tests__/unit/bootstrap-first-user-admin.test.js` that verifies:

1. When a fresh instance has zero users, creating the first user assigns `role='admin'`
2. When an instance already has users, creating a new user does NOT override the default `role` to admin (i.e., first-user-admin is only for the first one, not a backdoor)
3. The `scripts/promote-founder-to-admin.mjs` script (if added in T2) correctly promotes a specific user by email and is idempotent on re-run

Mock the database layer using the project's existing mock pattern from `__tests__/unit/guard.route.test.js` (which Plan 01-01 updated to mock the repository layer). Do not hit a real database in unit tests.
</action>
<read_first>
- `__tests__/unit/guard.route.test.js` (for the repository-mocking pattern)
- `app/lib/repositories/users.repository.js` (for the function signature to mock)
- `scripts/promote-founder-to-admin.mjs` (if added in T2)
- `scripts/migrate-api-keys-compat.mjs` (as a template for script testing patterns)
</read_first>
<acceptance_criteria>
- New test file exists at `__tests__/unit/bootstrap-first-user-admin.test.js`
- Test suite has at least 3 test cases covering the three scenarios above
- `npm test -- --run bootstrap-first-user-admin` passes
- Tests mock the DB — no real connection required
</acceptance_criteria>
</task>

<task id="01.5-02-T4">
<name>End-to-end validation: Wes logs in and verifies admin access on /approvals</name>
<action>
Human-action task. After all prior tasks complete:

1. **Wes reloads the `/approvals` page** in his browser (full refresh, not cached)
2. **The "READ-ONLY ACCESS" banner should be gone**
3. **Wes should be able to click on an action in the queue and see Approve / Deny buttons**
4. **Trigger a `require_approval` flow** (e.g., a governed Bash command against a policy that requires approval) and verify:
   - The approval appears in the queue
   - Wes can click Approve
   - The waiting command in Claude Code proceeds
5. **Screenshot the Approvals page without the banner** and save it to `.planning/phases/01.5-governance-bugfix/01.5-BUG03-VALIDATION.md` as evidence

This is NOT something the executor can do. Prepare the environment and prompt Wes.

Write the validation result to `.planning/phases/01.5-governance-bugfix/01.5-BUG03-VALIDATION.md`:
- Screenshot evidence (or path to screenshot in the image-cache)
- Approval flow timing: time from command block → Wes approved → command resumed
- Any remaining issues
</action>
<read_first>
- `.planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md`
- The approvals page component (to know what the fixed state should look like)
</read_first>
<acceptance_criteria>
- `.planning/phases/01.5-governance-bugfix/01.5-BUG03-VALIDATION.md` exists with evidence
- Wes confirms visually that the READ-ONLY banner is gone
- A successful end-to-end approval flow is demonstrated at least once
</acceptance_criteria>
</task>

</tasks>

<verification>
After all tasks complete:

```bash
# Linting + contracts
npm run lint
npm run route-sql:check
npm run openapi:check

# Test suite (new + existing)
npm test -- --run bootstrap-first-user-admin
npm test -- --run users
npm test -- --run auth

# Validation artifacts exist
test -f .planning/phases/01.5-governance-bugfix/01.5-BUG03-DIAGNOSIS.md
test -f .planning/phases/01.5-governance-bugfix/01.5-BUG03-VALIDATION.md
```
</verification>

<must_haves>
1. **Founder role on own instance is admin.** Wes's user record is `role='admin'` (verified by UI access, not just DB query). The "READ-ONLY ACCESS" banner is gone from his `/approvals` page
2. **Bootstrap flow promotes the first user to admin automatically.** Future fresh DashClaw deploys via Vercel 1-click will create their first user as admin without manual SQL intervention. This is tested by the regression test in T3
3. **One-off promotion script for existing instances** (`scripts/promote-founder-to-admin.mjs`) exists so any existing user who was incorrectly created as `member` can be promoted by running the script with their email — this is needed for any user of DashClaw who installed before today and is still stuck as a member
4. **Commit on the combined-middleware fix from 01-01 is cleared or patched.** If the diagnosis shows `3dcb43dc` caused a regression, the fix preserves the Lief LAN/CSP intent while correctly resolving org/role — no silent revert of Phase 1's work
5. **Regression test covers all three scenarios** (first-user-is-admin, non-first-user-is-not-admin, promotion-script-is-idempotent)
6. **End-to-end validation by Wes himself** — not executor testimony, but Wes's screenshot + his confirmation that an approval flow completed with him as approver
</must_haves>

<notes>
**On scope:**
- This plan is BUG-03 only. BUG-01 and BUG-02 are in Plan 01.5-01, which this plan depends on
- Do NOT modify `README.md`, `packages/openclaw-plugin/`, `CHANGELOG.md`, `sdk/README.md`, `app/docs/page.js`, `docs/**`, `PROJECT_DETAILS.md`, `QUICK-START.md`, or any marketing/doc surface — those belong to a parallel agent
- Do NOT fold in other auth/RBAC improvements (session management, password reset, MFA, etc.) — keep the scope surgical to "first user is admin + founder can approve"

**On the dependency on 01.5-01:**
- This plan is Wave 2, depending on 01.5-01 (Wave 1). Rationale: the diagnosis for BUG-03 will go faster when BUG-02's fix means the decisions ledger actually shows blocked actions, so the diagnosis can reason about "what should each role see" with real data. Also, running both plans at the same time would risk overlapping on the server auth layer (guard + user repositories are neighbors), and sequential execution is safer for a small bugfix phase
- If 01.5-01 is blocked or cancelled, this plan can still run independently — its diagnosis task works from source code analysis and doesn't strictly require a working audit trail. But prefer the dependent order

**On the `3dcb43dc` suspicion:**
- Commit `3dcb43dc` is the Lief LAN/CSP/HSTS port from Plan 01-01. It specifically touched `app/api/auth/local/route.js` JWT org resolution (reading `DASHCLAW_API_KEY_ORG` with `org_default` fallback). This is the highest-probability candidate for the BUG-03 root cause because it was the most recent change to auth-layer org handling and it landed on main today. The diagnosis task specifically flags this commit for review
- **Do not revert `3dcb43dc` without approval** — it closes FIX-03, which is a Phase 1 requirement. If the diagnosis shows it caused BUG-03, the fix must preserve the LAN-plain-HTTP cookie behavior while correctly resolving org/role

**On files_modified being "TBD":**
- This plan's `files_modified` is intentionally "TBD-after-diagnosis" because the exact files depend on the root-cause hypothesis from T1. The executor should update the plan's frontmatter after T1 completes to reflect the actual files that will be modified in T2. This is a rare and explicit deviation from the normal "declare all files upfront" rule — it is allowed here because BUG-03 is a diagnosis-then-fix workflow and pre-declaring files would require speculation
</notes>

---
*Plan created: 2026-04-11 in response to the same live dogfood session that produced Plan 01.5-01*
*BUG-03 screenshot evidence: captured by Wes at 2026-04-11, `/approvals` page showing "READ-ONLY ACCESS — You are currently viewing as a member" banner*
