mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
52dc41b96a
* feat(apps): app install provisioning, install codes and redeem (TASK-3397, U8b) Confirm re-normalizes the staged pack, compares it with the reviewed preview, and provisions in ONE store transaction that re-checks every state-dependent fact: the install row, companion collections (via_app), the bot (editor/viewer, collection_access specific over the companions), the install client, the artifacts as owner drafts stamped with their pack and digests, a one-time install code, and the pending record's deletion. A refusal names the artifact or collection, the field and the holder, and writes nothing. Redeem is mounted outside the auth stack with a 4 KB whole-body cap and per-address and per-install limiters; it consumes the code and rotates the client secret. Owners can issue a new code to recover a lost redeem. * fix(apps): U8b codex round 1: re-check the destination schema in the provisioning tx, a dead install code never charges the install limiter, a reserved companion slug is refused (TASK-3397) * refactor(apps): U8b provisioning re-derives the normalization inside its transaction under locks (TASK-3397) Replaces the per-fact re-checks (codex rounds 1-2 kept finding facts the list missed) with a re-derivation: ProvisionAppInstall takes the pending row, the workspace lock, then on Postgres FOR SHARE on every collection row and the owner's membership row, re-runs the preview normalization on the transaction through the caller's derive function, locks the relation targets it resolved, and derives again; the caller compares each pass with the reviewed digests. SQLite is covered by BEGIN IMMEDIATE. The normalization path takes a Queryer (relation passes, unique check, slug probe, kind lookup); the pool versions delegate to it. Parity is pinned over the 8 importer fixtures plus a relation-bearing case captured before the refactor, the derivation is proven write-free under the write-capture harness, and the lock table is pinned on both dialects. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): U8b codex round 3: compare the reviewed changes list, lock the caller's visibility rows, take items before the membership (TASK-3397) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * docs(apps): U8b lock table matches the round 3 order (TASK-3397) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * refactor(apps): U8b locks break dependency cycles, not every read (TASK-3397) Lead ruling, day 86, superseding "lock every read": the visibility locks added in round 3 created deadlock cycles with member removal and account deletion and could never cover an inserted grant or a cached role (codex round 4). Provisioning now takes the pending row, the workspace lock and FOR SHARE on the workspace's collection rows, reads the owner role plainly, and derives once. The serializability argument for each raced writer class is in the lock comment. The two deadlock scenarios stay as tests. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(apps): U8b codex round 5: lock the confirming owner's users row first, account deletion is a real cycle (TASK-3397) Account deletion reads the bot and membership provisioning writes and deletes the owner provisioning read, and its FK cascade from users reaches the pending row. Provisioning now takes the owner's users row FOR SHARE before the pending row; deletion's first statement locks the same row, so the two serialize whole in either order. Tested through the real deletion in both orders. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * test(apps): U8b account-deletion cycle test uses the canonical owner (TASK-3397) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * feat(apps): U8b adoption re-stamps via_app, only from an uninstalled install (TASK-3397) Lead ruling, day 86 (from the #1771 review): companions are via_app = install, so an adopted companion is re-stamped to the new install inside the provisioning transaction. Any holder that is not uninstalled is refused (app_already_installed, naming the state). Lock 3 becomes FOR NO KEY UPDATE so the re-stamp never upgrades a share lock. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * test(apps): U8b redeem stays outside the app API token router (TASK-3397) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * feat(apps): bot lookups key on app_installs.bot_user_id, the address as the agreement check (TASK-3397) The U5/U8b agreement: whichever landed second switches the issuance barrier, AppPrincipalForInstall and the members list from the bot's address to the bot_user_id provisioning sets, keeping the address as an agreement check. An install without bot_user_id (made before provisioning) falls back to the address. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * chore(apps): drop a dead assignment in diffChanges (lint) (TASK-3397) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS