Warden plan
tier: destructive requires: [needs-postgres]
The five 2026-07-24 bug-sweep fixes hold as an accumulating regression floor:
create_wiki/log_entry/create_wiki_type accept an optional workspace (slug|id) resolved through one shared resolver (workspace-ref.ts) used by BOTH the handler and the permission gate, so a caller with write ability in a named non-root workspace lands content there, an omitted workspace still defaults to root, and a workspace outside the caller's org is rejected. The latent attach_signals no-gate hole is closed — it carries a real TOOL_PERMISSIONS write entry.POST /wikis/:id/backfill reverse-ingest extracts → dedupes → attaches signals idempotently (a re-run over an unchanged body creates no duplicate). The backfill reaches the pipeline via the queue only (no module→core/regen import).POST /workspaces grants the creator a workspace_admin membership inside the same transaction as the workspace insert, so a workspace can never commit with zero admins.ownership='member', authorId=self) and may attach a sponsor guardian at creation (recorded in the same transaction); a sponsor/admin promotes it to Company standing with author history retained; the MCP and in-process Socrates create_wiki paths derive ownership identically to the web path via the single shared deriveWikiOwnership (a member is not denied; a read-only caller is); and the stray "Head Guardian" nickname is gone.Fast file/grep guards fail early on a structural regression; the live-DB steps re-run the committed dbtests (the same suites CI runs) as the durable floor. Backend sprint, no web surface — no agent-browser E2E (see Shape).
greenlight-pg :5433 with the vector extension, greenlight-redis :6380).PROJECT_ROOT = a tree with the bug-sweep fixes merged — canary once PR #155 (Wave II, #132a/b) lands on top of the already-merged PR #153 (#150/#151/#152/#118/#132c) and PR #154 (member-authoring backend). Invoke via that tree's .warden symlink.pnpm, grep, psql on PATH. The env file (exports DATABASE_URL/ ADMIN_DATABASE_URL/REDIS_URL + the CI auth/key placeholders) 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)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"
# The committed dbtests each pushTestSchema()+truncate in their own hooks, but a
# prior plan (06 socrates) leaves its own schema state — reset so the sweep runs
# against a known-clean pgvector DB, then let the suites push their schema.
if psql "$DATABASE_URL" -q -X -c "DROP SCHEMA IF EXISTS public CASCADE; DROP SCHEMA IF EXISTS drizzle CASCADE; CREATE SCHEMA public; CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null 2>&1; then
warden_pass "reset robin_ci to a clean public schema with the vector extension"
else
warden_fail "could not reset robin_ci schema — is greenlight-pg (:5433) up with pgvector?"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# WS-1: one shared workspace resolver exists (handler + gate call the same one,
# so a gate-authorized workspace cannot diverge from the handler's write target).
if [ -f server/src/mcp/workspace-ref.ts ] && grep -q "resolveWorkspaceRef" server/src/mcp/workspace-ref.ts; then
warden_pass "shared resolveWorkspaceRef exists (single workspace-target resolver, #150)"
else
warden_fail "server/src/mcp/workspace-ref.ts / resolveWorkspaceRef missing — #150 double-lock fix regressed"
fi
# WS-2: attach_signals is write-gated (it was registered access:'read' with NO
# TOOL_PERMISSIONS entry — any caller could attach signals to any wiki).
if grep -q "attach_signals:" server/src/mcp/tool-permissions.ts; then
warden_pass "attach_signals carries a TOOL_PERMISSIONS write entry (no-gate hole closed, #150)"
else
warden_fail "attach_signals has no TOOL_PERMISSIONS entry — the write gate is bypassed again (#150)"
fi
# WS-3: the THREE create-path write tools (log_entry, create_wiki,
# create_wiki_type) each declare an OPTIONAL workspace target. Assert the
# actual param + its describe text, NOT a bare "workspace" substring — the
# file mentions workspaceMembers / list_workspaces everywhere, so a plain
# `grep workspace` can never fail and asserts nothing.
WS_PARAM_COUNT=$(grep -c "Target workspace slug or id" server/src/mcp/server.ts)
if grep -Eq "workspace: z" server/src/mcp/server.ts && [ "$WS_PARAM_COUNT" -ge 3 ]; then
warden_pass "the 3 MCP create/capture tools declare an optional workspace param (#150; describes=$WS_PARAM_COUNT)"
else
warden_fail "MCP write tools missing the optional workspace param — expected >=3 'Target workspace slug or id' describes, got $WS_PARAM_COUNT; writes root-locked again (#150)"
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}"
# Proves: a named-workspace write lands there; omitted → root; a foreign-org
# workspace is rejected on BOTH the gate and the handler; attach_signals is
# gated; list_workspaces returns the caller's writable targets.
if pnpm --filter @robin/server exec vitest run \
src/mcp/__tests__/workspace-targeting.dbtest.test.ts \
>/tmp/warden-mcp-workspace.log 2>&1; then
warden_pass "#150 MCP workspace-targeting dbtest passes against a live DB"
else
tail -25 /tmp/warden-mcp-workspace.log
warden_fail "#150 workspace-targeting dbtest FAILED — see /tmp/warden-mcp-workspace.log"
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}"
# BF-1 (static): the on-demand backfill trigger exists on the wikis resource,
# and it reaches regen/pipeline via the queue — modules must NOT import core/regen.
# Anchor the boundary check to a real import PATH (from '…core/regen' / require)
# — a bare `grep core/regen` also matches the comments here that state the
# module never imports it, a false positive (the boundary is really enforced by
# depcruise's modules-never-import-pipeline).
if grep -q "/:id/backfill" server/src/modules/wikis/routes.ts \
&& ! grep -qE "from ['\"][^'\"]*core/regen|require\(['\"][^'\"]*core/regen" server/src/modules/wikis/routes.ts; then
warden_pass "POST /wikis/:id/backfill exists and does not import core/regen (queue-only, #151)"
else
warden_fail "backfill route missing or reaches into core/regen directly (#151 boundary)"
fi
# BF-2 (live DB): regen preserves a zero-signal authored body (no blank), and a
# backfill re-run over an unchanged body creates no duplicate signal (idempotent).
if pnpm --filter @robin/server exec vitest run \
src/core/regen/regen.test.ts src/modules/wikis/wiki-backfill.dbtest.test.ts \
>/tmp/warden-backfill.log 2>&1; then
warden_pass "#151 regen no-blank guard + idempotent backfill pass against a live DB"
else
tail -25 /tmp/warden-backfill.log
warden_fail "#151 regen/backfill suite FAILED — see /tmp/warden-backfill.log"
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}"
# WA-1 (static): the creator's workspace_admin membership is inserted INSIDE
# the create transaction, so an admin-less workspace can never commit. Scope
# the check to the `db.transaction(async (tx) ... return ws)` block and require
# BOTH the workspaceMembers insert and the workspace_admin role within it —
# not just that the two tokens appear somewhere in the file (which would pass
# even if the grant moved outside the tx or onto a different route).
RT=server/src/modules/workspaces/routes.ts
if awk '/db\.transaction\(async \(tx\)/{f=1} f&&/insert\(workspaceMembers\)/{m=1} f&&/workspace_admin/{r=1} f&&/return ws/{f=0} END{exit !(m&&r)}' "$RT"; then
warden_pass "POST /workspaces inserts the creator's workspace_admin membership inside the create transaction (#152)"
else
warden_fail "the workspace_admin grant is no longer inside POST /workspaces' create tx — admin-less workspaces possible (#152)"
fi
# WA-2 (live DB): creating a workspace yields exactly one workspace_admin.
if pnpm --filter @robin/server exec vitest run \
src/modules/workspaces/routes.test.ts \
>/tmp/warden-ws-admin.log 2>&1; then
warden_pass "#152 workspace-admin-at-creation dbtest passes against a live DB"
else
tail -25 /tmp/warden-ws-admin.log
warden_fail "#152 workspaces route suite FAILED — see /tmp/warden-ws-admin.log"
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}"
# CA-1 (static): the org-admin-scoped connected-apps read exists.
if grep -q "connected-apps" server/src/modules/admin/routes/admin.ts; then
warden_pass "GET /admin/connected-apps org-scoped read is present (#118)"
else
warden_fail "no /admin/connected-apps read — third-party client names fall back to raw ids again (#118)"
fi
# CA-2 (live DB): org-A caller resolves the third-party client NAME; an org-B-only
# client is invisible (no cross-org leak); a non-admin is denied (403).
if pnpm --filter @robin/server exec vitest run \
src/modules/admin/routes/connected-apps.dbtest.test.ts \
>/tmp/warden-connected-apps.log 2>&1; then
warden_pass "#118 connected-apps org-isolation dbtest passes against a live DB"
else
tail -25 /tmp/warden-connected-apps.log
warden_fail "#118 connected-apps dbtest FAILED — see /tmp/warden-connected-apps.log"
fi
set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# MW-1: deriveWikiOwnership is the single shared source — the HTTP route, the MCP
# handler, and the MCP gate resolver all call it, and the Socrates in-process
# transport threads callerAbility so it derives identically (no divergent path).
# Anchor to the CALL shape `deriveWikiOwnership(` — a bare token also matches the
# explanatory comments/imports, so a regression that deletes the call but leaves
# a comment would pass. `callerAbility:` anchors to the ctx assignment, not prose.
if [ -f server/src/lib/wiki-ownership.ts ] \
&& grep -q "deriveWikiOwnership(" server/src/modules/wikis/routes.ts \
&& grep -q "deriveWikiOwnership(" server/src/mcp/handlers.ts \
&& grep -q "deriveWikiOwnership(" server/src/mcp/tool-permissions.ts \
&& grep -q "callerAbility:" server/src/agent/run.ts; then
warden_pass "deriveWikiOwnership is the single ownership source across HTTP + MCP + Socrates (#132b)"
else
warden_fail "ownership derivation is missing a call site (HTTP/MCP/gate/Socrates) — parity regressed (#132b)"
fi
# MW-2: the stray "Head Guardian" nickname stays retired (comment + UI string).
if ! grep -rqIE "Head Guardian|isHeadGuardian" server/src app/src; then
warden_pass "no 'Head Guardian' nickname in server/src or app/src (#132c)"
else
grep -rInE "Head Guardian|isHeadGuardian" server/src app/src | head -5
warden_fail "'Head Guardian' nickname reappeared — should read workspace_admin/Admin (#132c)"
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}"
# Proves, end to end against a live DB:
# - MCP create_wiki: member → ownership='member'/author=self (not denied),
# admin → company/null, member-into-named-workspace authorized with BOTH
# the target workspace AND member ownership (create-wiki-ownership).
# - sponsor-at-creation attaches a kind='sponsor' guardian_grants row in the
# SAME create transaction; promotion accept flips to company + retains
# authorId; decline path (routes.promotion).
# - Socrates in-process turn: a member's create_wiki derives ownership=member,
# a read-only turn is DENIED with no row written (run.dbtest).
if pnpm --filter @robin/server exec vitest run \
src/mcp/__tests__/create-wiki-ownership.dbtest.test.ts \
src/modules/wikis/routes.promotion.dbtest.test.ts \
src/agent/run.dbtest.test.ts \
>/tmp/warden-member-authoring.log 2>&1; then
warden_pass "#132 member-authoring (ownership parity + sponsor-at-creation + promotion + Socrates gate) passes against a live DB"
else
tail -30 /tmp/warden-member-authoring.log
warden_fail "#132 member-authoring suite FAILED — see /tmp/warden-member-authoring.log"
fi
Backend sprint spanning five issues — no dedicated web surface, so no agent-browser E2E (mirrors plan 08). The acceptance core of every issue is a committed dbtest CI already runs; warden re-runs them as the accumulating regression floor, gated behind fast structural greps that fail loud on a shape regression (the resolver going away, attach_signals losing its entry, a module reaching into core/regen, ownership losing a call site, "Head Guardian" creeping back). Step 1 resets the shared robin_ci schema because a preceding plan (06 socrates) leaves its own state; each suite then pushes its schema and truncates in its own hooks.
Stays manual / not asserted here (so the operator knows the edges):
app/src/components/screens/settings/connected-apps.test.tsx, not a warden browser step.