SECURITY: Prevent one User from exhausting Instance storage #333

Closed
opened 2026-09-28 11:38:26 +00:00 by kayg · 25 comments
Owner

Evidence

InstanceConfig::default sets default_quota to 0. sync_homes treats 0 as unlimited and clears every Home quota. The Files API limits one User to 16 live uploads and rejects an upload larger than 1 TiB by default, but it does not limit the total bytes that one User can write over time.

A User can fill the Data directory. This makes writes fail for all Users and breaks the shared Instance. The current code has only one Instance-wide default quota. It does not apply different storage tiers to different Users.

Required decision

Set the quota policy for managed accounts and self-hosted Instances. Then enforce the effective quota on every Home, including upload staging, copies, versions, and restores. Keep quota use based on logical bytes per User, as DESIGN §5 requires. Add a two-User regression that proves one User cannot consume another User's reserved quota or exhaust the complete Instance allocation.

## Evidence `InstanceConfig::default` sets `default_quota` to `0`. `sync_homes` treats `0` as unlimited and clears every Home quota. The Files API limits one User to 16 live uploads and rejects an upload larger than 1 TiB by default, but it does not limit the total bytes that one User can write over time. A User can fill the Data directory. This makes writes fail for all Users and breaks the shared Instance. The current code has only one Instance-wide default quota. It does not apply different storage tiers to different Users. ## Required decision Set the quota policy for managed accounts and self-hosted Instances. Then enforce the effective quota on every Home, including upload staging, copies, versions, and restores. Keep quota use based on logical bytes per User, as DESIGN §5 requires. Add a two-User regression that proves one User cannot consume another User's reserved quota or exhaust the complete Instance allocation.
Author
Owner

Owner decision (2026-09-28): default quota for new calternal.cloud accounts: 5 GB. Orchestrator defaults for the rest (confirm later with the plans research): the instance owner/admin is unlimited; admins can set a per-user quota on invites and user settings; a server-wide reserve (e.g. keep ≥5% or ≥10 GB of the data disk free, whichever is larger) blocks new writes before the disk fills; quota counts logical bytes (DESIGN §5), including versions, trash and staged uploads; clear UI (usage bar in Settings → Account, a toast plus a banner at 90%/100%, deletes still allowed when over quota). Tiers follow from the pricing research issue.

Owner decision (2026-09-28): **default quota for new calternal.cloud accounts: 5 GB.** Orchestrator defaults for the rest (confirm later with the plans research): the instance owner/admin is unlimited; admins can set a per-user quota on invites and user settings; a server-wide reserve (e.g. keep ≥5% or ≥10 GB of the data disk free, whichever is larger) blocks new writes before the disk fills; quota counts logical bytes (DESIGN §5), including versions, trash and staged uploads; clear UI (usage bar in Settings → Account, a toast plus a banner at 90%/100%, deletes still allowed when over quota). Tiers follow from the pricing research issue.
Author
Owner

Starting quota work on branch job/quota, based on dev at eb4ff20a98.

Starting quota work on branch job/quota, based on dev at eb4ff20a9862a627f6d0aea1ca617ca9a9491bce.
Author
Owner

Finding confirmed from the pre-change code: InstanceConfig::default() set default_quota to 0 and sync_homes cleared every Home quota, so normal Users had no effective per-User limit. The upload route also used one global 1 GiB reserve and summed every User's active uploads into one disk check. Quota enforcement and upload reservations are now in the shared filesystem layer; the Files plugin regression covers two separate Users. The owner-selected defaults and reserve policy are recorded in docs/DESIGN.md.

Finding confirmed from the pre-change code: `InstanceConfig::default()` set `default_quota` to 0 and `sync_homes` cleared every Home quota, so normal Users had no effective per-User limit. The upload route also used one global 1 GiB reserve and summed every User's active uploads into one disk check. Quota enforcement and upload reservations are now in the shared filesystem layer; the Files plugin regression covers two separate Users. The owner-selected defaults and reserve policy are recorded in `docs/DESIGN.md`.
Author
Owner

Quota-path audit found an additional failure mode: Notes and the DAV adapter map unknown filesystem write errors to HTTP 500. A rejected quota or server-reserve write would therefore look like an application crash to DAV and Notes clients. The shared per-User settings writer had the same issue for small settings writes. These paths now preserve an insufficient-storage result (507), with focused regressions for collaboration saves, DAV mapping and the shared settings writer.

Quota-path audit found an additional failure mode: Notes and the DAV adapter map unknown filesystem write errors to HTTP 500. A rejected quota or server-reserve write would therefore look like an application crash to DAV and Notes clients. The shared per-User settings writer had the same issue for small settings writes. These paths now preserve an insufficient-storage result (507), with focused regressions for collaboration saves, DAV mapping and the shared settings writer.
Author
Owner

Clippy finding and fix: the new reserved-write API exceeded clippy’s argument limit in write_checked_reserved and write_inner. Grouped the expected digest, source modification time, and upload reservation into WriteConditions, updated upload finalization and the two-Home regression, and verified the focused test plus cargo clippy -p calternal-fs --all-targets -- -D warnings. The full workspace clippy pass is running now.

Clippy finding and fix: the new reserved-write API exceeded clippy’s argument limit in `write_checked_reserved` and `write_inner`. Grouped the expected digest, source modification time, and upload reservation into `WriteConditions`, updated upload finalization and the two-Home regression, and verified the focused test plus `cargo clippy -p calternal-fs --all-targets -- -D warnings`. The full workspace clippy pass is running now.
Author
Owner

Resuming quota work from job/quota at b86a1f78 (origin/job/quota). The branch had merged the then-current dev at 3b300558; local dev is now fba83527, four commits ahead, and I will merge it once before final gates.

Resuming quota work from job/quota at b86a1f78 (origin/job/quota). The branch had merged the then-current dev at 3b300558; local dev is now fba83527, four commits ahead, and I will merge it once before final gates.
Author
Owner

UI review found that Account Storage showed the 90%/100% banner but did not also raise the toast required by the owner decision. I am adding the toast through the existing app toast store and will refresh the production screenshots.

UI review found that Account Storage showed the 90%/100% banner but did not also raise the toast required by the owner decision. I am adding the toast through the existing app toast store and will refresh the production screenshots.
Author
Owner

The one full-workspace cargo test run stopped in calternal-collab on its 10,000-block timing assertion. Exact measured output:

10,000-block collaboration phases: parse=1.554970023s, Yrs=756.24988ms, block-index=22.77611ms, first-sync=67.902451ms (616204 bytes), snapshot=76.6316ms (616199 bytes), total=2.478530064s
10,000-block open, first sync and snapshot took 2.478530064s
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.70s

The test requires under 2s. The auth crate's 54 tests and CLI's 10 tests passed, as did the other 14 collaboration tests. This is a SLOW-only result on the shared build host; I did not change the existing timing expectation or rerun the test. Cargo stopped at calternal-collab, so later workspace test binaries did not run.

The one full-workspace `cargo test` run stopped in `calternal-collab` on its 10,000-block timing assertion. Exact measured output: ``` 10,000-block collaboration phases: parse=1.554970023s, Yrs=756.24988ms, block-index=22.77611ms, first-sync=67.902451ms (616204 bytes), snapshot=76.6316ms (616199 bytes), total=2.478530064s 10,000-block open, first sync and snapshot took 2.478530064s test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.70s ``` The test requires under 2s. The auth crate's 54 tests and CLI's 10 tests passed, as did the other 14 collaboration tests. This is a SLOW-only result on the shared build host; I did not change the existing timing expectation or rerun the test. Cargo stopped at `calternal-collab`, so later workspace test binaries did not run.
Author
Owner

Completed

Implemented Forgejo #333 on job/quota at 89c1d0d59746651e52da0a1e072aa1c337cef888. The branch is pushed. The one merge of current dev is 3e2adb83.

New non-admin Users inherit the 5 GB default. Owner/Admin roles are unlimited. Invites and admin User settings support per-User limits. Quota enforcement covers staged/resumable uploads, versions, copies, restores, DAV writes, and collaboration saves. The filesystem reserve uses the larger of 10 GB or 5% of filesystem capacity. The Settings UI shows usage and 90%/100% warnings. OpenAPI and the generated API client are updated. Regression and adversarial coverage includes two-User isolation and the write paths above.

Changed files (36):

  • UI: apps/web/src/routes/settings/account/AccountSection.svelte, apps/web/src/routes/settings/account/StorageGroup.svelte, apps/web/src/routes/settings/admin/AdminSection.svelte, apps/web/src/routes/settings/admin/InvitationsGroup.svelte, apps/web/src/routes/settings/admin/QuotaLimitField.svelte, apps/web/src/routes/settings/admin/UsersGroup.svelte, apps/web/src/routes/settings/sections.ts.
  • API: contracts/openapi.json, packages/api-client/src/generated.ts.
  • Auth: crates/calternal-auth/migrations/0008_user_quotas.sql, crates/calternal-auth/src/api.rs, crates/calternal-auth/src/store.rs.
  • Filesystem and storage: crates/calternal-fs/src/error.rs, file_ops.rs, lib.rs, quota.rs, root.rs, uploads.rs, write.rs, crates/calternal-fs/tests/storage.rs.
  • Write integrations: crates/calternal-dav/src/protocol.rs, crates/calternal-plugin/src/user_settings.rs, crates/calternal-server/src/appearance.rs, crates/calternal-server/src/wire.rs, crates/plugins/calendar/src/items.rs, crates/plugins/calendar/src/routes.rs, crates/plugins/files/src/lib.rs, crates/plugins/files/src/public.rs, crates/plugins/files/src/uploads.rs, crates/plugins/notes/src/lib.rs, crates/plugins/notes/src/tasks_dav.rs, crates/plugins/notifications/src/routes.rs, crates/plugins/photos/src/routes.rs.
  • Design and probes: docs/DESIGN.md, tests/adversarial/attack2.py, tests/adversarial/setup.mjs.

Evidence

The local quota adversarial round completed against the real server:

fixture passkey login requests: 30/30
---------- quota ----------
owner installation session assertion refreshed
server alive at end: True

==== ROUND 2 FINDINGS 0

==== ROUND 2 SLOW 0

The production-build Settings screenshots cover 390/820/1440 px in light and dark for account storage, admin Users, and invitations, plus both warning thresholds. The 30-image archive is attached: quota-settings-review.zip.

Gates

Web check passed:

$ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/quota/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Web tests passed:

 Test Files  113 passed (113)
      Tests  745 passed (745)
   Start at  21:19:39
   Duration  124.66s (transform 58%, environment 17%, import 14%, tests 7%, setup 4%)

cargo fmt --check exited 0 with no output.

Clippy passed; exact final output:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 81m 29s

The single full cargo test invocation stopped at a SLOW-only timing assertion in calternal-collab. Exact output:

10,000-block collaboration phases: parse=1.554970023s, Yrs=756.24988ms, block-index=22.77611ms, first-sync=67.902451ms (616204 bytes), snapshot=76.6316ms (616199 bytes), total=2.478530064s
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.70s
error: test failed, to rerun pass `-p calternal-collab --lib`

The test requires under 2 seconds. Auth (54 tests) and CLI (10 tests) passed. The other 14 collaboration tests passed. Later workspace test binaries did not run. I kept the existing timing expectation and did not rerun it. The measured output and shared-host load are recorded in the prior issue comment.

Decisions and known gaps

The existing AppToaster rule tucks alerts while the Settings overlay is open. The banner stays visible in Settings; its alert toast is queued and resumes when the overlay closes. This preserves the shared chrome behavior. No other open DESIGN decision was needed. The adversarial build and Cargo gates used system OpenSSL 3.5.7 via OPENSSL_NO_VENDOR=1 to avoid rebuilding vendored OpenSSL; dependency manifests were not changed.

cargo clean removed 16,821 files (11.9 GiB), and apps/web/.svelte-kit and apps/web/build were deleted. No screenshots or review artifacts were committed.

