SECURITY: concurrent dedup can corrupt files across Users #375

Open
opened 2026-09-28 20:44:30 +00:00 by kayg · 29 comments
Owner

Found in the one local real-server adversarial round run for motion-spring (#291) on 2026-09-28. This is a follow-up to cross-User campaign #331.

Evidence

tests/adversarial/attack2.py ran its concurrent multi-User dedup campaign. It reported:

  • Several completed same-byte and near-identical uploads whose stored bytes differed from their source bytes.
  • Overwriting a linked file as one User changed files read back as other Users.
  • A copy did not retain the source bytes.
  • The hardlink check expected 12 Home paths on one inode and found none; the near-identical check expected distinct byte strings but found no corresponding inodes.
  • Restore returned HTTP 200, then the probe raised FileNotFoundError while reading the expected restored renamed.bin path.

The round ran under shared-host load, so it did not isolate the cause. These are data-integrity and cross-User isolation findings and need controlled reproduction before this merge proceeds. The adversarial expectation was not changed, and the round was not repeated.

Found in the one local real-server adversarial round run for motion-spring (#291) on 2026-09-28. This is a follow-up to cross-User campaign #331. ## Evidence `tests/adversarial/attack2.py` ran its concurrent multi-User dedup campaign. It reported: - Several completed same-byte and near-identical uploads whose stored bytes differed from their source bytes. - Overwriting a linked file as one User changed files read back as other Users. - A copy did not retain the source bytes. - The hardlink check expected 12 Home paths on one inode and found none; the near-identical check expected distinct byte strings but found no corresponding inodes. - Restore returned HTTP 200, then the probe raised `FileNotFoundError` while reading the expected restored `renamed.bin` path. The round ran under shared-host load, so it did not isolate the cause. These are data-integrity and cross-User isolation findings and need controlled reproduction before this merge proceeds. The adversarial expectation was not changed, and the round was not repeated.
Author
Owner

Started job/dedup-375 from dev at 02be0946d2. I am reproducing #375 and #374 on a local server, then will trace the write paths and add regression tests before fixes.

Started job/dedup-375 from dev at 02be0946d2a4908cdf323f1e9a6ed643b455795e. I am reproducing #375 and #374 on a local server, then will trace the write paths and add regression tests before fixes.
Author
Owner

Static trace before the quiet-server run: write_inner in crates/calternal-fs/src/write.rs can hardlink a Home file to the Instance-wide .cas digest when reflinks are disabled. copy in file_ops.rs can hardlink that same CAS inode. Versions hardlink their source inside a Home; Trash restores by atomic rename. A writer with direct Data directory access can mutate every linked User file through one inode. The local campaign is building now to separate this design flaw from the earlier probe’s observed byte mismatches.

Static trace before the quiet-server run: write_inner in crates/calternal-fs/src/write.rs can hardlink a Home file to the Instance-wide .cas digest when reflinks are disabled. copy in file_ops.rs can hardlink that same CAS inode. Versions hardlink their source inside a Home; Trash restores by atomic rename. A writer with direct Data directory access can mutate every linked User file through one inode. The local campaign is building now to separate this design flaw from the earlier probe’s observed byte mismatches.
Author
Owner

Controlled #375 reproduction: the baseline real-server dedup campaign completed 20 concurrent same/near-byte uploads, quota checks, overwrite, copy, rename, Trash and restore without byte findings before reaching the CLI sync phase (the temporary baseline binary run lacked this worktree’s calternal CLI binary). Four Users’ same-2.bin paths had the same inode 16126155. After the local server stopped, a direct write through one Home path changed the bytes read through another User’s path (verified True). Thus the original API overwrite/copy/restore mismatches did not reproduce in this quiet first phase; the cross-User hardlink isolation flaw did reproduce.

Controlled #375 reproduction: the baseline real-server dedup campaign completed 20 concurrent same/near-byte uploads, quota checks, overwrite, copy, rename, Trash and restore without byte findings before reaching the CLI sync phase (the temporary baseline binary run lacked this worktree’s calternal CLI binary). Four Users’ same-2.bin paths had the same inode 16126155. After the local server stopped, a direct write through one Home path changed the bytes read through another User’s path (verified True). Thus the original API overwrite/copy/restore mismatches did not reproduce in this quiet first phase; the cross-User hardlink isolation flaw did reproduce.
Author
Owner

#375 design decision and first implementation slice: use per-User Blob store partitions under .cas/users//, retaining read/repair of legacy Instance-wide .cas entries. A 50-User unit test failed on baseline (one inode, expected 50) and passed after the change; the complete calternal-fs test suite passed 40 unit and 42 storage tests. Copies now reuse a blob only within the same Home. Next: live-safe idempotent legacy-link repair and per-User scrub coverage.

#375 design decision and first implementation slice: use per-User Blob store partitions under .cas/users/<immutable User ID>/, retaining read/repair of legacy Instance-wide .cas entries. A 50-User unit test failed on baseline (one inode, expected 50) and passed after the change; the complete calternal-fs test suite passed 40 unit and 42 storage tests. Copies now reuse a blob only within the same Home. Next: live-safe idempotent legacy-link repair and per-User scrub coverage.
Author
Owner

#375 progress: per-User Blob store partitions are committed (24373a39), the dry-run/atomic legacy repair is committed (39a5579b), and the bounded scrub now checks and repairs each per-User partition without searching another Home (c420f7a6). The new scrub test failed on the old cursor (zero detected corruptions) and passed after the change. calternal-fs gates: Clippy exit 0; 40 unit tests and 44 storage tests passed. I found one more path to harden: a cross-Home move can carry a linked inode into the destination Home, so I am adding a regression and fixing that before the server route and adversarial round.

#375 progress: per-User Blob store partitions are committed (24373a39), the dry-run/atomic legacy repair is committed (39a5579b), and the bounded scrub now checks and repairs each per-User partition without searching another Home (c420f7a6). The new scrub test failed on the old cursor (zero detected corruptions) and passed after the change. calternal-fs gates: Clippy exit 0; 40 unit tests and 44 storage tests passed. I found one more path to harden: a cross-Home move can carry a linked inode into the destination Home, so I am adding a regression and fixing that before the server route and adversarial round.
Author
Owner

#375 progress, branch job/dedup-375. The migration scan now identifies cross-Home inodes and links to the legacy Instance-wide Blob store, including Versions and Trash. A dry run hashes candidates without changing them. Apply verifies each copied inode before an atomic exchange and is idempotent. One additional case appeared in review: a legacy CAS name can have damaged bytes. The repair now reports the mismatch and still isolates each Home's current bytes, so a later write cannot propagate further damage; the integrity scrub remains responsible for restoring the intended bytes. A regression test covers this case. The Owner-only repair endpoint and the updated adversarial campaign are pending Rust build and live verification.

#375 progress, branch job/dedup-375. The migration scan now identifies cross-Home inodes and links to the legacy Instance-wide Blob store, including Versions and Trash. A dry run hashes candidates without changing them. Apply verifies each copied inode before an atomic exchange and is idempotent. One additional case appeared in review: a legacy CAS name can have damaged bytes. The repair now reports the mismatch and still isolates each Home's current bytes, so a later write cannot propagate further damage; the integrity scrub remains responsible for restoring the intended bytes. A regression test covers this case. The Owner-only repair endpoint and the updated adversarial campaign are pending Rust build and live verification.
Author
Owner

#375 additional finding: the legacy integrity scrub still rebuilt one global Blob store inode and linked repaired files from multiple Homes to it. This could reintroduce the cross-User corruption path after migration. Commit 10ae3e60 restores legacy damage into each affected User's own Blob store partition. Evidence: the storage test now asserts distinct inodes for Alice and Bob after scrub; cargo test -p calternal-fs passed (40 unit, 46 storage, 0 failures) and cargo clippy -p calternal-fs --all-targets -- -D warnings passed. The server route and five live dedup runs remain in progress.

#375 additional finding: the legacy integrity scrub still rebuilt one global Blob store inode and linked repaired files from multiple Homes to it. This could reintroduce the cross-User corruption path after migration. Commit 10ae3e60 restores legacy damage into each affected User's own Blob store partition. Evidence: the storage test now asserts distinct inodes for Alice and Bob after scrub; `cargo test -p calternal-fs` passed (40 unit, 46 storage, 0 failures) and `cargo clippy -p calternal-fs --all-targets -- -D warnings` passed. The server route and five live dedup runs remain in progress.
Author
Owner

#375 additional finding: a Group file can retain an old hardlink to a per-User Blob store inode even after its global CAS name is gone. A later equal-byte upload could then link that inode into a Home. Commit 3f35d04a makes every linked Group file private during the one-time repair. A storage regression writes through the Group file, then uploads equal bytes again and checks that the Home and Group remain independent. cargo test -p calternal-fs passed (40 unit, 47 storage, 0 failures); cargo clippy -p calternal-fs --all-targets -- -D warnings passed. The runnable server build and live adversarial campaign are in progress.

#375 additional finding: a Group file can retain an old hardlink to a per-User Blob store inode even after its global CAS name is gone. A later equal-byte upload could then link that inode into a Home. Commit 3f35d04a makes every linked Group file private during the one-time repair. A storage regression writes through the Group file, then uploads equal bytes again and checks that the Home and Group remain independent. `cargo test -p calternal-fs` passed (40 unit, 47 storage, 0 failures); `cargo clippy -p calternal-fs --all-targets -- -D warnings` passed. The runnable server build and live adversarial campaign are in progress.
Author
Owner

#375 migration scope finding: staged Homes under .user-data-internal/user-deletions and archived Homes under .user-data-internal/user-archives can retain legacy shared inodes outside users/. A later transfer can bring staged bytes back into a live Home. Commit 58c0f8c3 scans these trees, verifies and atomically copies each linked file, and checks the existing free-space reserve before every copy. The migration regression now covers live, Version, staged, and archived paths on one legacy inode. cargo test -p calternal-fs passed (40 unit, 47 storage, 0 failures); cargo clippy -p calternal-fs --all-targets -- -D warnings and cargo fmt --check passed. Live API verification remains in progress.

#375 migration scope finding: staged Homes under `.user-data-internal/user-deletions` and archived Homes under `.user-data-internal/user-archives` can retain legacy shared inodes outside `users/`. A later transfer can bring staged bytes back into a live Home. Commit 58c0f8c3 scans these trees, verifies and atomically copies each linked file, and checks the existing free-space reserve before every copy. The migration regression now covers live, Version, staged, and archived paths on one legacy inode. `cargo test -p calternal-fs` passed (40 unit, 47 storage, 0 failures); `cargo clippy -p calternal-fs --all-targets -- -D warnings` and `cargo fmt --check` passed. Live API verification remains in progress.
Author
Owner

Job report for #375 and #374. Branch: job/dedup-375. Head: f2c4cb39a0. No push, deployment, or merge into dev was performed.

Built: Blob store entries are partitioned by immutable User ID; new cross-Home hardlinks are prohibited. Group/system writes remain private. Cross-Home copy, move, transfer, Versions, Trash, scrub and restore paths preserve private bytes. Added an idempotent live repair with dry-run, count-only Owner/Admin endpoint, inode/hash verification, free-space reserve, and atomic per-path replacement for legacy links in live/staged/archived Homes and Group files. Added 50-concurrent-User and legacy repair regression tests. Updated attack2.py dedup expectations and exact copy/restore checks. #374: a fresh Owner assertion issued an app-password profile with HTTP 200 on the local server; the earlier 403 arose after assertion expiry in the campaign, so the probe now refreshes it before issuance. No auth authorization code changed.

Files: crates/calternal-fs/src/{blob,file_ops,lib,path,root,user_homes,write}.rs; crates/calternal-fs/tests/storage.rs; crates/calternal-server/src/wire.rs; docs/DESIGN.md; tests/adversarial/attack2.py.

Final clean-branch gate output (verbatim):
cargo fmt --check: exit 0, no output
cargo clippy -p calternal-fs --all-targets -- -D warnings:
Checking calternal-fs v0.1.0 (/home/kayg/Developer/calternal-wt/dedup-375/crates/calternal-fs)
Finished dev profile [unoptimized + debuginfo] target(s) in 10.59s
cargo test -p calternal-fs:
test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.00s
test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.53s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
python3 -m py_compile tests/adversarial/attack2.py: exit 0, no output
git diff --check: exit 0, no output

Five sequential dedup-only local-server campaigns, built before the clean dev rebase, each reported:
server alive at end: True
==== ROUND 2 FINDINGS 0
Slow-only counts were 1, 0, 1, 1, 0 (GC Trash orphans). The server/CLI/sync binary build on that earlier branch exited 0. Cargo clean completed: "Removed 8881 files, 5.9GiB total". Web build output was removed.

Known gaps: clean-branch calternal-server Clippy/test and full workspace gates did not finish before the four-hour job limit; server Clippy was still compiling dependencies and was stopped. The five live campaigns used the pre-rebase build; the clean branch was not rebuilt or probed on a live server. The migration has not run on calternal.cloud. The orchestrator must complete these checks before merging; no claim is made that those gates passed.

Decisions beyond DESIGN: select per-User Blob store isolation (option a) to eliminate both mutable cross-User links and cross-User existence/timing signals; preserve each legacy file's current bytes if its old digest name disagrees, and report the mismatch rather than overwriting it. The repair holds writer/operation locks and reports counts without paths.

Job report for #375 and #374. Branch: job/dedup-375. Head: f2c4cb39a0e2c090b0cc3f725d1f2e0b6698beff. No push, deployment, or merge into dev was performed. Built: Blob store entries are partitioned by immutable User ID; new cross-Home hardlinks are prohibited. Group/system writes remain private. Cross-Home copy, move, transfer, Versions, Trash, scrub and restore paths preserve private bytes. Added an idempotent live repair with dry-run, count-only Owner/Admin endpoint, inode/hash verification, free-space reserve, and atomic per-path replacement for legacy links in live/staged/archived Homes and Group files. Added 50-concurrent-User and legacy repair regression tests. Updated attack2.py dedup expectations and exact copy/restore checks. #374: a fresh Owner assertion issued an app-password profile with HTTP 200 on the local server; the earlier 403 arose after assertion expiry in the campaign, so the probe now refreshes it before issuance. No auth authorization code changed. Files: crates/calternal-fs/src/{blob,file_ops,lib,path,root,user_homes,write}.rs; crates/calternal-fs/tests/storage.rs; crates/calternal-server/src/wire.rs; docs/DESIGN.md; tests/adversarial/attack2.py. Final clean-branch gate output (verbatim): cargo fmt --check: exit 0, no output cargo clippy -p calternal-fs --all-targets -- -D warnings: Checking calternal-fs v0.1.0 (/home/kayg/Developer/calternal-wt/dedup-375/crates/calternal-fs) Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.59s cargo test -p calternal-fs: test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 22.00s test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.53s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s python3 -m py_compile tests/adversarial/attack2.py: exit 0, no output git diff --check: exit 0, no output Five sequential dedup-only local-server campaigns, built before the clean dev rebase, each reported: server alive at end: True ==== ROUND 2 FINDINGS 0 Slow-only counts were 1, 0, 1, 1, 0 (GC Trash orphans). The server/CLI/sync binary build on that earlier branch exited 0. Cargo clean completed: "Removed 8881 files, 5.9GiB total". Web build output was removed. Known gaps: clean-branch calternal-server Clippy/test and full workspace gates did not finish before the four-hour job limit; server Clippy was still compiling dependencies and was stopped. The five live campaigns used the pre-rebase build; the clean branch was not rebuilt or probed on a live server. The migration has not run on calternal.cloud. The orchestrator must complete these checks before merging; no claim is made that those gates passed. Decisions beyond DESIGN: select per-User Blob store isolation (option a) to eliminate both mutable cross-User links and cross-User existence/timing signals; preserve each legacy file's current bytes if its old digest name disagrees, and report the mismatch rather than overwriting it. The repair holds writer/operation locks and reports counts without paths.
Author
Owner

Starting round 2 on job/dedup-375. Branch HEAD before merge: f2c4cb39a0; fetched origin/dev: 369ab6a2f9. I read CLAUDE.md, CONTEXT.md and DESIGN.md §5. I will merge origin/dev once and resolve the compiler conflicts while preserving both sides’ behavior.

Starting round 2 on job/dedup-375. Branch HEAD before merge: f2c4cb39a0e2c090b0cc3f725d1f2e0b6698beff; fetched origin/dev: 369ab6a2f9fc673e3564b94857fbecfeb04df404. I read CLAUDE.md, CONTEXT.md and DESIGN.md §5. I will merge origin/dev once and resolve the compiler conflicts while preserving both sides’ behavior.
Author
Owner

Resumed from the usage-limit interruption at 79ccb1ff18 on job/dedup-375. The worktree is clean. Fetch and merge of origin/dev reports "Already up to date." The production web build completed before Cargo; Frontend::get call sites remain unchanged. I will run merged-build crate gates, five consecutive live dedup campaigns, and the two-User matrix, then publish count-only dry-run evidence and admin commands.

Resumed from the usage-limit interruption at 79ccb1ff180dbae9f6d62ea8c4bc6536a58f151e on job/dedup-375. The worktree is clean. Fetch and merge of origin/dev reports "Already up to date." The production web build completed before Cargo; Frontend::get call sites remain unchanged. I will run merged-build crate gates, five consecutive live dedup campaigns, and the two-User matrix, then publish count-only dry-run evidence and admin commands.
Author
Owner

Resume progress at 0df4d37f: production web build passed before Cargo. cargo fmt --check passed; cargo clippy -p calternal-fs --all-targets -- -D warnings finished successfully. Web output (verbatim):
svelte-check found 0 errors and 0 warnings
Test Files 129 passed (129)
Tests 825 passed (825)
Server Clippy is still compiling dependencies on the shared build host. I added the repair operation to the fresh-admin authorization policy; the contract/client regeneration and merged-build live campaigns follow the runnable server build. No Frontend::get call site was changed.

Resume progress at 0df4d37f: production web build passed before Cargo. `cargo fmt --check` passed; `cargo clippy -p calternal-fs --all-targets -- -D warnings` finished successfully. Web output (verbatim): svelte-check found 0 errors and 0 warnings Test Files 129 passed (129) Tests 825 passed (825) Server Clippy is still compiling dependencies on the shared build host. I added the repair operation to the fresh-admin authorization policy; the contract/client regeneration and merged-build live campaigns follow the runnable server build. No Frontend::get call site was changed.
Author
Owner

Merged-build calternal-server Clippy passed after the production web build. Verbatim completion:
Finished dev profile [unoptimized + debuginfo] target(s) in 3m 25s
The reported Frontend::get errors did not recur. No call sites were changed. The initial server Clippy attempt was interrupted after shared compiler-cache requests stalled; disabling RUSTC_WRAPPER for this job restored active compilation. CARGO_BUILD_JOBS remains 4. Filesystem tests passed (39 unit, 47 storage). Server tests and the live build are running.

Merged-build calternal-server Clippy passed after the production web build. Verbatim completion: Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 25s The reported Frontend::get errors did not recur. No call sites were changed. The initial server Clippy attempt was interrupted after shared compiler-cache requests stalled; disabling RUSTC_WRAPPER for this job restored active compilation. CARGO_BUILD_JOBS remains 4. Filesystem tests passed (39 unit, 47 storage). Server tests and the live build are running.
Author
Owner

Resumed #375 on job/dedup-375 at 6ece080ac. The worktree was clean. Fetched and merged origin/dev once. Building apps/web/build/ before Rust gates as instructed; no Frontend::get changes. Will run the three crate gates, five consecutive live dedup campaigns, the two-User matrix, and report count-only repair preview and exact admin commands.

Resumed #375 on `job/dedup-375` at `6ece080ac`. The worktree was clean. Fetched and merged `origin/dev` once. Building `apps/web/build/` before Rust gates as instructed; no Frontend::get changes. Will run the three crate gates, five consecutive live dedup campaigns, the two-User matrix, and report count-only repair preview and exact admin commands.
Author
Owner

Current origin/dev was merged once at d39227f27. Production web build completed before Cargo. Server Clippy passed without any Frontend::get changes. Verbatim completion:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 54s

Filesystem tests passed (39 unit, 47 storage, zero failures). Tests for the merged server and Files plugin, and the live binary build, are in progress. Commit 916637e2f documents the existing fresh-admin requirement for both repair preview and apply.

Current origin/dev was merged once at d39227f27. Production web build completed before Cargo. Server Clippy passed without any Frontend::get changes. Verbatim completion: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 54s ``` Filesystem tests passed (39 unit, 47 storage, zero failures). Tests for the merged server and Files plugin, and the live binary build, are in progress. Commit 916637e2f documents the existing fresh-admin requirement for both repair preview and apply.
Author
Owner

Merged-build progress at dfb658944: all three scoped Clippy gates passed. Filesystem and server tests passed. Web check and tests passed. Files plugin test and live binary build remain in progress. Gate excerpts (verbatim):

fs-comment-clippy.log
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 36s
server-clippy.log
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 54s
files-clippy.log
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 18s
fs-comment-test.log
    Finished `test` profile [unoptimized + debuginfo] target(s) in 17m 14s
test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 116.13s
test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.06s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
server-test.log
    Finished `test` profile [unoptimized + debuginfo] target(s) in 16m 05s
test result: ok. 84 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 68.64s
web-check.log
svelte-check found 0 errors and 0 warnings
web-test.log
 Test Files  131 passed (131)
      Tests  845 passed (845)

The 40-unit-test filesystem count is the current merged result; my previous progress comment used the older 39-unit-test count. No Frontend::get call sites were changed.

Merged-build progress at dfb658944: all three scoped Clippy gates passed. Filesystem and server tests passed. Web check and tests passed. Files plugin test and live binary build remain in progress. Gate excerpts (verbatim): ```text fs-comment-clippy.log Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 36s server-clippy.log Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 54s files-clippy.log Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 18s fs-comment-test.log Finished `test` profile [unoptimized + debuginfo] target(s) in 17m 14s test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 116.13s test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.06s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s server-test.log Finished `test` profile [unoptimized + debuginfo] target(s) in 16m 05s test result: ok. 84 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 68.64s web-check.log svelte-check found 0 errors and 0 warnings web-test.log Test Files 131 passed (131) Tests 845 passed (845) ``` The 40-unit-test filesystem count is the current merged result; my previous progress comment used the older 39-unit-test count. No Frontend::get call sites were changed.
Author
Owner

Admin commands (DESIGN §5): set CALTERNAL_ORIGIN to the Instance origin, without a trailing slash. Set CALTERNAL_AUTH_CONFIG to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue.

Dry run:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths.

Admin commands (DESIGN §5): set `CALTERNAL_ORIGIN` to the Instance origin, without a trailing slash. Set `CALTERNAL_AUTH_CONFIG` to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue. Dry run: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths.
Author
Owner

Resumed job/dedup-375 from clean head dfb658944. Fetched origin/dev once and merged current dev. The sole conflict was the write_inner doc comment: retained #375 per-User Blob store isolation and #468 synchronous durability span rationale. Earlier scoped gates completed before interruption, except Files tests and live binary build. Rebuilding the web before Cargo, then validating the new merged head and running five live dedup campaigns plus the two-User matrix. No Frontend::get call sites changed.

Resumed job/dedup-375 from clean head dfb658944. Fetched origin/dev once and merged current dev. The sole conflict was the write_inner doc comment: retained #375 per-User Blob store isolation and #468 synchronous durability span rationale. Earlier scoped gates completed before interruption, except Files tests and live binary build. Rebuilding the web before Cargo, then validating the new merged head and running five live dedup campaigns plus the two-User matrix. No Frontend::get call sites changed.
Author
Owner

The current merged source registers admin_repair_cross_home_links in wire.rs, but contracts/openapi.json and packages/api-client/src/generated.ts contain no such operation. The generated contract must include the repair route so route-complete authorization and cross-User probes can classify it. I will regenerate from the merged binary and update the generated parity matrix. Web check reports: svelte-check found 0 errors and 0 warnings. Web tests report: Tests 877 passed (877).

The current merged source registers admin_repair_cross_home_links in wire.rs, but contracts/openapi.json and packages/api-client/src/generated.ts contain no such operation. The generated contract must include the repair route so route-complete authorization and cross-User probes can classify it. I will regenerate from the merged binary and update the generated parity matrix. Web check reports: svelte-check found 0 errors and 0 warnings. Web tests report: Tests 877 passed (877).
Author
Owner

Merged head dfbdb5d1c: fmt passed (no output), filesystem Clippy and tests passed, server Clippy passed, web check and all 877 web tests passed. Server tests are still compiling/linking the merged plugin graph. The server compiler spent time in folio_wait_bit_common (disk reads), then moved to do_sys_poll; no compiler errors have appeared. Keeping one Cargo gate sequence, four build jobs, and the assigned target directory. Next: finish server/Files tests and binary, regenerate repair OpenAPI/client/parity, run five fresh live dedup campaigns and the two-User matrix, post count-only preview and exact admin commands, then clean job build output.

Merged head dfbdb5d1c: fmt passed (no output), filesystem Clippy and tests passed, server Clippy passed, web check and all 877 web tests passed. Server tests are still compiling/linking the merged plugin graph. The server compiler spent time in folio_wait_bit_common (disk reads), then moved to do_sys_poll; no compiler errors have appeared. Keeping one Cargo gate sequence, four build jobs, and the assigned target directory. Next: finish server/Files tests and binary, regenerate repair OpenAPI/client/parity, run five fresh live dedup campaigns and the two-User matrix, post count-only preview and exact admin commands, then clean job build output.
Author
Owner

Admin commands (DESIGN §5): set CALTERNAL_ORIGIN to the Instance origin, without a trailing slash. Set CALTERNAL_AUTH_CONFIG to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue.

Dry run:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths.

Admin commands (DESIGN §5): set `CALTERNAL_ORIGIN` to the Instance origin, without a trailing slash. Set `CALTERNAL_AUTH_CONFIG` to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue. Dry run: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths.
Author
Owner

Merged-build Files test result: the existing 1,000-write reconcile storm exceeded its 300-second deadline under shared-host disk load. No expectation changed and no gate retry. 128 tests passed, one failed, one ignored. The first panic is Elapsed(()); atomic write 917 then reports entry not found after the test unwinds and removes its temporary directory. Treating this as a timing gate gap, not evidence of a live cross-User integrity failure. Continuing the live binary build and required campaigns. Verbatim output:

---- tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm stdout ----

thread 'tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm' (2033845) panicked at crates/plugins/files/src/lib.rs:4227:14:
writes, reconcile scans, and watcher adoption complete within five minutes: Elapsed(())
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'tokio-rt-worker' (2033849) panicked at crates/plugins/files/src/lib.rs:4201:45:
atomic write 917 failed: entry not found


failures:
    tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm

test result: FAILED. 128 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 332.82s

error: test failed, to rerun pass `-p calternal-plugin-files --lib`
Merged-build Files test result: the existing 1,000-write reconcile storm exceeded its 300-second deadline under shared-host disk load. No expectation changed and no gate retry. 128 tests passed, one failed, one ignored. The first panic is Elapsed(()); atomic write 917 then reports entry not found after the test unwinds and removes its temporary directory. Treating this as a timing gate gap, not evidence of a live cross-User integrity failure. Continuing the live binary build and required campaigns. Verbatim output: ```text ---- tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm stdout ---- thread 'tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm' (2033845) panicked at crates/plugins/files/src/lib.rs:4227:14: writes, reconcile scans, and watcher adoption complete within five minutes: Elapsed(()) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'tokio-rt-worker' (2033849) panicked at crates/plugins/files/src/lib.rs:4201:45: atomic write 917 failed: entry not found failures: tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm test result: FAILED. 128 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 332.82s error: test failed, to rerun pass `-p calternal-plugin-files --lib` ```
Author
Owner

The merged binary builds successfully with no Frontend::get changes. Regeneration adds only the repair operation and its two schemas to OpenAPI and the typed client (85 JSON lines, 76 TypeScript lines). The offline cross-User classification gate passes: 309 operations classified; three classification regression tests pass. Parity regeneration also found dev's stale #468 inventory: the web calls notes_journal_create_log_batch instead of notes_journal_create_log. Its CLI route is present; MCP and WebMCP remain listed as gaps. Refreshing that generated inventory keeps the merged CI check current; no adapter behaviour changes.

The merged binary builds successfully with no Frontend::get changes. Regeneration adds only the repair operation and its two schemas to OpenAPI and the typed client (85 JSON lines, 76 TypeScript lines). The offline cross-User classification gate passes: 309 operations classified; three classification regression tests pass. Parity regeneration also found dev's stale #468 inventory: the web calls notes_journal_create_log_batch instead of notes_journal_create_log. Its CLI route is present; MCP and WebMCP remain listed as gaps. Refreshing that generated inventory keeps the merged CI check current; no adapter behaviour changes.
Author
Owner

Count-only dry-run sample from the real merged local server and a synthetic Home fixture (two linked legacy Home paths):

DEDUP_REPAIR_DRY_RUN scanned_files=22 shared_paths=2 repaired_paths=0 verified_bytes=100 corrupt_paths=0

Preview made no repairs. The live campaign checks that both Home inodes stay unchanged during preview, then checks apply and retry. Exact dry-run and apply curl commands are in the preceding admin-command comment; both require a fresh Owner/Admin passkey assertion and an admin-scoped installation session.

Count-only dry-run sample from the real merged local server and a synthetic Home fixture (two linked legacy Home paths): ```text DEDUP_REPAIR_DRY_RUN scanned_files=22 shared_paths=2 repaired_paths=0 verified_bytes=100 corrupt_paths=0 ``` Preview made no repairs. The live campaign checks that both Home inodes stay unchanged during preview, then checks apply and retry. Exact dry-run and apply curl commands are in the preceding admin-command comment; both require a fresh Owner/Admin passkey assertion and an admin-scoped installation session.
Author
Owner

The two-User matrix exited 0 on merged build 6d2c1b4d0. Verbatim result (including fixture coverage limits):

Two-User OpenAPI matrix: 309 operations classified; 153 operations replayed; 549 A-ID vs missing-ID comparisons across B, C, D and anonymous; 15 identifier routes classified with no local fixture factory; median absolute timing delta 6.3 ms
Job/Mail/quota ownership checks: 77 comparisons; 0 denial failures
Identifier routes without a seeded User-owned object: PUT /api/v1/admin/config (put_config), PUT /api/v1/admin/plugins/{id} (set_instance_plugin), GET /api/v1/appearance/unsplash/images/{photo_id} (appearance_unsplash_thumbnail), POST /api/v1/appearance/unsplash/{photo_id} (appearance_unsplash_select), GET /api/v1/auth/app-password-profiles/{token} (download_app_password_profile), POST /api/v1/auth/app-passwords (create_app_password), POST /api/v1/auth/invites/start (invite_start), DELETE /api/v1/auth/invites/{id} (revoke_invite), POST /api/v1/auth/passkeys/reenrol/start (reenrol_start), POST /api/v1/auth/setup/start (setup_start), GET /api/v1/mail/inbox/messages (mail_unified_inbox), POST /api/v1/notes/from-template (notes_template_create), POST /api/v1/notes/journal/{date}/lines/{hash}/fix (notes_journal_fix_line), PUT /api/v1/notes/templates/default (notes_templates_default_set), PUT /api/v1/plugins/{id}/me (set_user_plugin)
The two-User matrix exited 0 on merged build 6d2c1b4d0. Verbatim result (including fixture coverage limits): ```text Two-User OpenAPI matrix: 309 operations classified; 153 operations replayed; 549 A-ID vs missing-ID comparisons across B, C, D and anonymous; 15 identifier routes classified with no local fixture factory; median absolute timing delta 6.3 ms Job/Mail/quota ownership checks: 77 comparisons; 0 denial failures Identifier routes without a seeded User-owned object: PUT /api/v1/admin/config (put_config), PUT /api/v1/admin/plugins/{id} (set_instance_plugin), GET /api/v1/appearance/unsplash/images/{photo_id} (appearance_unsplash_thumbnail), POST /api/v1/appearance/unsplash/{photo_id} (appearance_unsplash_select), GET /api/v1/auth/app-password-profiles/{token} (download_app_password_profile), POST /api/v1/auth/app-passwords (create_app_password), POST /api/v1/auth/invites/start (invite_start), DELETE /api/v1/auth/invites/{id} (revoke_invite), POST /api/v1/auth/passkeys/reenrol/start (reenrol_start), POST /api/v1/auth/setup/start (setup_start), GET /api/v1/mail/inbox/messages (mail_unified_inbox), POST /api/v1/notes/from-template (notes_template_create), POST /api/v1/notes/journal/{date}/lines/{hash}/fix (notes_journal_fix_line), PUT /api/v1/notes/templates/default (notes_templates_default_set), PUT /api/v1/plugins/{id}/me (set_user_plugin) ```
Author
Owner

Head 6d2c1b4d0: contract reproducibility passes (check-generated.sh exit 0 with no git diff). Two complete consecutive live dedup campaigns have passed on the merged source. Round 1: FINDINGS 0, SLOW 6, server alive True. Round 2: FINDINGS 0, SLOW 2, server alive True. Both completed the upload, publish, and GC restart checks. Round 3 is active; no Rust source or test expectation changed between runs. The initial incomplete attempt stopped on a missing CLI executable; the CLI/sync build is now complete and that attempt is not counted. The two-User matrix passed separately. The remaining gate gap is the Files 1,000-write storm's 300-second timeout already reported with exact output.

Head 6d2c1b4d0: contract reproducibility passes (check-generated.sh exit 0 with no git diff). Two complete consecutive live dedup campaigns have passed on the merged source. Round 1: FINDINGS 0, SLOW 6, server alive True. Round 2: FINDINGS 0, SLOW 2, server alive True. Both completed the upload, publish, and GC restart checks. Round 3 is active; no Rust source or test expectation changed between runs. The initial incomplete attempt stopped on a missing CLI executable; the CLI/sync build is now complete and that attempt is not counted. The two-User matrix passed separately. The remaining gate gap is the Files 1,000-write storm's 300-second timeout already reported with exact output.
Author
Owner

The fifth required campaign has request timeouts at the existing 30-second deadline. Four near-identical fixtures were never uploaded: a read-only live Index check found 4 pending campaign upload rows, 1,048,768 declared bytes, 0 received bytes, and 0 installing rows. These account for four missing Home paths and the quota/inode deficits. No wrong bytes have been observed for a successful completed upload.

Filed #485 (#485) to record that the probe labels missing incomplete fixtures as corrupted completed files. Existing status, byte, quota and inode expectations remain unchanged. The fifth campaign is continuing through the remaining restart checks; it cannot be reported as a clean pass. The preceding four complete runs each reported FINDINGS 0 and server alive True. I will report the actual fifth-run totals and the Files gate timeout, then clean build output and exit without a repeat loop.

The fifth required campaign has request timeouts at the existing 30-second deadline. Four near-identical fixtures were never uploaded: a read-only live Index check found 4 pending campaign upload rows, 1,048,768 declared bytes, 0 received bytes, and 0 installing rows. These account for four missing Home paths and the quota/inode deficits. No wrong bytes have been observed for a successful completed upload. Filed #485 (https://git.kayg.org/kayg/calternal/issues/485) to record that the probe labels missing incomplete fixtures as corrupted completed files. Existing status, byte, quota and inode expectations remain unchanged. The fifth campaign is continuing through the remaining restart checks; it cannot be reported as a clean pass. The preceding four complete runs each reported FINDINGS 0 and server alive True. I will report the actual fifth-run totals and the Files gate timeout, then clean build output and exit without a repeat loop.
Author
Owner

Per-User Blob stores, private cross-Home copy and move, resumable legacy-link repair with an explicit dry-run mode, scoped scrub recovery, and the restore probe correction are committed. The repair API and typed client are current. The dev merge preserves write durability tracing. No Frontend::get call site was changed; the production web build was made before Cargo.

Branch: job/dedup-375. Head: 6d2c1b4d03. Merged dev commit: dfb5964a2f. No push, deploy, or merge into dev/main.

Files:

  • contracts/openapi.json
  • crates/calternal-fs/src/blob.rs
  • crates/calternal-fs/src/file_ops.rs
  • crates/calternal-fs/src/lib.rs
  • crates/calternal-fs/src/path.rs
  • crates/calternal-fs/src/root.rs
  • crates/calternal-fs/src/user_homes.rs
  • crates/calternal-fs/src/write.rs
  • crates/calternal-fs/tests/storage.rs
  • crates/calternal-server/src/wire.rs
  • docs/DESIGN.md
  • docs/parity-exceptions.json
  • docs/parity-matrix.md
  • packages/api-client/src/generated.ts
  • tests/adversarial/attack2.py
  • tests/adversarial/authz_matrix.py

Decisions: no new product decision. The accepted choices are in DESIGN §5. Regeneration also refreshes dev’s stale Log batch parity inventory; it records the existing MCP/WebMCP gaps without changing an adapter.

Scoped gate output (verbatim completion lines):

cargo fmt --check exited 0 with no output.

cargo clippy -p calternal-fs --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.31s

cargo test -p calternal-fs

    Finished `test` profile [unoptimized + debuginfo] target(s) in 11.16s
test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 18.69s
test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.60s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 47s

cargo test -p calternal-server

    Finished `test` profile [unoptimized + debuginfo] target(s) in 21m 44s
test result: ok. 85 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 30.72s

cargo clippy -p calternal-plugin-files --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 21s

cargo test -p calternal-plugin-files

    Finished `test` profile [unoptimized + debuginfo] target(s) in 12m 05s
test result: FAILED. 128 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 332.82s

final-web-check (exit 0)

svelte-check found 0 errors and 0 warnings

final-web-test (exit 0)

 Test Files  136 passed (136)
      Tests  877 passed (877)

generated-web-check (exit 0)

svelte-check found 0 errors and 0 warnings

api-client-test (exit 0)

 9 pass
 0 fail
Ran 9 tests across 1 file. [944.00ms]

parity-check (exit 0)

Parity matrix: 187 web API actions, 113 shortcuts, 2 static commands, 131 menu actions, 30 settings groups, 169 actions with adapter gaps

contract-check (exit 0)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 25m 29s
🚀 ../../contracts/openapi.json → src/generated.ts [4.9s]

classification (exit 0)

Cross-User classification gate: 309 operations classified
Ran 3 tests in 0.086s
OK

Known gate gap: the Files 1,000-write test exceeded its unchanged 300-second deadline. Its first panic was Elapsed(()). During unwind, the temporary directory was removed and worker write 917 then reported entry not found. No test expectation changed and no gate repeat.

The two-User matrix exited 0. Verbatim result:

Two-User OpenAPI matrix: 309 operations classified; 153 operations replayed; 549 A-ID vs missing-ID comparisons across B, C, D and anonymous; 15 identifier routes classified with no local fixture factory; median absolute timing delta 6.3 ms
Job/Mail/quota ownership checks: 77 comparisons; 0 denial failures

Its 15 routes without seeded User-owned objects remain a coverage limit, listed in the matrix log and preceding issue comment.

Five consecutive complete live campaign attempts (verbatim summaries):

Round 1, exit 0:

server alive at end: True
==== ROUND 2 FINDINGS 0
==== ROUND 2 SLOW 6

Round 2, exit 0:

server alive at end: True
==== ROUND 2 FINDINGS 0
==== ROUND 2 SLOW 2

Round 3, exit 0:

server alive at end: True
==== ROUND 2 FINDINGS 0
==== ROUND 2 SLOW 2

Round 4, exit 0:

server alive at end: True
==== ROUND 2 FINDINGS 0
==== ROUND 2 SLOW 20

Round 5, exit 1:

server alive at end: True
==== ROUND 2 FINDINGS 21
==== ROUND 2 SLOW 25

Four clean runs were achieved; five clean runs were not achieved. Round 5 timed out POST/PATCH calls at the existing 30-second deadline. The read-only live Index inspection found four pending fixtures with 1,048,768 declared bytes, zero received bytes, and zero installing rows. They explain the missing files and dependent quota/inode findings. No wrong bytes were observed for a successful completed upload. All non-SLOW findings are recorded in #485: #485. Assertions remain unchanged; there was no repeat loop.

Count-only synthetic dry-run sample:

DEDUP_REPAIR_DRY_RUN scanned_files=22 shared_paths=2 repaired_paths=0 verified_bytes=100 corrupt_paths=0

Admin commands (DESIGN §5): set CALTERNAL_ORIGIN to the Instance origin, without a trailing slash. Set CALTERNAL_AUTH_CONFIG to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue.

Dry run:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply:

curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair"

Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths.

Cleanup completed. Verbatim Cargo output:

     Removed 17738 files, 11.0GiB total

Production web output and the local media test runtime were removed. The worktree is clean. Reports and logs remain in ignored artifacts/dedup-375.

Per-User Blob stores, private cross-Home copy and move, resumable legacy-link repair with an explicit dry-run mode, scoped scrub recovery, and the restore probe correction are committed. The repair API and typed client are current. The dev merge preserves write durability tracing. No Frontend::get call site was changed; the production web build was made before Cargo. Branch: job/dedup-375. Head: 6d2c1b4d037615f8f3095f151e8fd7488f268712. Merged dev commit: dfb5964a2fcf13dc8b9a50a319eee09bc386f322. No push, deploy, or merge into dev/main. Files: - `contracts/openapi.json` - `crates/calternal-fs/src/blob.rs` - `crates/calternal-fs/src/file_ops.rs` - `crates/calternal-fs/src/lib.rs` - `crates/calternal-fs/src/path.rs` - `crates/calternal-fs/src/root.rs` - `crates/calternal-fs/src/user_homes.rs` - `crates/calternal-fs/src/write.rs` - `crates/calternal-fs/tests/storage.rs` - `crates/calternal-server/src/wire.rs` - `docs/DESIGN.md` - `docs/parity-exceptions.json` - `docs/parity-matrix.md` - `packages/api-client/src/generated.ts` - `tests/adversarial/attack2.py` - `tests/adversarial/authz_matrix.py` Decisions: no new product decision. The accepted choices are in DESIGN §5. Regeneration also refreshes dev’s stale Log batch parity inventory; it records the existing MCP/WebMCP gaps without changing an adapter. Scoped gate output (verbatim completion lines): `cargo fmt --check` exited 0 with no output. `cargo clippy -p calternal-fs --all-targets -- -D warnings` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.31s ``` `cargo test -p calternal-fs` ```text Finished `test` profile [unoptimized + debuginfo] target(s) in 11.16s test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 18.69s test result: ok. 47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.60s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 47s ``` `cargo test -p calternal-server` ```text Finished `test` profile [unoptimized + debuginfo] target(s) in 21m 44s test result: ok. 85 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 30.72s ``` `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 21s ``` `cargo test -p calternal-plugin-files` ```text Finished `test` profile [unoptimized + debuginfo] target(s) in 12m 05s test result: FAILED. 128 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 332.82s ``` `final-web-check` (exit 0) ```text svelte-check found 0 errors and 0 warnings ``` `final-web-test` (exit 0) ```text Test Files 136 passed (136) Tests 877 passed (877) ``` `generated-web-check` (exit 0) ```text svelte-check found 0 errors and 0 warnings ``` `api-client-test` (exit 0) ```text 9 pass 0 fail Ran 9 tests across 1 file. [944.00ms] ``` `parity-check` (exit 0) ```text Parity matrix: 187 web API actions, 113 shortcuts, 2 static commands, 131 menu actions, 30 settings groups, 169 actions with adapter gaps ``` `contract-check` (exit 0) ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 25m 29s 🚀 ../../contracts/openapi.json → src/generated.ts [4.9s] ``` `classification` (exit 0) ```text Cross-User classification gate: 309 operations classified Ran 3 tests in 0.086s OK ``` Known gate gap: the Files 1,000-write test exceeded its unchanged 300-second deadline. Its first panic was `Elapsed(())`. During unwind, the temporary directory was removed and worker write 917 then reported `entry not found`. No test expectation changed and no gate repeat. The two-User matrix exited 0. Verbatim result: ```text Two-User OpenAPI matrix: 309 operations classified; 153 operations replayed; 549 A-ID vs missing-ID comparisons across B, C, D and anonymous; 15 identifier routes classified with no local fixture factory; median absolute timing delta 6.3 ms Job/Mail/quota ownership checks: 77 comparisons; 0 denial failures ``` Its 15 routes without seeded User-owned objects remain a coverage limit, listed in the matrix log and preceding issue comment. Five consecutive complete live campaign attempts (verbatim summaries): Round 1, exit 0: ```text server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 6 ``` Round 2, exit 0: ```text server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 2 ``` Round 3, exit 0: ```text server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 2 ``` Round 4, exit 0: ```text server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 20 ``` Round 5, exit 1: ```text server alive at end: True ==== ROUND 2 FINDINGS 21 ==== ROUND 2 SLOW 25 ``` Four clean runs were achieved; five clean runs were not achieved. Round 5 timed out POST/PATCH calls at the existing 30-second deadline. The read-only live Index inspection found four pending fixtures with 1,048,768 declared bytes, zero received bytes, and zero installing rows. They explain the missing files and dependent quota/inode findings. No wrong bytes were observed for a successful completed upload. All non-SLOW findings are recorded in #485: https://git.kayg.org/kayg/calternal/issues/485. Assertions remain unchanged; there was no repeat loop. Count-only synthetic dry-run sample: ```text DEDUP_REPAIR_DRY_RUN scanned_files=22 shared_paths=2 repaired_paths=0 verified_bytes=100 corrupt_paths=0 ``` Admin commands (DESIGN §5): set `CALTERNAL_ORIGIN` to the Instance origin, without a trailing slash. Set `CALTERNAL_AUTH_CONFIG` to a private curl config file containing the Authorization header for an Owner/Admin installation session with the admin scope. Complete a fresh passkey assertion before each operation. An App Password is not sufficient. Do not put the session token in the command line, logs, or issue. Dry run: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":true}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply: ```sh curl --fail-with-body --silent --show-error --config "$CALTERNAL_AUTH_CONFIG" --header "Origin: $CALTERNAL_ORIGIN" --header 'Content-Type: application/json' --data '{"dry_run":false}' "$CALTERNAL_ORIGIN/api/v1/admin/dedup/repair" ``` Apply uses the live server writer lock. It is safe to retry after a restart. It checks the disk reserve before each private copy. It preserves current bytes and reports a damaged legacy digest; the integrity scrub handles byte recovery. Preview and apply return counts without Home paths. Cleanup completed. Verbatim Cargo output: ```text Removed 17738 files, 11.0GiB total ``` Production web output and the local media test runtime were removed. The worktree is clean. Reports and logs remain in ignored artifacts/dedup-375.
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#375
No description provided.