Warden plan
tier: needs-postgres requires: []
The wiki-types sprint's D35 ruling holds against the code + a live database: wiki_types is workspace-scoped (synthetic id PK + workspace_id NOT NULL + unique(workspace_id, slug)), the bare-slug global PK is gone, migration 0007 guards its delete-and-reseed against destroying customized rows, every wikis.type → wiki_types.slug join self-binds on the wiki's workspace, and the cross-tenant isolation invariant holds end-to-end (a user_modified edit in one workspace is invisible to another). Steps 1-2 are fast file/grep assertions that later sprints re-run as a regression floor; step 3 runs the committed DB-backed isolation test against the branch's own worktree (deps + pg :5433).
greenlight-pg :5433 with the vector extension, greenlight-redis :6380). See .warden/HANDOFF.md.PROJECT_ROOT points at the wiki-types worktree; invoke through that worktree's .warden symlink so the greps + the dbtest target the branch code.pnpm, grep, ls 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)}"
# WT-1: the table is workspace-scoped — synthetic id PK + workspace_id NOT NULL
# FK + unique(workspace_id, slug); the bare-slug PK is gone. Assert against the
# wikiTypes table BLOCK (extracted via awk) so multi-line column formatting
# (Biome splits .notNull()/.references() across lines) doesn't false-negative.
SCHEMA=server/src/db/schema.ts
BLOCK=$(awk '/export const wikiTypes = pgTable\(/,/^\)/' "$SCHEMA")
if printf '%s' "$BLOCK" | grep -q "wiki_types_workspace_slug_uidx" \
&& printf '%s' "$BLOCK" | grep -q "text('workspace_id')" \
&& printf '%s' "$BLOCK" | grep -q "\.notNull()" \
&& printf '%s' "$BLOCK" | grep -q "references(() => workspaces.id"; then
warden_pass "wiki_types carries workspace_id NOT NULL FK + unique(workspace_id, slug)"
else
warden_fail "wiki_types is missing the workspace_id column or the (workspace_id, slug) unique index"
fi
# The bare-slug primary key must be gone (that was the cross-tenant gap).
if grep -nE "slug: text\('slug'\)\.primaryKey\(\)" "$SCHEMA" | grep -q .; then
warden_fail "wiki_types still has a bare-slug primaryKey — the global-registry gap survives"
else
warden_pass "the bare-slug primary key is gone"
fi
# WT-3: migration 0007 guards the delete-and-reseed so a customized row aborts
# it instead of being silently destroyed.
MIG=server/drizzle/migrations/0007_wiki_types_workspace_scope.sql
if grep -q "RAISE EXCEPTION" "$MIG" && grep -q "user_modified" "$MIG" && grep -q 'DELETE FROM "wiki_types"' "$MIG"; then
warden_pass "migration 0007 guards its DELETE against user_modified rows"
else
warden_fail "migration 0007 is missing the user_modified data-loss guard before its DELETE"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# WT-2: every wikis.type → wiki_types.slug join must self-bind on workspace.
# A bare join (slug eq with NO workspaceId eq on the same statement) would leak
# another tenant's type descriptor. Scan the four known join files; each file
# that joins on wikiTypes.slug must also reference wikiTypes.workspaceId.
JOIN_FILES="server/src/mcp/resolvers.ts server/src/mcp/server.ts server/src/modules/wikis/routes.ts"
leak=0
for f in $JOIN_FILES; do
if grep -q "eq(wikis.type, wikiTypes.slug)" "$f"; then
if ! grep -q "eq(wikiTypes.workspaceId, wikis.workspaceId)" "$f"; then
warden_observe "join in $f does not self-bind on workspaceId"
leak=1
fi
fi
done
[ "$leak" = 0 ] \
&& warden_pass "every wikis.type→wiki_types.slug join self-binds on wikiTypes.workspaceId (WT-2)" \
|| warden_fail "a wiki_types join is missing its workspace self-bind — cross-tenant descriptor leak"
# WT-6: the boot/HTTP/MCP standalone seeder + reads carry a workspace key.
# seedWikiTypes must be per-workspace (takes a workspaceId), never the old
# zero-arg global seed.
if grep -qE "seedWikiTypes\(workspaceId" server/src/bootstrap/seed-wiki-types.ts \
&& ! grep -qE "await seedWikiTypes\(\)" server/src/index.ts; then
warden_pass "seedWikiTypes is per-workspace (no surviving zero-arg global seed)"
else
warden_fail "seedWikiTypes still has a global (zero-arg) seed path"
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 committed isolation regression seeds two workspaces + a fresh third and
# asserts: fresh workspace gets the full built-in set; a user_modified edit in A
# is invisible to B across list/get/regen; same slug lives independently per
# workspace; the wikis.type join resolves within-workspace only (driven through
# the production listWikis resolver). Run it against the branch's real schema.
if pnpm --filter @robin/server exec vitest run \
src/mcp/__tests__/wiki-types-isolation.dbtest.test.ts >/tmp/warden-wt-isolation.log 2>&1; then
warden_pass "cross-tenant wiki_types isolation dbtest passes against a live DB (WT-9)"
else
tail -20 /tmp/warden-wt-isolation.log
warden_fail "cross-tenant wiki_types isolation dbtest FAILED — see /tmp/warden-wt-isolation.log"
fi
Backend sprint, no web surface — so no agent-browser E2E here (unlike 06). The acceptance core (WT-9 cross-tenant isolation) is a committed dbtest that CI's verify job already runs; step 3 re-runs it under warden as the accumulating regression floor. Steps 1-2 are the cheap structural guards that would catch a future refactor silently dropping a workspace predicate (the exact class of bug the security review hunted for). If the join file set changes, update the JOIN_FILES list in step 2.