## Completed Implemented Forgejo #333 on `job/quota` at `89c1d0d59746651e52da0a1e072aa1c337cef888`. The branch is pushed. The one merge of current `dev` is `3e2adb83`. New non-admin Users inherit the 5 GB default. Owner/Admin roles are unlimited. Invites and admin User settings support per-User limits. Quota enforcement covers staged/resumable uploads, versions, copies, restores, DAV writes, and collaboration saves. The filesystem reserve uses the larger of 10 GB or 5% of filesystem capacity. The Settings UI shows usage and 90%/100% warnings. OpenAPI and the generated API client are updated. Regression and adversarial coverage includes two-User isolation and the write paths above. Changed files (36): - UI: `apps/web/src/routes/settings/account/AccountSection.svelte`, `apps/web/src/routes/settings/account/StorageGroup.svelte`, `apps/web/src/routes/settings/admin/AdminSection.svelte`, `apps/web/src/routes/settings/admin/InvitationsGroup.svelte`, `apps/web/src/routes/settings/admin/QuotaLimitField.svelte`, `apps/web/src/routes/settings/admin/UsersGroup.svelte`, `apps/web/src/routes/settings/sections.ts`. - API: `contracts/openapi.json`, `packages/api-client/src/generated.ts`. - Auth: `crates/calternal-auth/migrations/0008_user_quotas.sql`, `crates/calternal-auth/src/api.rs`, `crates/calternal-auth/src/store.rs`. - Filesystem and storage: `crates/calternal-fs/src/error.rs`, `file_ops.rs`, `lib.rs`, `quota.rs`, `root.rs`, `uploads.rs`, `write.rs`, `crates/calternal-fs/tests/storage.rs`. - Write integrations: `crates/calternal-dav/src/protocol.rs`, `crates/calternal-plugin/src/user_settings.rs`, `crates/calternal-server/src/appearance.rs`, `crates/calternal-server/src/wire.rs`, `crates/plugins/calendar/src/items.rs`, `crates/plugins/calendar/src/routes.rs`, `crates/plugins/files/src/lib.rs`, `crates/plugins/files/src/public.rs`, `crates/plugins/files/src/uploads.rs`, `crates/plugins/notes/src/lib.rs`, `crates/plugins/notes/src/tasks_dav.rs`, `crates/plugins/notifications/src/routes.rs`, `crates/plugins/photos/src/routes.rs`. - Design and probes: `docs/DESIGN.md`, `tests/adversarial/attack2.py`, `tests/adversarial/setup.mjs`. ## Evidence The local quota adversarial round completed against the real server: ``` fixture passkey login requests: 30/30 ---------- quota ---------- owner installation session assertion refreshed server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 0 ``` The production-build Settings screenshots cover 390/820/1440 px in light and dark for account storage, admin Users, and invitations, plus both warning thresholds. The 30-image archive is attached: [quota-settings-review.zip](https://git.kayg.org/attachments/cded9ecd-4eca-4d9e-a98d-d6d07428c96b). ## Gates Web check passed: ``` $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/quota/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Web tests passed: ``` Test Files 113 passed (113) Tests 745 passed (745) Start at 21:19:39 Duration 124.66s (transform 58%, environment 17%, import 14%, tests 7%, setup 4%) ``` `cargo fmt --check` exited 0 with no output. Clippy passed; exact final output: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 81m 29s ``` The single full `cargo test` invocation stopped at a SLOW-only timing assertion in `calternal-collab`. Exact output: ``` 10,000-block collaboration phases: parse=1.554970023s, Yrs=756.24988ms, block-index=22.77611ms, first-sync=67.902451ms (616204 bytes), snapshot=76.6316ms (616199 bytes), total=2.478530064s test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.70s error: test failed, to rerun pass `-p calternal-collab --lib` ``` The test requires under 2 seconds. Auth (54 tests) and CLI (10 tests) passed. The other 14 collaboration tests passed. Later workspace test binaries did not run. I kept the existing timing expectation and did not rerun it. The measured output and shared-host load are recorded in the prior issue comment. ## Decisions and known gaps The existing AppToaster rule tucks alerts while the Settings overlay is open. The banner stays visible in Settings; its alert toast is queued and resumes when the overlay closes. This preserves the shared chrome behavior. No other open DESIGN decision was needed. The adversarial build and Cargo gates used system OpenSSL 3.5.7 via `OPENSSL_NO_VENDOR=1` to avoid rebuilding vendored OpenSSL; dependency manifests were not changed. `cargo clean` removed 16,821 files (11.9 GiB), and `apps/web/.svelte-kit` and `apps/web/build` were deleted. No screenshots or review artifacts were committed.
Author
Owner

