Skip to content

fix(security): bind writes to live membership - #133

Merged
KIDA-MNESIA merged 1 commit into
yetone:mainfrom
KIDA-MNESIA:fix/sec-06-membership-authorization
Aug 30, 2026
Merged

fix(security): bind writes to live membership#133
KIDA-MNESIA merged 1 commit into
yetone:mainfrom
KIDA-MNESIA:fix/sec-06-membership-authorization

Conversation

@KIDA-MNESIA

Copy link
Copy Markdown
Collaborator

Summary

  • serialize membership mutations with current-tenant actor and target locks, preserving concurrent joins/leaves without reviving revoked members
  • bind HTTP, CLI, runtime, WebSocket, document, poll, scheduler, email, and convene side effects to live authorization at the final write or delivery boundary
  • persist exact one-shot departure recipients, durable runtime notice idempotency, and email retry identity while keeping Redis fan-out bounded and fail-open after durable commits
  • harden document cold loading against pool self-exhaustion and publish document change events only after commit

This follows up #115: that PR fixed whole-array lost updates; this PR closes the remaining authorization TOCTOU paths around those membership changes.

Security invariants

  • participant and conversation locks use a consistent order, and transactional work reuses the same PostgreSQL client
  • tenant moves, offboarding, kicks, and leaves serialize before or after protected side effects
  • revoked members receive only their exact durable departure notice and cannot regain ordinary room access
  • auxiliary runtime writes and reads require current tenant and conversation authorization
  • document hydration reserves one connection for snapshot plus tail and evicts rejected rooms

Validation

  • npm run lint (passes; one pre-existing informational finding in server/src/agents/cli.ts)
  • npm run guard:big-brain
  • npm run guard:llm-tracked
  • npm run guard:engine-registry
  • npm run typecheck
  • npm run server:typecheck
  • npm run build
  • full unit suite: 967 total, 963 passed, 4 platform skips
  • full integration suite: 245 total, 240 passed, 5 credential-gated Resend skips
  • deterministic concurrency coverage: HTTP and CLI revocation races, 20-slot document pool pressure, failed hydration retry, post-commit document events, runtime auxiliary authorization, and mid-convene kicks
  • independent strict security review: 0 merge blockers

@KIDA-MNESIA
KIDA-MNESIA marked this pull request as ready for review August 30, 2026 22:41
@KIDA-MNESIA
KIDA-MNESIA merged commit 7697bbf into yetone:main Aug 30, 2026
7 checks passed
yetone added a commit that referenced this pull request Aug 31, 2026
…nd-kept list

Production served a v0.11.0 build whose migration had silently not applied.
Every BYOA daemon's GET /inbox 500'd on
`column delivered.delivery_recipient_id does not exist`; ~900 daemons retried,
the database saturated, /api/health 503'd on "health db check timed out", and
agent runs fell from ~6000 per 10 minutes to near zero.

The migration was in the image and correct. `ALTER TABLE messages ADD COLUMN
IF NOT EXISTS delivery_recipient_id` needs a brief AccessExclusiveLock on the
hottest table in the schema; under live agent traffic it lost the race and
tripped the 5s lock_timeout (55P03). ensureSchema then asked
schemaAlreadyCurrent() whether it could skip, that function said yes, and
migrate-bin logged "schema ensured" and exited 0 — so the main container
started against a schema missing a column the app queries on every request.

It said yes because it checked a hand-maintained list of sentinels, and #133
added the column without adding it there. A whitelist cannot catch what nobody
remembered to add: the absent column was simply never asked about. The doc
comment claimed the opposite — that forgetting was benign and the shortcut
"can never skip a migration that hasn't actually been applied" — and that claim
is what made the omission look harmless. The same shape had already been caught
twice (llm_calls, llm_calls_rollup), each time answered by appending one more
entry to the list.

So stop keeping the list. `ddlExpectations()` reads every
`CREATE TABLE IF NOT EXISTS` and `ALTER TABLE … ADD COLUMN IF NOT EXISTS` out
of the DDL text (comments stripped, so a commented-out example cannot become a
phantom expectation that disables the shortcut forever), and the check verifies
all of them in one query. The few correctness indexes stay explicit because
partial/concurrent index DDL is not derivable the same way. A negative result
now names what is missing, rather than exiting 0 in silence.

Applied to production separately: the migration ran clean in 17s once traffic
was off, which is the whole shape of the bug — it only fails under the load
that makes the shortcut fire.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant