Warden plan
tier: needs-postgres requires: []
The workspaces router READS run on the ability engine and are provably equivalent to the retired hand-written visibility predicate: a read Workspace rule exists for non-admin roles (open arm + membership arm), visibility is mapped in SUBJECT_TABLES.Workspace so the open arm is a real predicate (not sql\false\`), the hand-written visibilityPredicate is gone from the read path, and the DB equivalence sweep (accessibleByDrizzle ≡ ability.can for read Workspace`) is green. Steps 1-2 are fast file/grep guards; step 3 runs the committed equivalence dbtest against a live DB.
greenlight-pg :5433 + vector, greenlight-redis :6380).PROJECT_ROOT = the workspace-reads worktree (invoke via its .warden symlink).pnpm, grep on PATH. The env file is provisioned automatically by .warden/run.sh from the tracked .warden/env/ci-env.template.sh plus the machine-local secrets file ~/.config/robin/warden-secrets.sh (mode 600, outside every git repository; override with WARDEN_SECRETS_FILE); first-time setup: mkdir -p ~/.config/robin && cp .warden/env/secrets.local.example.sh ~/.config/robin/warden-secrets.sh && chmod 600 ~/.config/robin/warden-secrets.sh.set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# WR-1: a read Workspace rule exists (both arms).
AB=packages/permissions/src/ability.ts
if grep -q "can('read', 'Workspace'" "$AB" || grep -q "can('read','Workspace'" "$AB"; then
warden_pass "a read Workspace ability rule is present"
else
warden_fail "no read Workspace rule in ability.ts — non-admin workspace reads would 403"
fi
# WR-2 (load-bearing): visibility mapped in SUBJECT_TABLES.Workspace, else the
# open arm compiles to sql`false` and every open-but-non-member read silently 404s.
ST=server/src/core/authz/subject-tables.ts
# The mapping is distinctive — it appears only in the Workspace subject-table
# entry — so a direct grep is sufficient and robust against inline-object
# formatting (an awk block-range closes early on the first `}`).
if grep -q "visibility: workspaces.visibility" "$ST"; then
warden_pass "SUBJECT_TABLES.Workspace maps the visibility column (open arm is load-bearing)"
else
warden_fail "SUBJECT_TABLES.Workspace is missing visibility — the read-Workspace open arm no-ops to false"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# The LIST read must run through accessibleByDrizzle now, not the hand-rolled
# visibilityPredicate + isPrivileged-for-reads branch.
RT=server/src/modules/workspaces/routes.ts
if grep -q "accessibleByDrizzle" "$RT" && ! grep -q "visibilityPredicate" "$RT"; then
warden_pass "workspaces LIST reads via accessibleByDrizzle; hand-written visibilityPredicate retired"
else
warden_fail "workspaces routes still carry the hand-written visibilityPredicate (read not migrated)"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"
# The equivalence dbtest sweeps accessibleByDrizzle vs ability.can across a
# role × visibility × membership principal matrix (incl. read Workspace), and the
# route suite pins the LIST-hiding + detail/roster 404 tightening.
if pnpm --filter @robin/server exec vitest run \
src/core/authz/equivalence.dbtest.test.ts src/modules/workspaces/routes.test.ts \
>/tmp/warden-wsreads.log 2>&1; then
warden_pass "read Workspace equivalence + workspaces route reads pass against a live DB (WR-4)"
else
tail -20 /tmp/warden-wsreads.log
warden_fail "workspace-reads equivalence/route suite FAILED — see /tmp/warden-wsreads.log"
fi
Backend sprint, no web surface — no agent-browser E2E. The acceptance core (D36 equivalence) is a committed dbtest CI already runs; step 3 re-runs it under warden as the accumulating regression floor. The detail/roster reads were deliberately tightened (non-member of a private workspace → 404, closing a latent by-id read leak) — the route suite covers that 404; consumer impact was orchestrator-verified low (only an admin roster drawer + LIST-sourced switcher ids consume these endpoints).