Review of job/quota 89c1d0d5 (Claude): one round before merge

  1. Admin → Users at 390 is broken. The second user's row collapses the name column to one character per line ("M / a / l / l / o / r / y"), because the new quota field takes the row width. On narrow widths the row stacks: the identity (avatar, name, role, joined) on top, then the quota field below it at full width. Use the shared settings row that already stacks at the narrow breakpoint (the Mail account row in #313 does this); no new layout. Check 820 as well.
  2. "Users" twice: the page title "Users" is followed by a section header "Users". Drop the duplicate section header when a page has a single section (same rule as #369, "Info" twice).
  3. The 90% warning uses the error colour. 90% is a warning (warning role token); only 100% (full) uses the error/danger token. The meter fill follows the same roles.
  4. Gates: your full cargo test stopped at the SLOW collab timing test, so the calternal-fs, Files, Notes, DAV and plugin tests after it never ran, and this change touches all of them. Run them explicitly (cargo test -p calternal-fs -p calternal-plugin-files -p calternal-plugin-notes -p calternal-dav -p calternal-plugin-calendar -p calternal-plugin-photos -p calternal-plugin-notifications) and quote the results.

Re-shoot the Admin Users and Storage screenshots (390/820/1440, light and dark) and attach them as PNGs (not a zip). Commit and push job/quota; no merge into dev.

## Review of job/quota 89c1d0d5 (Claude): one round before merge 1. **Admin → Users at 390 is broken.** The second user's row collapses the name column to one character per line ("M / a / l / l / o / r / y"), because the new quota field takes the row width. On narrow widths the row stacks: the identity (avatar, name, role, joined) on top, then the quota field below it at full width. Use the shared settings row that already stacks at the narrow breakpoint (the Mail account row in #313 does this); no new layout. Check 820 as well. 2. **"Users" twice:** the page title "Users" is followed by a section header "Users". Drop the duplicate section header when a page has a single section (same rule as #369, "Info" twice). 3. **The 90% warning uses the error colour.** 90% is a warning (warning role token); only 100% (full) uses the error/danger token. The meter fill follows the same roles. 4. **Gates:** your full `cargo test` stopped at the SLOW collab timing test, so the calternal-fs, Files, Notes, DAV and plugin tests after it never ran, and this change touches all of them. Run them explicitly (`cargo test -p calternal-fs -p calternal-plugin-files -p calternal-plugin-notes -p calternal-dav -p calternal-plugin-calendar -p calternal-plugin-photos -p calternal-plugin-notifications`) and quote the results. Re-shoot the Admin Users and Storage screenshots (390/820/1440, light and dark) and attach them as PNGs (not a zip). Commit and push job/quota; no merge into dev.
Author
Owner

Resuming the Claude review corrections on branch job/quota at 89c1d0d597. Current local dev is 95520d547d; branch merge-base is fba83527f2. I will merge dev once before final gates as requested.

Resuming the Claude review corrections on branch job/quota at 89c1d0d59746651e52da0a1e072aa1c337cef888. Current local dev is 95520d547dedb2d5286dd2dc6f61d1547621ab7c; branch merge-base is fba83527f2cccf2334934bb1fd0932be7c0e209b. I will merge dev once before final gates as requested.
Author
Owner

Reproduced the review findings in the existing production captures: at 390 px, Mallory wraps one character per line below the duplicate Users heading; the Account Storage 90% warning and meter use the error-like red token. I am fixing these in the shared AccountRow/SettingsGroup components and the shared warning/danger theme tokens.

Reproduced the review findings in the existing production captures: at 390 px, Mallory wraps one character per line below the duplicate Users heading; the Account Storage 90% warning and meter use the error-like red token. I am fixing these in the shared AccountRow/SettingsGroup components and the shared warning/danger theme tokens.
Author
Owner

UI decision for this review: on one-group Admin pages where the group title repeats the page title, hide the duplicate group heading and keep the group region named; the page heading already has the same Copy link. In the shared AccountRow, keep existing inline extras in the text column by default and use the full-width variant only for the quota control. This preserves existing inline editors while giving the quota field room at phone and tablet widths.

UI decision for this review: on one-group Admin pages where the group title repeats the page title, hide the duplicate group heading and keep the group region named; the page heading already has the same Copy link. In the shared AccountRow, keep existing inline extras in the text column by default and use the full-width variant only for the quota control. This preserves existing inline editors while giving the quota field room at phone and tablet widths.
Author
Owner

Migration collision (Claude): #328 merged into dev with crates/calternal-auth/migrations/0008_app_password_scopes.sql. Rename this branch's 0008_user_quotas.sql to 0009_user_quotas.sql when you merge dev (an sqlx version collision would fail at startup), and resolve the calternal-auth api.rs/store.rs conflicts keeping both scopes and quotas.

Migration collision (Claude): #328 merged into dev with crates/calternal-auth/migrations/0008_app_password_scopes.sql. Rename this branch's 0008_user_quotas.sql to 0009_user_quotas.sql when you merge dev (an sqlx version collision would fail at startup), and resolve the calternal-auth api.rs/store.rs conflicts keeping both scopes and quotas.
Author
Owner

Continuing #333 on job/quota from HEAD 49d390cbc4852cdaf33e58aea8400a2db3c77b55. The original job branch already contains a merge of dev at c72c88a3; the current dev is 5cb977188697c7f1b5dca6d2dd70dc05a6817626, which I will merge once before final gates. I am addressing all four Claude review points, preserving both the #328 app-password scopes and quota changes while renumbering quotas to migration 0009, adding an empty-database auth migration-order test, and preparing individual 390/820/1440 light/dark Admin Users and Storage PNGs for attachment.

Continuing #333 on `job/quota` from HEAD `49d390cbc4852cdaf33e58aea8400a2db3c77b55`. The original job branch already contains a merge of `dev` at `c72c88a3`; the current `dev` is `5cb977188697c7f1b5dca6d2dd70dc05a6817626`, which I will merge once before final gates. I am addressing all four Claude review points, preserving both the #328 app-password scopes and quota changes while renumbering quotas to migration 0009, adding an empty-database auth migration-order test, and preparing individual 390/820/1440 light/dark Admin Users and Storage PNGs for attachment.
Author
Owner

The new empty-database auth migration test reproduced the #328 collision before the rename: sqlx::migrate!().run() failed at migration application with SQLite error 1555, UNIQUE constraint failed: _sqlx_migrations.version, because both app-password scopes and per-User quotas were version 8. I am renumbering quotas to 0009 and will verify the full sequence and both schema additions on a fresh database.

The new empty-database auth migration test reproduced the #328 collision before the rename: `sqlx::migrate!().run()` failed at migration application with SQLite error 1555, `UNIQUE constraint failed: _sqlx_migrations.version`, because both app-password scopes and per-User quotas were version 8. I am renumbering quotas to 0009 and will verify the full sequence and both schema additions on a fresh database.
Author
Owner

After merging current dev, the server migration-test build found an error-mapping gap: calternal-plugin::UserSettingsError now includes InsufficientStorage, but calternal-server/src/wire.rs::dav_settings_error does not match it. The compiler reports E0004 at wire.rs:728; quota/server-reserve failures passed through this DAV settings adapter would not compile. I am adding a regression for HTTP 507 before updating the mapping.

After merging current `dev`, the server migration-test build found an error-mapping gap: `calternal-plugin::UserSettingsError` now includes `InsufficientStorage`, but `calternal-server/src/wire.rs::dav_settings_error` does not match it. The compiler reports E0004 at `wire.rs:728`; quota/server-reserve failures passed through this DAV settings adapter would not compile. I am adding a regression for HTTP 507 before updating the mapping.
Author
Owner

Updated production screenshots for the Claude review. These are individual PNG attachments, captured at 390, 820 and 1440 px in light and dark themes from the real local server.

Admin Users

Admin Users, 1440 px, dark theme
Admin Users, 1440 px, light theme
Admin Users, 390 px, dark theme
Admin Users, 390 px, light theme
Admin Users, 820 px, dark theme
Admin Users, 820 px, light theme

Account Storage

Account Storage, 1440 px, dark theme
Account Storage, 1440 px, light theme
Account Storage, 390 px, dark theme
Account Storage, 390 px, light theme
Account Storage, 820 px, dark theme
Account Storage, 820 px, light theme

Updated production screenshots for the Claude review. These are individual PNG attachments, captured at 390, 820 and 1440 px in light and dark themes from the real local server. ### Admin Users ![Admin Users, 1440 px, dark theme](https://git.kayg.org/attachments/a6db1f5a-3762-4e77-aa69-48a918633cef) ![Admin Users, 1440 px, light theme](https://git.kayg.org/attachments/85c559f4-f1a4-49fb-8d62-636daf3d8a86) ![Admin Users, 390 px, dark theme](https://git.kayg.org/attachments/90ce025d-9476-404e-bba2-90563b3a2689) ![Admin Users, 390 px, light theme](https://git.kayg.org/attachments/16673850-3e0f-4d18-9127-4179a2fd6685) ![Admin Users, 820 px, dark theme](https://git.kayg.org/attachments/0382af76-bdff-4d12-9a88-133f72a71554) ![Admin Users, 820 px, light theme](https://git.kayg.org/attachments/2e3ddfbb-62b8-4c13-816d-1f0bed60bb05) ### Account Storage ![Account Storage, 1440 px, dark theme](https://git.kayg.org/attachments/de2a2b93-9981-424c-91b9-931ad73f1bce) ![Account Storage, 1440 px, light theme](https://git.kayg.org/attachments/66e7e93b-1e34-428d-a945-cd93daccf25d) ![Account Storage, 390 px, dark theme](https://git.kayg.org/attachments/935054d5-ae0f-4f91-ab29-948b9fb78b41) ![Account Storage, 390 px, light theme](https://git.kayg.org/attachments/3f80a545-ed6b-4e14-99a8-5ceb09e92c47) ![Account Storage, 820 px, dark theme](https://git.kayg.org/attachments/d7b736f3-2861-43de-9016-a0076509f04f) ![Account Storage, 820 px, light theme](https://git.kayg.org/attachments/0ae29c67-fc4d-4693-8659-fbb7f81ea948)
Author
Owner

Completed

Implemented Forgejo #333 on branch job/quota. The branch is pushed and clean at 8f5af8239f5c70fd499ef7364e2f167fefe6e880. I merged the then-current dev (5cb977188697c7f1b5dca6d2dd70dc05a6817626) into this branch once, in merge commit b7472fed; I did not merge the job branch into dev.

The change enforces per-User logical-byte quotas across Home files, Versions, Trash, staged uploads, copies, restores, DAV writes and collaboration saves. It adds the 5 GB decimal default for non-admin Users, unlimited Owner/Admin roles, per-User overrides, and the 10 GB or 5% filesystem reserve from DESIGN §§5 and 26. Admin can set a quota on Users and invite forms; Account Storage reports usage, reservations and 90%/100% thresholds.

All four Claude review points are addressed: User rows stack at narrow widths, the duplicate Users heading is removed while the region remains named, 90% uses warning color while 100% uses error color, and only the quota control uses the full-width AccountRow variant. The quota migration is now 0009_user_quotas.sql, after #328's 0008_app_password_scopes.sql. A new auth test applies migrations 1–9 to an empty database and checks both schemas. DAV quota errors map to Insufficient Storage (507), with a regression test.

The 12 production-build PNGs are attached individually in the preceding #333 comment and remain on disk at artifacts/quota-333-2026-09-29/: Admin Users and Account Storage at 390, 820 and 1440 px in light and dark themes. No screenshots are committed.

Changed files (42)

  • Settings UI: apps/web/src/routes/settings/account/AccountSection.svelte, apps/web/src/routes/settings/account/StorageGroup.svelte, apps/web/src/routes/settings/admin/AdminSection.svelte, apps/web/src/routes/settings/admin/InvitationsGroup.svelte, apps/web/src/routes/settings/admin/QuotaLimitField.svelte, apps/web/src/routes/settings/admin/SignInGroup.svelte, apps/web/src/routes/settings/admin/SystemGroup.svelte, apps/web/src/routes/settings/admin/UsersGroup.svelte, apps/web/src/routes/settings/parts/AccountRow.svelte, apps/web/src/routes/settings/parts/AccountRow.svelte.test.ts, apps/web/src/routes/settings/parts/SettingsGroup.svelte, apps/web/src/routes/settings/parts/SettingsGroup.svelte.test.ts, apps/web/src/routes/settings/sections.ts.
  • API and design: contracts/openapi.json, packages/api-client/src/generated.ts, docs/DESIGN.md.
  • Auth: crates/calternal-auth/migrations/0009_user_quotas.sql, crates/calternal-auth/src/api.rs, crates/calternal-auth/src/store.rs.
  • Filesystem: crates/calternal-fs/src/error.rs, crates/calternal-fs/src/file_ops.rs, crates/calternal-fs/src/lib.rs, crates/calternal-fs/src/quota.rs, crates/calternal-fs/src/root.rs, crates/calternal-fs/src/uploads.rs, crates/calternal-fs/src/write.rs, crates/calternal-fs/tests/storage.rs.
  • Write integrations: crates/calternal-dav/src/protocol.rs, crates/calternal-plugin/src/user_settings.rs, crates/calternal-server/src/appearance.rs, crates/calternal-server/src/wire.rs, crates/plugins/calendar/src/items.rs, crates/plugins/calendar/src/routes.rs, crates/plugins/files/src/lib.rs, crates/plugins/files/src/public.rs, crates/plugins/files/src/uploads.rs, crates/plugins/notes/src/lib.rs, crates/plugins/notes/src/tasks_dav.rs, crates/plugins/notifications/src/routes.rs, crates/plugins/photos/src/routes.rs.
  • Adversarial coverage: tests/adversarial/attack2.py, tests/adversarial/setup.mjs.

Gates

The explicit review-list crate tests passed. Exact test summaries:

calternal-auth: test result: ok. 57 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 41.36s
calternal-fs: test result: ok. 37 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.60s
test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.83s
calternal-plugin-files: test result: ok. 125 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 202.49s
calternal-plugin-notes: test result: ok. 109 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 72.15s
calternal-dav: test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
calternal-plugin-calendar: test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s
calternal-plugin-photos: test result: ok. 44 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 11.04s
calternal-plugin-notifications: test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.80s

The focused server checks passed: wire::dav_settings_error_tests::quota_write_failures_map_to_dav_insufficient_storage and live_apps_run_in_separate_processes. The empty-database auth migration test also passed in the full run:

test store::tests::all_auth_migrations_run_in_order_on_an_empty_database ... ok
test result: ok. 57 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 41.36s

Formatting passed:

cargo fmt --check
exit_code=0

Clippy passed:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 18m 19s

The single full-workspace cargo test invocation stopped at a collaboration persistence regression. The live-client assertion passed, but the final persisted note lacked both client edits. I left the existing expectation unchanged and filed the evidence as #382. Exact gate output:

test concurrent_clients_and_external_writer_converge ... FAILED
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.63s
error: test failed, to rerun pass `-p calternal-collab --test two_clients`

Web check passed:

$ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/quota/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Web tests passed:

 Test Files  116 passed (116)
      Tests  760 passed (760)
   Start at  03:50:35
   Duration  215.44s (transform 52%, environment 20%, import 13%, tests 7%, setup 7%)

Cleanup completed:

Removed 21556 files, 20.4GiB total

apps/web/.svelte-kit and apps/web/build are absent. All 12 PNGs remain in artifacts/quota-333-2026-09-29/.

Known gaps

The single time-boxed adversarial invocation ended without the runner's final summary. Nested calendar/mail probes could not locate the server because the helper looked in target/debug while Cargo used the preset job target; the run did not set CALTERNAL_SERVER_BIN. Mixed-load transport timeouts and fixture HTTP -1 results are recorded under #368. Editor consistency observations are tracked under #363 and #364. The 10,000-block timing probe passed in the final workspace run; one earlier run exceeded its 2 s limit under shared-host load, and its expectation was not changed.

Decisions outside DESIGN

  • For one-section Admin screens whose section title repeats the page title, hide that repeated heading while retaining the named region; the page heading already provides the Copy link.
  • Keep shared AccountRow extras inline by default. Use the full-width variant only for quota fields, so existing inline editors retain their layout.
  • Preserve #328's migration at version 8 and place quotas at version 9 to keep all auth migrations unique and ordered.
  • Gates used OPENSSL_NO_VENDOR=1 with the host OpenSSL; dependency manifests and lockfiles did not change.

No open DESIGN item was implemented. cargo clean removed 20.4 GiB. The branch push was already up to date; no merge into dev was made.

## Completed Implemented Forgejo #333 on branch `job/quota`. The branch is pushed and clean at `8f5af8239f5c70fd499ef7364e2f167fefe6e880`. I merged the then-current `dev` (`5cb977188697c7f1b5dca6d2dd70dc05a6817626`) into this branch once, in merge commit `b7472fed`; I did not merge the job branch into `dev`. The change enforces per-User logical-byte quotas across Home files, Versions, Trash, staged uploads, copies, restores, DAV writes and collaboration saves. It adds the 5 GB decimal default for non-admin Users, unlimited Owner/Admin roles, per-User overrides, and the 10 GB or 5% filesystem reserve from DESIGN §§5 and 26. Admin can set a quota on Users and invite forms; Account Storage reports usage, reservations and 90%/100% thresholds. All four Claude review points are addressed: User rows stack at narrow widths, the duplicate Users heading is removed while the region remains named, 90% uses warning color while 100% uses error color, and only the quota control uses the full-width AccountRow variant. The quota migration is now `0009_user_quotas.sql`, after #328's `0008_app_password_scopes.sql`. A new auth test applies migrations 1–9 to an empty database and checks both schemas. DAV quota errors map to Insufficient Storage (507), with a regression test. The 12 production-build PNGs are attached individually in the preceding #333 comment and remain on disk at `artifacts/quota-333-2026-09-29/`: Admin Users and Account Storage at 390, 820 and 1440 px in light and dark themes. No screenshots are committed. ## Changed files (42) - Settings UI: `apps/web/src/routes/settings/account/AccountSection.svelte`, `apps/web/src/routes/settings/account/StorageGroup.svelte`, `apps/web/src/routes/settings/admin/AdminSection.svelte`, `apps/web/src/routes/settings/admin/InvitationsGroup.svelte`, `apps/web/src/routes/settings/admin/QuotaLimitField.svelte`, `apps/web/src/routes/settings/admin/SignInGroup.svelte`, `apps/web/src/routes/settings/admin/SystemGroup.svelte`, `apps/web/src/routes/settings/admin/UsersGroup.svelte`, `apps/web/src/routes/settings/parts/AccountRow.svelte`, `apps/web/src/routes/settings/parts/AccountRow.svelte.test.ts`, `apps/web/src/routes/settings/parts/SettingsGroup.svelte`, `apps/web/src/routes/settings/parts/SettingsGroup.svelte.test.ts`, `apps/web/src/routes/settings/sections.ts`. - API and design: `contracts/openapi.json`, `packages/api-client/src/generated.ts`, `docs/DESIGN.md`. - Auth: `crates/calternal-auth/migrations/0009_user_quotas.sql`, `crates/calternal-auth/src/api.rs`, `crates/calternal-auth/src/store.rs`. - Filesystem: `crates/calternal-fs/src/error.rs`, `crates/calternal-fs/src/file_ops.rs`, `crates/calternal-fs/src/lib.rs`, `crates/calternal-fs/src/quota.rs`, `crates/calternal-fs/src/root.rs`, `crates/calternal-fs/src/uploads.rs`, `crates/calternal-fs/src/write.rs`, `crates/calternal-fs/tests/storage.rs`. - Write integrations: `crates/calternal-dav/src/protocol.rs`, `crates/calternal-plugin/src/user_settings.rs`, `crates/calternal-server/src/appearance.rs`, `crates/calternal-server/src/wire.rs`, `crates/plugins/calendar/src/items.rs`, `crates/plugins/calendar/src/routes.rs`, `crates/plugins/files/src/lib.rs`, `crates/plugins/files/src/public.rs`, `crates/plugins/files/src/uploads.rs`, `crates/plugins/notes/src/lib.rs`, `crates/plugins/notes/src/tasks_dav.rs`, `crates/plugins/notifications/src/routes.rs`, `crates/plugins/photos/src/routes.rs`. - Adversarial coverage: `tests/adversarial/attack2.py`, `tests/adversarial/setup.mjs`. ## Gates The explicit review-list crate tests passed. Exact test summaries: ```text calternal-auth: test result: ok. 57 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 41.36s calternal-fs: test result: ok. 37 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.60s test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.83s calternal-plugin-files: test result: ok. 125 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 202.49s calternal-plugin-notes: test result: ok. 109 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 72.15s calternal-dav: test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s calternal-plugin-calendar: test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.02s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s calternal-plugin-photos: test result: ok. 44 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 11.04s calternal-plugin-notifications: test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.80s ``` The focused server checks passed: `wire::dav_settings_error_tests::quota_write_failures_map_to_dav_insufficient_storage` and `live_apps_run_in_separate_processes`. The empty-database auth migration test also passed in the full run: ```text test store::tests::all_auth_migrations_run_in_order_on_an_empty_database ... ok test result: ok. 57 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 41.36s ``` Formatting passed: ```text cargo fmt --check exit_code=0 ``` Clippy passed: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 18m 19s ``` The single full-workspace `cargo test` invocation stopped at a collaboration persistence regression. The live-client assertion passed, but the final persisted note lacked both client edits. I left the existing expectation unchanged and filed the evidence as [#382](https://git.kayg.org/kayg/calternal/issues/382). Exact gate output: ```text test concurrent_clients_and_external_writer_converge ... FAILED test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.63s error: test failed, to rerun pass `-p calternal-collab --test two_clients` ``` Web check passed: ```text $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/quota/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Web tests passed: ```text Test Files 116 passed (116) Tests 760 passed (760) Start at 03:50:35 Duration 215.44s (transform 52%, environment 20%, import 13%, tests 7%, setup 7%) ``` Cleanup completed: ```text Removed 21556 files, 20.4GiB total ``` `apps/web/.svelte-kit` and `apps/web/build` are absent. All 12 PNGs remain in `artifacts/quota-333-2026-09-29/`. ## Known gaps The single time-boxed adversarial invocation ended without the runner's final summary. Nested calendar/mail probes could not locate the server because the helper looked in `target/debug` while Cargo used the preset job target; the run did not set `CALTERNAL_SERVER_BIN`. Mixed-load transport timeouts and fixture HTTP `-1` results are recorded under #368. Editor consistency observations are tracked under #363 and #364. The 10,000-block timing probe passed in the final workspace run; one earlier run exceeded its 2 s limit under shared-host load, and its expectation was not changed. ## Decisions outside DESIGN - For one-section Admin screens whose section title repeats the page title, hide that repeated heading while retaining the named region; the page heading already provides the Copy link. - Keep shared AccountRow extras inline by default. Use the full-width variant only for quota fields, so existing inline editors retain their layout. - Preserve #328's migration at version 8 and place quotas at version 9 to keep all auth migrations unique and ordered. - Gates used `OPENSSL_NO_VENDOR=1` with the host OpenSSL; dependency manifests and lockfiles did not change. No open DESIGN item was implemented. `cargo clean` removed 20.4 GiB. The branch push was already up to date; no merge into `dev` was made.
Author
Owner

Starting work on job/quota at 8f5af823. Base SHA (merge-base with dev): 5cb9771886. I will merge dev once, resolve the reported conflicts while preserving quota enforcement plus the merged scopes, signing, MCP, jobs, and reminders behavior, then run the requested gates.

Starting work on job/quota at 8f5af823. Base SHA (merge-base with dev): 5cb977188697c7f1b5dca6d2dd70dc05a6817626. I will merge dev once, resolve the reported conflicts while preserving quota enforcement plus the merged scopes, signing, MCP, jobs, and reminders behavior, then run the requested gates.
Author
Owner

Finding during verification: cargo test -p calternal-server --bin calternal-server failed before compiling the server because RustEmbed could not find apps/web/build/; the follow-on Frontend::get errors came from the missing embed input. I am building the production web app, which supplies that required directory, then I will retry the server tests.

Finding during verification: `cargo test -p calternal-server --bin calternal-server` failed before compiling the server because RustEmbed could not find `apps/web/build/`; the follow-on `Frontend::get` errors came from the missing embed input. I am building the production web app, which supplies that required directory, then I will retry the server tests.
Author
Owner

Verification finding: the full workspace cargo test run failed three calternal-collab::hostile_clients assertions at 4–5 second save deadlines under the default parallel test harness. A single diagnostic run of only those three tests with --test-threads=1 passed (3 passed; 0 failed; 8 filtered out; finished in 9.05s). This indicates the failures were deadline contention during the parallel workspace run; I did not change test expectations or collab behavior.

Verification finding: the full workspace `cargo test` run failed three `calternal-collab::hostile_clients` assertions at 4–5 second save deadlines under the default parallel test harness. A single diagnostic run of only those three tests with `--test-threads=1` passed (`3 passed; 0 failed; 8 filtered out; finished in 9.05s`). This indicates the failures were deadline contention during the parallel workspace run; I did not change test expectations or collab behavior.
Author
Owner

Adversarial finding: the config-TOML unreachable provider probe reports missing details.provider because its base_toml fixture explicitly sets default_quota = 0. The new quota policy rejects that value before OIDC discovery, so the expected provider error is never reached. DESIGN §5 requires a 5 GB Instance default; I am updating this fixture to 5,000,000,000 bytes and keeping its status/detail assertions unchanged.

Adversarial finding: the config-TOML `unreachable` provider probe reports missing `details.provider` because its `base_toml` fixture explicitly sets `default_quota = 0`. The new quota policy rejects that value before OIDC discovery, so the expected provider error is never reached. DESIGN §5 requires a 5 GB Instance default; I am updating this fixture to 5,000,000,000 bytes and keeping its status/detail assertions unchanged.
Author
Owner

Finished

Merged dev once and resolved its three conflicts while retaining both sides' behavior: calternal-fs exports WriteConditions and SystemSecretFile; server startup retains quota parsing and profile-signing configuration; the adversarial setup retains the quota fixture and Owner credentials. Auth migration 0009_user_quotas.sql is the only 0009 migration. Merge commit: cabd604e. Follow-up fixture commit: bb504994. Head: bb504994e42d37a0a30f40f423ce9d13c4a3b83a.

The quota work sets the 5 GB Instance default, stores per-User quota overrides, enforces logical-byte limits across managed filesystem writes, uploads and restore paths, exposes quota settings in Account and Admin screens, and maps quota exhaustion to the relevant API/DAV errors.

Primary files include crates/calternal-fs/src/quota.rs, crates/calternal-fs/src/write.rs, crates/calternal-fs/src/uploads.rs, crates/calternal-auth/migrations/0009_user_quotas.sql, crates/calternal-auth/src/store.rs, crates/calternal-auth/src/api.rs, crates/calternal-server/src/wire.rs, apps/web/src/routes/settings/account/StorageGroup.svelte, apps/web/src/routes/settings/admin/QuotaLimitField.svelte, docs/DESIGN.md, and quota tests. Merge resolution also touched crates/calternal-fs/src/lib.rs and tests/adversarial/setup.mjs. The follow-up changed tests/adversarial/attack.py to use the valid 5,000,000,000-byte default in the config probe.

Gate output

cargo fmt --check produced no output and exited 0.

cargo test -p calternal-fs -p calternal-auth --lib:

calternal-auth: test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
calternal-fs: test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

cargo test -p calternal-server --bin calternal-server:

test result: ok. 79 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out

cargo clippy --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 14m 58s

bun run check:

svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  120 passed (120)
      Tests  777 passed (777)
   Start at  05:58:56
   Duration  183.39s (transform 52%, environment 21%, import 13%, tests 10%, setup 4%)

The one full-workspace cargo test gate failed in three deadline-based calternal-collab::hostile_clients cases under the parallel harness:

failures:
    wiki_embeds_open_live_and_save_unchanged
    continuous_typing_is_saved_within_the_maximum_wait
    unrepresentable_update_is_rejected_and_room_keeps_saving

test result: FAILED. 8 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.01s

error: test failed, to rerun pass `-p calternal-collab --test hostile_clients`

A single diagnostic run of those three tests with --test-threads=1 passed:

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 9.05s

I did not change those expectations or rerun the full workspace gate.

Adversarial run and gaps

The single time-boxed local API adversarial run reported 42 findings, including SLOW-only load results. The non-SLOW config probe finding was fixed in bb504994 by using the valid 5 GB default. A Journal CalDAV PUT accepted a VEVENT with a nested VALARM; because that changes behavior outside this quota job, I filed it as #392. The DAV discovery assertion checks a principal-only property on a calendar collection, and the oversized-body 502 was generated by the local Node proxy after an upstream socket error. I filed those probe corrections as #394. The runner was not repeated. No other non-SLOW server-side finding from this run remains unfiled.

cargo clean output:

Removed 18837 files, 16.7GiB total

Removed apps/web/build and apps/web/.svelte-kit. The worktree is clean. The branch push was already up to date at this head; no merge into dev was made.

Decisions not covered by DESIGN

  • Kept 0009_user_quotas.sql after the dev merge because no other auth 0009 exists.
  • Kept zero invalid as an Instance default and reserved unlimited quota for Owner/Admin and an explicit per-User override, following the quota policy recorded in DESIGN §5 and #333.
  • Left the parallel collab deadline expectations unchanged; the sequential diagnostic passed.
## Finished Merged `dev` once and resolved its three conflicts while retaining both sides' behavior: `calternal-fs` exports `WriteConditions` and `SystemSecretFile`; server startup retains quota parsing and profile-signing configuration; the adversarial setup retains the quota fixture and Owner credentials. Auth migration `0009_user_quotas.sql` is the only `0009` migration. Merge commit: `cabd604e`. Follow-up fixture commit: `bb504994`. Head: `bb504994e42d37a0a30f40f423ce9d13c4a3b83a`. The quota work sets the 5 GB Instance default, stores per-User quota overrides, enforces logical-byte limits across managed filesystem writes, uploads and restore paths, exposes quota settings in Account and Admin screens, and maps quota exhaustion to the relevant API/DAV errors. Primary files include `crates/calternal-fs/src/quota.rs`, `crates/calternal-fs/src/write.rs`, `crates/calternal-fs/src/uploads.rs`, `crates/calternal-auth/migrations/0009_user_quotas.sql`, `crates/calternal-auth/src/store.rs`, `crates/calternal-auth/src/api.rs`, `crates/calternal-server/src/wire.rs`, `apps/web/src/routes/settings/account/StorageGroup.svelte`, `apps/web/src/routes/settings/admin/QuotaLimitField.svelte`, `docs/DESIGN.md`, and quota tests. Merge resolution also touched `crates/calternal-fs/src/lib.rs` and `tests/adversarial/setup.mjs`. The follow-up changed `tests/adversarial/attack.py` to use the valid 5,000,000,000-byte default in the config probe. ## Gate output `cargo fmt --check` produced no output and exited 0. `cargo test -p calternal-fs -p calternal-auth --lib`: ```text calternal-auth: test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out calternal-fs: test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` `cargo test -p calternal-server --bin calternal-server`: ```text test result: ok. 79 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out ``` `cargo clippy --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 14m 58s ``` `bun run check`: ```text svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 120 passed (120) Tests 777 passed (777) Start at 05:58:56 Duration 183.39s (transform 52%, environment 21%, import 13%, tests 10%, setup 4%) ``` The one full-workspace `cargo test` gate failed in three deadline-based `calternal-collab::hostile_clients` cases under the parallel harness: ```text failures: wiki_embeds_open_live_and_save_unchanged continuous_typing_is_saved_within_the_maximum_wait unrepresentable_update_is_rejected_and_room_keeps_saving test result: FAILED. 8 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.01s error: test failed, to rerun pass `-p calternal-collab --test hostile_clients` ``` A single diagnostic run of those three tests with `--test-threads=1` passed: ```text test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 9.05s ``` I did not change those expectations or rerun the full workspace gate. ## Adversarial run and gaps The single time-boxed local API adversarial run reported 42 findings, including SLOW-only load results. The non-SLOW config probe finding was fixed in `bb504994` by using the valid 5 GB default. A Journal CalDAV PUT accepted a VEVENT with a nested `VALARM`; because that changes behavior outside this quota job, I filed it as #392. The DAV discovery assertion checks a principal-only property on a calendar collection, and the oversized-body 502 was generated by the local Node proxy after an upstream socket error. I filed those probe corrections as #394. The runner was not repeated. No other non-SLOW server-side finding from this run remains unfiled. `cargo clean` output: ```text Removed 18837 files, 16.7GiB total ``` Removed `apps/web/build` and `apps/web/.svelte-kit`. The worktree is clean. The branch push was already up to date at this head; no merge into `dev` was made. ## Decisions not covered by DESIGN - Kept `0009_user_quotas.sql` after the dev merge because no other auth `0009` exists. - Kept zero invalid as an Instance default and reserved unlimited quota for Owner/Admin and an explicit per-User override, following the quota policy recorded in DESIGN §5 and #333. - Left the parallel collab deadline expectations unchanged; the sequential diagnostic passed.
Author
Owner

Closed by the quota work merged and deployed on 2026-09-29: the app default for a new non-admin User is 1 GB (wire.rs default_quota), zero is not allowed as an Instance default, Owners and Admins are unlimited, and quota reservations cover uploads (write_checked_reserved). Remaining per-User storage isolation items are tracked in #435.

Closed by the quota work merged and deployed on 2026-09-29: the app default for a new non-admin User is 1 GB (wire.rs default_quota), zero is not allowed as an Instance default, Owners and Admins are unlimited, and quota reservations cover uploads (write_checked_reserved). Remaining per-User storage isolation items are tracked in #435.
kayg closed this issue 2026-09-29 14:09:21 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#333
No description provided.