Warden plan
tier: hermetic requires: []
The permissions sprint's authorization engine holds structurally: ONE CASL engine (@robin/permissions) composing org-role × workspace-membership × guardian-grant policies, translated to SQL by accessibleByDrizzle (proven ≡ ability.can), enforced at every layer (HTTP routers, the MCP tool gate, queue snapshots), the legacy checkWritePermission/callerRole path retired, guardian-as-wiki-only-bundle (D31), the 8 permission.* audit events, and the cross-tenant holes the validator chain closed (MCP real-org scoping, wiki-types + admin/people gated). File/grep-level assertions — fast, deterministic, no DB; the DB-backed equivalence + full suite is plan 01's green bar.
None beyond a clean checkout of the branch under test.
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# The pure ability builder + its whitelist guard live in the package barrel.
{ grep -qrE "export (function|const) defineAbilityFor" packages/permissions/src \
&& grep -qrE "assertWhitelistedConditions" packages/permissions/src; } \
&& warden_pass "defineAbilityFor + assertWhitelistedConditions exported from @robin/permissions (D4)" \
|| warden_fail "the CASL ability builder is missing from @robin/permissions"
# The rules->SQL translator + its subject map are server-side.
{ [ -f server/src/core/authz/accessible-by-drizzle.ts ] \
&& grep -qE "export (async )?function accessibleByDrizzle" server/src/core/authz/accessible-by-drizzle.ts \
&& grep -qrE "SUBJECT_TABLES" server/src/core/authz; } \
&& warden_pass "accessibleByDrizzle rules->where translator + SUBJECT_TABLES present" \
|| warden_fail "accessibleByDrizzle or SUBJECT_TABLES is missing from core/authz"
# The equivalence guards exist (accessibleByDrizzle ≡ can; readableWorkspaceIds ≡ content set).
{ [ -f server/src/core/authz/equivalence.dbtest.test.ts ] \
&& [ -f server/src/core/authz/readable-workspaces.dbtest.test.ts ]; } \
&& warden_pass "both DB equivalence-guard suites present (can≡SQL, readableWorkspaceIds≡content-set)" \
|| warden_fail "an equivalence-guard test suite is missing"
# ONE engine: the legacy write-check + callerRole are fully retired from server/src.
LEGACY=$(grep -rE "\b(checkWritePermission|callerRole)\b" server/src 2>/dev/null | wc -l | tr -d ' ')
[ "$LEGACY" = "0" ] \
&& warden_pass "legacy checkWritePermission/callerRole retired — one engine (grep→0)" \
|| { grep -rnE "\b(checkWritePermission|callerRole)\b" server/src | head; warden_fail "$LEGACY legacy authz call(s) remain in server/src"; }
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# The migrated content routers gate + scope via the ability engine.
MISS=0
for m in signals wikis people content; do
grep -qE "requireAbility" "server/src/modules/$m/routes.ts" \
&& grep -qE "accessibleByDrizzle" "server/src/modules/$m/routes.ts" \
|| { echo "router not ability-enforced: $m"; MISS=1; }
done
[ "$MISS" = "0" ] \
&& warden_pass "signals/wikis/people/content routers gate (requireAbility) + scope (accessibleByDrizzle)" \
|| warden_fail "a content router is not wired to the ability engine"
# The MCP tool gate is mandatory (non-optional) in the registry.
{ grep -qE "checkToolPermission" server/src/mcp/registry.ts \
&& grep -qE "checkToolPermission:" server/src/mcp/tool-permissions.ts 2>/dev/null \
|| grep -qrE "evaluateToolPermission|checkToolPermission" server/src/mcp; } \
&& warden_pass "ability-driven MCP tool gate present in the registry (C-21/D20)" \
|| warden_fail "the MCP tool permission gate is missing"
# GET /audit-log is ability-scoped (was an any-signed-in-user unscoped read).
grep -qE "requireAbility\('read', ?'AuditEvent'\)" server/src/modules/audit/routes.ts \
&& warden_pass "GET /audit-log scoped by the read-audit ability (D-10)" \
|| warden_fail "audit-log read is not ability-scoped"
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# D31 triple-enforced: DB CHECK + app zod + port filter.
grep -qrE "guardian_grants_subject_type_wiki|subject_type.*=.*'wiki'|CHECK.*subject_type" server/drizzle/migrations \
&& warden_pass "guardian_grants subject_type='wiki' CHECK present in a migration (D31)" \
|| warden_fail "the D31 guardian wiki-only DB CHECK is missing"
grep -qE "z\.literal\('wiki'\)" server/src/routes/org.ts \
&& warden_pass "app-layer z.literal('wiki') at the guardian-grant write-site (D31)" \
|| warden_fail "the D31 app-layer guardian-grant zod is missing"
# The port reads guardian grants filtered to wiki.
grep -qrE "subjectType.*wiki|subject_type.*wiki" server/src/core/authz/port.ts \
&& warden_pass "PermissionPort filters guardian grants to subject_type='wiki'" \
|| warden_fail "the guardian-grant wiki filter is missing from the port"
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# The 8 permission.* events are emitted.
EMITS=$(grep -rhoE "permission\.(role_changed|member_removed|member_added|grant_added|grant_revoked|workspace_visibility_changed|key_issued|key_revoked)" server/src 2>/dev/null | sort -u | wc -l | tr -d ' ')
[ "$EMITS" -ge 8 ] \
&& warden_pass "all 8 permission.* audit events emitted ($EMITS/8)" \
|| warden_fail "only $EMITS/8 permission.* events found"
# Cross-tenant MCP write gate: the target wiki's REAL org is resolved (not the caller's),
# closing the super_admin tautology the validator chain found.
grep -qE "innerJoin\(workspaces" server/src/routes/mcp.ts \
&& warden_pass "MCP write gate resolves the target wiki's real org (cross-tenant fix)" \
|| warden_fail "resolveWikiForGate no longer joins the wiki's real org — cross-tenant regression"
# The previously-ungated session-only mutation routers are now gated.
{ grep -qE "requireAbility\('manage', ?'WikiType'\)" server/src/modules/preferences/wiki-types.routes.ts \
&& grep -qE "requireAbility" server/src/modules/admin/routes/people.ts; } \
&& warden_pass "wiki-types + admin/people mutations gated (closed cross-tenant holes)" \
|| warden_fail "a previously-ungated mutation router regressed to unenforced"