Files
buzz/docs/operator-user-deletion-intake.md
ravarora2 834dca26bf Generate user community inventory for the dummy deletion workflow (#8605)
## Summary

User deletion can now discover current communities from a hex public key
or npub, save that inventory, and run it through the existing approval
and dummy workflow. `--pubkey demo` remains the default. Execution only
prints messages; it does not fence users or delete data.

Discovery combines explicit relay memberships with active channel
memberships, including users who have no relay membership row. It
deduplicates communities and preserves owner relationships. The saved
manifest contains only community IDs, labels and relationships.
Historical discovery and owned-community execution remain later work.
Submission rejects owner inventories atomically with a specific
unsupported-execution error; preview still includes owners.

This is an expand-schema change after #8520. Migrations 0072–0073
replace the demo-only target check with a bounded identifier check, with
5-second lock and 30-second statement timeouts. The database keeps the
user-only entity-type check while allowing different identity formats in
the target field. Workflow-specific commands reject entity types they
cannot execute. The application currently resolves demo and hex/npub
inputs; account-ID lookup is future work. The desired-state constraint
matches. No backfill, trigger, function, or foreign key is added.
Existing demo rows remain valid. Old binaries fail closed on new
manifests; use the new CLI for their approval and execution. Older
auto-migrating binaries need the documented rollback procedure.

Identity parsing and demo fixture selection stay in intake. Approval and
resume use the saved target without reparsing its format. The user
workflow defines its stage names and handlers; CLI stage validation
reads those rules instead of a separate three-name list. Shared
persistence accepts caller-defined stages. Permanent target uniqueness
is unchanged. Dummy submissions and execution belong in isolated,
disposable development databases; production preview uses read-only
access. CLI operator names remain self-asserted.

### Related issue

Follows #8520. Searched open PRs; no duplicate inventory change found.
#8489 is earlier workflow scaffolding.

### Testing

Built `buzz-admin` locally and applied all migrations to a fresh
disposable PostgreSQL database. Ran the actual CLI through preview →
submit → inspect → approve → `run --next-step` → resume → completed for
both demo and a public key with a channel-only membership fixture. Demo
completed five stages; the public-key run completed three. Injected
`--fail-stage purge_db`, waited for the retry deadline, and resumed the
same failed stage successfully. Membership data remained intact.

Repeated the CLI workflow on October 26 for demo and an npub backed by a
fresh production inventory. Production access used read-only credentials
and the real `inventory --pubkey "$SUBJECT"` handler. Copied only the
relevant discovery fields into an isolated local PostgreSQL database;
local channel parent rows supplied placeholder fields for foreign keys.
The inventory saved by local submission exactly matched the production
result. All workflow writes stayed local.

For each input, ran `submit --pubkey "$SUBJECT" --requested-by
local-e2e-submitter`, inspected the saved inventory, and confirmed
execution before approval was rejected. Approved its digest, then ran
each handler with `run "$REQUEST_ID" --next-step --stage-delay-ms 2500`
in a new process. Captured all three workflow tables during and after
every stage. Demo completed five stages across two communities; the npub
completed three stages for its discovered community. Each handler saved
`dummy: true`. Both requests finished with no current stage, no worker
lease, and no retries. Copied memberships remained intact. This
additional run exercised orderly resume; it did not inject a failure.

The reproducible command sequence and fixture setup are in [the
inventory walkthrough](docs/operator-user-deletion-inventory.md).
Production was not changed. The earlier full-workflow runs used
synthetic local fixtures; the additional npub run used captured
production discovery fields. Repository-wide `just ci` was not run;
validation focused on the affected packages and PostgreSQL workflow. The
pre-push desktop build failed with “Too many open files in system.”
Stopped the broader hook run and bypassed it to publish the earlier
commits; the affected-package Clippy and formatting checks had passed.

Generated with Codex

---------

Signed-off-by: Ravneet Arora <rarora@squareup.com>
2026-10-26 15:44:12 -04:00

178 lines
8.0 KiB
Markdown

# Prepare and approve a synthetic user-deletion request
For read-only discovery of actual memberships from a hex key or npub, see
[Preview a user's current-community inventory](operator-user-deletion-inventory.md).
Submit saves a request and its inventory together. By default (`--pubkey demo`),
this is community B (participant), then community C (former participant).
Use `--pubkey <hex-or-npub>` to discover current communities instead. The request
stores the normalized hex key as its target; the inventory contains only
community IDs, labels and relationships. `--target demo:<label>` still works for
separate demo runs and cannot be combined with `--pubkey`.
Approve saves authorization without creating stages. The `run` command initializes
execution through `get_next_stage`, dispatches dummy handlers, and asks the
transition function for the next saved row. No user or community data is deleted.
See [the inventory walkthrough](operator-user-deletion-inventory.md) for both
complete paths.
## Prepare a local database
From the repository root, activate Hermit and start a separate PostgreSQL
container. These are disposable local credentials.
```bash
. ./bin/activate-hermit
docker run --detach --name buzz-user-deletion-intake \
--publish 127.0.0.1:55433:5432 \
--env POSTGRES_USER=buzz \
--env POSTGRES_PASSWORD=buzz-local-demo \
--env POSTGRES_DB=buzz postgres:17-alpine
export DATABASE_URL=postgres://buzz:buzz-local-demo@127.0.0.1:55433/buzz
cargo build -p buzz-admin
# Wait for "accepting connections" before migrating.
docker exec buzz-user-deletion-intake pg_isready -U buzz -d buzz
./target/debug/buzz-admin migrate
```
Use an unused port and container name if these are occupied. The commands below
need only PostgreSQL; a relay, Redis, and object storage are unnecessary.
## Submit and see the row
```bash
./target/debug/buzz-admin user-deletions submit \
--target demo:alice --requested-by operator-a
```
The command prints the saved row as JSON. It contains:
| Field | Value |
| --- | --- |
| `id` | New UUID; copy this for inspection |
| `entity_type` | `user` |
| `target` | `demo:alice` |
| `status` | `inventoried` |
| `requested_by` | `operator-a` |
| `reason` | `null`, unless supplied with `--reason` |
| `created_at` | Submission time |
| `updated_at` | Same time; no execution has started |
| `inventory_manifest` | Ordered communities and relationships |
| `inventory_digest` | SHA-256 identifying the saved inventory |
List recent requests, then inspect the UUID returned above:
```bash
./target/debug/buzz-admin user-deletions list --limit 10
REQUEST_ID=<returned-uuid>
./target/debug/buzz-admin user-deletions inspect "$REQUEST_ID"
```
To see the PostgreSQL row directly:
```bash
docker exec buzz-user-deletion-intake psql -X -U buzz -d buzz -c \
"SELECT id, entity_type, target, status, requested_by, created_at
FROM entity_deletion_requests ORDER BY created_at DESC;"
```
Exactly one row is saved in `entity_deletion_requests`. Repeating the same
target, operator, and reason returns that row without changing its timestamps.
Reusing the target with a different operator or reason fails and preserves the
original row. Use a new `demo:` label for a new request.
## Approve without starting execution
Inspect the inventory before approving. Copy its `inventory_digest` into the
command so approval is bound to what you inspected:
```bash
INVENTORY_DIGEST=<digest-from-inspect>
./target/debug/buzz-admin user-deletions approve "$REQUEST_ID" \
--approved-by operator-b --inventory-digest "$INVENTORY_DIGEST"
./target/debug/buzz-admin user-deletions inspect "$REQUEST_ID"
```
The parent now has `status: approved`. Approval evidence lives in the separate
`entity_deletion_approvals` table. Each row has `id`, `request_id`,
`inventory_digest`, `approved_by`, and `approved_at`. Inspection returns these
rows in an `approvals` array (empty before approval), ordered by time and ID.
The parent keeps the inventory; approval rows bind each operator's decision to
its digest without overwriting earlier decisions.
`stage_history` is empty after approval. The first `run` creates the initial
stage. Inspection reads parent, approvals, and history from one consistent
snapshot. No worker is started by approval.
```bash
docker exec buzz-user-deletion-intake psql -X -U buzz -d buzz -c \
"SELECT id, status, inventory_digest
FROM entity_deletion_requests ORDER BY created_at DESC;"
docker exec buzz-user-deletion-intake psql -X -U buzz -d buzz -c \
"SELECT id, request_id, approved_by, approved_at, inventory_digest
FROM entity_deletion_approvals ORDER BY approved_at, id;"
docker exec buzz-user-deletion-intake psql -X -U buzz -d buzz -c \
"SELECT request_id, stage, status, is_current, target_community_id,
started_at, finished_at, metadata_json
FROM entity_deletion_stage_history;"
```
The first approval inserts an approval row and changes the parent status in
one transaction. A failure rolls both back.
Repeating the same operator and digest preserves the approval ID and timestamp.
Another operator adds a new approval row, with its own time, without creating
another stage or changing the earlier approval:
```bash
./target/debug/buzz-admin user-deletions approve "$REQUEST_ID" \
--approved-by operator-c --inventory-digest "$INVENTORY_DIGEST"
./target/debug/buzz-admin user-deletions inspect "$REQUEST_ID"
```
There are now two approvals and still no stage rows. The unique
key `(request_id, inventory_digest, approved_by)` makes retries idempotent.
A digest that does not match the saved inventory is rejected. This slice has no
inventory replacement or later approval gates yet; those policies can be added
without replacing these historical rows. A partial unique index prevents two
current stages for one request. Executor retries update the same stage row.
Operator names are self-asserted demo input. This is not authenticated approval
for real deletion. Owned communities and actual deletion handlers belong to
later changes. Continue with [the executor walkthrough](operator-user-deletion-executor.md)
to run the five dummy stages, inspect transitions, and try failure recovery.
## Upgrade and rollout
Apply after the intake schema from PR #8496. Migrations 0069 and 0070 add
inventory and execution fields, widen the lifecycle constraint, and create the
operator-global stage-history and approval tables. Constraint validation uses a
separate transaction so it does not retain the schema-change transaction's
write-blocking lock. Both
migrations set a five-second lock timeout and a thirty-second statement timeout.
There are no new triggers, functions, foreign keys, or data backfills.
Existing `submitted` requests remain unchanged by migration. Resubmit with the
same target, operator, and reason to prepare their dummy inventory; the request
ID and original creation time stay unchanged. The previous CLI can still insert
and read requests against the expanded schema. The serving relay does not
automatically execute these requests; an operator runs the CLI worker explicitly.
Stage history and approvals intentionally survive
community deletion as operator recovery/audit evidence.
Verify on a test environment with the submit, inspect, approve, and SQL commands
above using a new `demo:` target. Rollback has the usual sqlx limitation: an older
binary running auto-migration refuses unknown migrations (`VersionMissing`).
Do not roll back to an older auto-migrating relay without the deployment's
reviewed rollback procedure; the additive schema itself can remain in place.
The unmerged draft migration 0069 changed when approvals moved to their own
table, and execution fields were added to requests. If you tested an earlier
draft, use a fresh disposable local database;
do not edit its sqlx checksums or erase its saved demo requests.
PR #8520 did not change SQL schema. Earlier draft requests whose
stage metadata predates saved inputs/results/predecessor IDs are not a supported
resume format. Keep their records intact and submit a new `demo:` label to test
this version. Approval no longer creates a gate row; it is initialized by the
first `get_next_stage` call under worker ownership.