Sync v0.2.x: encrypted profile backup, v2 control plane, and fleet sync - #37
anhtahaylove wants to merge 33 commits into
Conversation
* Improve MCP setup guidance in Settings * Add MCP download status in settings * Clarify MCP token setup step * Add custom build label and MCP auto-detect * Allow selecting existing MCP folder * Remove unused launch import * Clarify MCP repair and existing folder actions * Point custom builds to fork releases * Add ShardX MCP health and profile lookup helpers * Add Codex-friendly MCP profile helpers * Improve launcher profile launch error states * Improve Automation API token settings UX * Release v0.1.12 custom integration Combine the MCP helper and Launcher UX branches, expose actual API/MCP readiness, and add CI plus signed release packaging for the fork. Pin the bundled MCP archive to the matching v0.1.12 release. * Update GitHub Actions Node 24 runtimes * Update release actions to Node 24 runtimes * Improve MCP setup version status * Clarify Codex MCP registration UX * Add Codex MCP registration check * Check release version parity * Validate manual signed build requests * Add ShardBrowser DevTools handoff helpers * Fix Python SDK UTF-8 profile JSON IO * Support proxy bulk import name tags * Allow temporary profile noise overrides * Add safe launch extras and custom font handoff * Port team server auth and lock core * Port snapshot safety shared crate * style: format team server port * chore: allow self-signed internal signing tests * fix: accept code signing EKU friendly name * fix: trust internal cert non-interactively * fix: verify internal self-signed signatures * fix: sign Windows portable artifact * docs: update signed custom release notes * Add MCP stale profile cleanup helper * Release v0.1.14 custom cleanup helper * Release v0.1.15 safe open cleanup fix * Fix smoke cleanup for ShardX process trees * Release v0.1.16 process-tree cleanup * feat(ui): polish launcher states and accessibility * chore(release): prepare v0.1.17 * fix(release): discard cached bundle outputs * fix(release): keep cleanup command valid YAML * feat(mcp): add human verification handoff * feat(ui): show required verification status * docs: add safe Cloudflare verification workflow * chore(release): prepare v0.1.18 custom build * chore(release): sync Cargo lock for v0.1.18 * feat: add Cloudflare verification pause and focus handoff * feat(mcp): persist verification handoff checkpoints * docs: design safe WP-CLI plugin fallback * chore(release): prepare v0.1.19 custom build * fix(mcp): derive server version from package * fix(api): prevent listener inheritance on Windows * chore(release): bump version to 0.1.20 * feat: prepare reproducible MCP and inline proxy creation * docs: refresh custom release guidance * chore(release): bump version to 0.1.21 * feat(updater): add trusted signed update flow * fix(fonts): gate unsupported custom font handoff * test(ui): add launcher regression coverage * fix(ci): provision updater verifier dependencies * fix(ci): clear handled internal signing exit code * chore(release): prepare updater-signed v0.1.22 * fix(release): normalize assets before publishing * fix(installer): make NSIS upgrades replace executable * chore(release): prepare v0.1.23 * feat: add Launcher sign-in startup * fix(release): include MCP contract test * fix(startup): keep Windows restore launches in tray * chore(deps): resolve npm security advisories * chore(release): prepare v0.1.26 * fix(profiles): fail closed around active browser mutations * chore(deps): resolve npm security advisories * fix(profile): close lifecycle race conditions * chore(release): prepare v0.1.27 * chore(deps): harden Rust SDK dependency patch * ci: run RustSec without deprecated action runtime * ci: keep updater verification on stable Rust * fix(mcp): preserve active tab for safe follow-up tools * fix(mcp): harden safe-open lifecycle and packaging * fix(mcp): bind safe-open cleanup to owned process * fix(mcp): close final lifecycle review gaps * fix: bind profile cleanup to launch instances * ci: enforce exact MCP archive manifests * chore(release): prepare v0.1.29 * docs: preserve v0.2.x team fleet design * feat(backup): add v2 encrypted profile backup container Add the v2 backup format as five modules in the shared crate: - canonical.rs: deterministic canonical CBOR that rejects non-canonical input, so signed bytes cannot be re-encoded into a different-but-accepted form. - keys.rs: key identities and DEK wrap/unwrap with ChaCha20-Poly1305, bound to a canonical slot context as associated data. - signing.rs: Ed25519 over a domain-separated, length-prefixed transcript, with acyclic commitments (a signature is never part of its own payload). - envelope.rs: DEK slots, pre-encryption intent records, STREAM frame nonces/AAD, and the restore-epoch Merkle tree with inclusion proofs. - backup.rs: the seal/open container API joining the above. Frames bind the exact intent hash, frame counter and final-frame flag, so truncation, reordering and cross-envelope splicing all fail closed. The prologue is bound to the signed head, so substituting the slot or intent before the signature check is rejected. The crypto dependencies are pinned with exact-version requirements: they define wire-visible behaviour (AEAD suite, signature encoding), so a silent minor bump could change bytes that signatures commit to. All golden vectors are pinned from the independently verified G2 conformance gate and reproduced byte-for-byte. * style(shared): clear the pre-existing clippy warnings Four warnings predating this work, so the crate can build under -D warnings: - snapshot.rs: replace a manual max/min chain with clamp. Safe because RATIO_FLOOR_BYTES (64 MB) is fixed below MAX_TOTAL_EXPANDED (4 GB), so the inverted-bounds panic is unreachable. - logins.rs: separate a rustdoc list from the paragraph following it. - snapshot.rs: use an array instead of vec! in a test fixture. - webdata.rs: keep the local binding with a narrow, documented allow. The binding is load-bearing rather than redundant — it ends the borrow of stmt before the function returns, and inlining it fails to compile (E0597). Clippy's suggestion is wrong here. * test(shared): add the G3 v1/v2 compatibility gate Prove the v2 container did not disturb the v1 portable snapshot path, against a real temporary user-data dir rather than fixtures: - a v1 snapshot still round-trips through pack/unpack; - sealing a v1 snapshot and opening it returns the exact v1 bytes, which still restore through the untouched v1 reader; - the v1 reader rejects a v2 container instead of misparsing it. Local State is deliberately excluded from the byte-equality check: the v1 restore path rewrites os_crypt.encrypted_key so the profile is decryptable by the local machine. Asserting equality there would assert the opposite of the intended portability behaviour, so the test checks the structural invariant instead. * release: v0.2.0 Bump the launcher, MCP server and Tauri app to 0.2.0 and document the encrypted profile backup format in the changelog. The MCP contract test pinned the served version, so it moves with the bump. The tool count stays asserted at 96 — that is the compatibility invariant, and it is unchanged. * test(mcp): pin the v0.1.28 96-tool compatibility fixture The baseline the v0.2.0 contract test is checked against. The .omx evidence writeup stays out of the repo — it is ignored scratch space. * feat(grants): add HPKE tenant root key grants Seal a tenant root key to a device's HPKE public key (RFC 9180 base mode, DHKEM-X25519/HKDF-SHA256/ChaCha20-Poly1305) so the server never holds the root key in the clear. The HPKE info is the byte-identical canonical TenantRootKeyGrantHpkeInfoV2 encoding, not a hash or a reconstruction, so tenant, device, recipient key, server instance, restore epoch and suite are all bound into key derivation. open_trk re-derives info and AAD from the caller's scope instead of reading them back from the record: a tampered grant derives a different key schedule and fails to open rather than opening and being believed. Tests cover every scope field, per-byte mutation of both the ciphertext and the encapsulated key, and the wrong-device case. Sealing uses the OS CSPRNG. The G2 spike sealed with a deterministic RNG so golden vectors could be reproduced byte-for-byte; carrying that into production would repeat encapsulation randomness across grants, so a test asserts two seals of the same key differ. rand_core 0.10 (hpke 0.14's dependency) dropped OsRng, so a small adapter bridges getrandom. It panics rather than falling back when OS entropy is unavailable. getrandom is aliased because the Windows os_crypt path already pulls in the 0.2 line. * feat(server): add the v2 team/fleet control-plane schema Additive migration for the v0.2 control plane: tenants, accounts, devices, signed authorization records, HPKE root-key grants, fleets, profiles, leases, snapshot manifests, the idempotency ledger and resumable upload sessions. The v1 tables are untouched, so an existing deployment migrates without a data rewrite. Two invariants are enforced structurally rather than in application code, because application code is the part most likely to be bypassed: - Tenant isolation via composite (tenant_id, id) foreign keys. A child row cannot reference a parent in another tenant even when a query forgets its WHERE clause; SQLite rejects the write. - The wire-integer domain from plan 5.6. CBOR's unsigned range is wider than SQLite's signed INTEGER, so every persisted wire integer is CHECK-constrained to 0..i64::MAX instead of silently corrupting on bind. Exact signed container bytes are stored verbatim next to their parsed columns; verification reads the bytes, never a re-encoding of the columns, so divergence is detectable instead of trusted. Idempotency keys are scoped to (server_instance_id, restore_epoch) per plan V5.1, so a request replayed from before a restore cannot be served a stale response. Tests assert the database *rejects* things, checking exact SQLite extended result codes so a weakened constraint fails loudly rather than passing as a generic error. Durability tests use a real on-disk file with close/reopen cycles per plan 11.3, since in-memory SQLite cannot prove what survives a process exit. * feat(server): verify signed authorization records on v2 endpoints Adds the request-path half of the v2 control plane: records are verified, consumed once, and filed under the tenant they name. authz: verify_record checks, in order, that the container hash matches its bytes, the signature is valid, the issuer is one this tenant trusts, and that the record binds this domain, this tenant, this server instance and the current restore epoch. Each of those is a separate rejection reason so failures are diagnosable rather than a blanket 400. The tenant binding is not redundant with the trusted-issuer check: a tenant's own issuer can sign a record naming a different tenant, and only the binding check separates the two. An e2e test presents exactly that record and a mutation run confirms removing the check turns it into a 201. idempotency: consume_replay_id claims a record via INSERT OR IGNORE on a UNIQUE index, so concurrent presentations of the same record resolve to one winner in the database rather than in application logic. A check-then- act version of the same function admits two winners under 8 concurrent tasks; the test covers that. Completed operations store their exact response bytes, so a retry replays the original result instead of re-running the effect. Endpoints: device approvals, capability grants, tenant root key grants and operation completion. The server files grants without being able to read them — the payload stays HPKE-sealed to the recipient device. server: 62 tests, clippy -D warnings clean. * docs(changelog): record the v2 control plane in 0.2.0 * feat(fleet): add snapshot upload, commit, and download transfer path Leases with monotonic fencing tokens make profile checkout exclusive, and chunked staging keeps peak memory bounded by the chunk size rather than the snapshot size. Commit is atomic: size, content hash, lease liveness, fencing token and base version are all checked before the staged blob is promoted and the version row is inserted in one transaction. The server stores ciphertext it cannot read. Snapshot manifests carry their own signed authorization domain so a capability grant cannot be replayed to publish a version. * test(fleet): measure bounded memory on a real profile tree G6 asserts peak resident memory tracks the chunk window, not the payload. Ignored by default and driven by SHARDX_G6_PROFILE_DIR, because it needs a real multi-hundred-megabyte profile that CI does not have. Measured on a 514 MB disposable copy of a real profile: peak RSS grew 1.8 MB across 2562 chunks. A mutant that buffers the payload instead of streaming it grew 515 MB and fails the assertion, so the bound is actually enforced rather than incidentally true.
…es (#14) The G7 drill covers what unit tests cannot express: two devices sharing one profile through a single server. Handover, concurrent checkout, fencing after a stall, rejected-commit rollback, restart durability and tenant isolation. Mutation testing drove the content: the first version killed only 3 of 7 removed guards. The fencing test asserted AlreadyLeased rather than a stale token, so the token comparison itself was never exercised; base-version, declared-size and chunk-ordering guards had no coverage at all. All 7 mutants are now killed. Also fixes a real test-isolation defect. The lifecycle claim map is a process singleton, and profile::tests serialized on a private lock that tests in lib.rs and api.rs could not reach, so they raced and intermittently observed each other's claims. Reproduced at 1/40 runs under thread contention, now 0/120. The lock is shared via pub(crate) and taken by every test that claims.
Three follow-ups after the v0.2.0 tag. Clippy: src-tauri now passes `--all-targets -- -D warnings`, which it did not at the tag (7 findings, all pre-existing). The arity finding on mcp_status_value was real and worth fixing rather than allowing: it took six adjacent bools across five call sites, so transposing two would have been a silent bug. They are now a named McpStatusFlags struct. The JSON shape it emits is unchanged. Test isolation: audited every test touching the four process-wide singletons in src-tauri. All 7 already take the shared lifecycle guard, and 150 runs at 32 threads produced no failures, so the class of race fixed in #14 appears closed. P-OP: adds the operator runbook and an ignored recovery drill covering seal/recover on a fresh machine plus four refusal cases. Writing it surfaced an operational hazard worth recording: because open() streams authenticated frames and only detects truncation at the signed head, a truncated container still emitted the entire 3,145,744-byte plaintext before failing. That is the documented contract, not a defect, but it means an in-place restore can leave an unauthenticated profile that looks complete. The runbook leads with the promote-on-Ok rule and the drill records the measurement. The P-OP gate itself remains blocked: it needs a named operator and a verifier, which no code change can supply.
…re (#16) shared::backup::{seal, open} has no production caller. The Launcher exposes no backup, restore, fleet or sync command and never contacts the team server, and server/src/fleet.rs transports ciphertext that no shipped client produces. The code and its tests were accurate, but the prose around them read as though the feature were usable. This corrects the framing in the changelog and the P-OP runbook, and the published release notes have been updated to match. The runbook needed more than a caveat. Three of its four sign-off rows (Launcher backup, downgrade against a real profile flow, artifact digests) describe product flows that do not exist yet, so they are now marked blocked. Only the recovery-bundle readback is runnable today. Leaving them tickable invited signing off a library call as evidence for a product flow, which is what the gate exists to prevent. Section 0's promote-on-Ok warning is retained but re-scoped: it is a constraint on the restore path whoever writes it, not a live hazard.
v0.2.0 shipped the sealing library and the fleet server with nothing in the
app able to reach them: `shared::backup::{seal, open}` had no production
caller and the Launcher had no backup surface at all (#17).
Add the two missing layers and the UI on top:
- `shared::passphrase` derives the backup FKEK with Argon2id (64 MiB, 3
passes). Only a random salt is stored, so a backup opens on a machine that
has never seen the source. The threat is an offline attacker holding the
file, which is what rules out a fast KDF.
- `shared::backup_file` joins `snapshot::pack` to `backup::seal` and owns the
self-contained `.shxbak` format. Files are written to a temp sibling and
renamed, so an interrupted backup cannot replace a good file with a
truncated one.
- `profile_backup_create` / `_restore` / `_inspect` Tauri commands, with the
profile menu entries and a passphrase prompt.
Restore recovers the whole container in memory and only then calls
`snapshot::unpack`. `backup::open` streams authenticated frames and can only
detect truncation when it reaches the signed head, so restoring in place could
leave a profile that looks complete and was never verified. The P-OP drill
measured this: cutting the last 64 bytes still emitted all 3,145,744 plaintext
bytes before the error surfaced.
Both mutating commands take the same `begin_user_mutation` claim as
delete/clone, since a backup taken from a running profile is torn and a restore
under one corrupts it.
…ion (#19) Adds the /v2/ client tracked in #17: lease, chunked upload, signed commit and ranged download, so a sealed profile snapshot can move between devices. The manifest format now lives in shared::fleet_manifest and is used by both the signing client and the tests. Keeping a copy on each side is how a client ends up building records the server rejects, with nothing catching it until a real sync fails. Also adds GET /v2/server-identity. A signed manifest binds to the live server_instance_id and restore_epoch, and without an endpoint publishing them a client could only guess. Security fix, found by mutating the manifest builder and watching the commit still succeed: /v2/fleet/uploads/commit verified the manifest signature and then read container_sha256, profile_id, snapshot_id, fleet_id, base_version and key_generation from the unsigned request body. A caller with a valid token could attach a genuine manifest signed for one snapshot to a body describing different bytes and publish those instead. The handler now refuses any body that contradicts the signed record. The regression test publishes version 1 when the check is removed. fleet_client has no UI caller yet. That needs device enrollment, which the server has no endpoint for, so #17 stays open.
…artifacts (#20) v0.2.0 was tagged but published nothing: the validation job failed on `npm audit --audit-level=moderate` over a high-severity browserslist advisory, so no build or publish step ever ran. The release has zero assets and v0.1.29 stayed the latest installable version. Updates browserslist to 4.28.8 (lockfile only) and bumps the four version declarations the workflow requires to agree. Also reads the MCP contract version from package.json instead of a literal. Pinning it means every release bump fails that test for no reason, which teaches people to edit the assertion without reading what it guards. The v0.2.0 tag is not moved or altered.
The eight /v2/fleet handlers take tenant_id from the request body or path and never checked it against the caller. AuthUser carries no tenant, and five handlers bound the extractor as _user, so any authenticated user of the deployment could lease, upload to, commit to, abort, or download any tenant's profiles by naming its ids. The signed-record endpoints were not affected: a tenant issuer signature covers their fields, and they only echo the verified hash rather than acting on the body. The fleet endpoints carry no such signature, so the boundary has to be enforced in the handler. Every fleet handler now resolves the caller's v2 account in the named tenant and refuses when it has none. Where the body also names an actor - account_id on lease, author_account_id on commit - that value must equal the caller's own account, so one member cannot act as another. The regression test drives real HTTP with a second user that belongs to no tenant. Against the previous code it gets 201 Created.
* Add device enrollment and Team settings UI The fleet client had no way to obtain an identity: it took a signing key from its caller, and nothing generated, registered or stored one. That left issue #17 open regardless of the client existing, and left the module without a caller, so the linker dropped it from the release binary entirely. Server: add the two-step enrollment the schema already anticipated. The client commits to its key pair when it requests a challenge, the server stores only the nonce's hash, and registration requires a signature over the challenge. Possession is proved, not asserted. Launcher: generate the device signing key locally, prove possession, and store it in team.json alongside the server URL and token. The key is read back and compared before enrollment reports success, so a file that did not round-trip fails here rather than at the first publish. UI: a Team server card in Settings for the URL, tenant and token. The token field clears after saving and is never returned by team_status; the UI only learns whether one is set. Changing the server clears the enrollment, since a device id is meaningless to a server that did not issue it. The proof encoding lives in shared/src/enrollment_proof.rs rather than being written twice. The manifest format was previously defined on both sides and drifted; there is no reason to repeat that with a second signed record. Verified by driving a real server process from the launcher's own client: enrollment succeeds, and re-enrolling the same key is refused. The test fails loudly when the server binary is absent instead of skipping, and CI now builds the server so it cannot silently stop running. Also refuses a duplicate enrollment explicitly. The unique index already stopped it, but the client received a raw SQLite error naming tables and columns. * Generate key seeds through a cross-platform entropy source The launcher only depends on getrandom under cfg(windows), so calling it directly from enrollment broke the Linux build. shared depends on getrandom 0.4 unconditionally, so expose fill_random there and use it. The 0.2 dependency in shared is also Windows-only, which is why this goes through the 0.4 one.
Wire the fleet transfer client to the Launcher so profiles can be pushed to and pulled from a team server. The client shipped in v0.2.1 with no caller, so the linker dropped it: a release binary built from main contained the enrollment routes but not /v2/fleet/uploads. Sync uploads the same sealed container a local backup writes. The server stores ciphertext, a digest and a version; it holds no key and cannot open what it stores. The passphrase is therefore a shared secret between devices, which is weaker than the per-device key wrapping the v2 design calls for. docs/key-custody.md records why: the tenant root key machinery exists in shared/src/grants.rs but has no caller, and POST /v2/tenant-root-key-grants verifies a grant and then discards it, so a second device has no way to obtain the key. Enrollment now returns the account id it assigned. Fleet routes are account scoped and the client cannot infer that id; a device enrolled before this change reports can_sync false and must re-enroll rather than guess. Also audited every v1 route for the tenant-boundary flaw fixed in #21 and found none: v1 derives identity from the session rather than accepting it from the caller. docs/v1-authorization-audit.md records the one route that resembles the flaw and why it is safe. Tests: a real-process round trip pushes a 300 KB container through a live server and asserts the downloaded bytes match, plus that a push from a stale base version is refused. The running-profile boundary test now covers both sync commands; it caught them checking configuration before running state, which would have told a user to fix server settings when the real problem was a running browser.
Bump the Launcher, MCP and Tauri manifests to 0.2.2 and record the release in the changelog. This is the first build where device enrollment and profile sync are reachable from the shipped binary rather than present only as source.
The v0.2.2 dry run failed in validation before any build step, the same way v0.2.0 did: `npm audit --audit-level=moderate` runs against three workspaces and the mcp one carried a high-severity fast-uri advisory and a moderate qs one. Both arrive transitively through @modelcontextprotocol/sdk, so there is no direct dependency to bump. Patched releases exist upstream and `npm audit fix` resolves both by lockfile alone, leaving package.json untouched. This also syncs mcp/package-lock.json to 0.2.2; the version bump reached package.json but not the lockfile, which still read 0.1.29. MCP tests: 29 pass.
The route table omitted GET /audit and POST /me/password. Both are sound -- /audit calls require_admin, /me/password acts on the session user's own id -- but an audit that silently skips routes is worth less than one that lists them.
The grant endpoint verified a record and discarded it, so the grants table never held a row and a second device had no way to obtain the tenant root key. Grant fields are read from the signed field map rather than the request body, so a caller cannot present another issuer's signature and redirect the custody it authorises. The new collection route derives membership from the session rather than the path. This uncovered a silent failure in the replay ledger. Its record_table CHECK omitted the root key grant table, and because the claim uses INSERT OR IGNORE, the violation was indistinguishable from a duplicate key: zero rows affected, reported as 'already used'. Filing a grant looked like a replay of a record that had never been stored, and grants had no replay protection because no ledger row was written. Migration 0005 widens the constraint while preserving existing rows, and the claim now confirms the row exists before reporting a replay.
INSERT OR IGNORE reports a constraint violation exactly like a duplicate key, which is how the root key grant ledger bug stayed hidden. The operation ledger uses the same pattern: it claims with OR IGNORE, then reads the row it expects to exist. A dropped insert surfaced as an unexplained database error, so it now fails with a message naming the likely cause. grant_variant is the one CHECK-constrained column fed from a signed client record. It was already validated in Rust before the insert, but nothing proved it, so a regression test asserts an unknown variant is refused with 400 naming the field. The remaining OR IGNORE and OR REPLACE statements were reviewed: they are cookie upserts, where replace-on-conflict is intended, and unconditional Chromium meta seeding.
* Add root key generation lifecycle with FirstRootSelfGrant binding Grants named a root generation, but nothing recorded which generations existed or which one was in force. A grant could name a generation that was never created and wrap an unrelated key; the mismatch would only surface later as a backup nobody could open. Add v2_root_key_generations and bind grants to it. A grant must name an existing generation and wrap the key that generation committed to. A FirstRootSelfGrant is accepted only while the generation is PREPARING and only once per tenant, so custody cannot be appended to after the fact. Activation is a separate step from filing. Filing proves a grant was written, not that anyone can open it; activating on filing would let a tenant seal itself under a key it had already lost, which the server cannot help recover because it never sees a key. The tenant column active_root_generation stays the number a tenant advertises; the new table records the lifecycle the column cannot express. Activation writes both in one transaction so they cannot disagree. * Wait for the server to be ready in the new lifecycle tests The new tests called the server immediately after spawning it. On a fast machine the process was already listening; on CI it was not, so all four failed with connection refused. Every other test in the suite waits on health first.
Enrollment generated an HPKE keypair, registered the public half, and dropped the private half. A grant sealed to a device could therefore never be opened by it, which broke the custody chain at its last step. The seed is now persisted alongside the signing seed and cleared with it. A new team_collect_custody command fetches this device's grants and opens each one, reporting counts rather than the key. Devices enrolled before this report can_receive_custody: false and are told to re-enrol instead of failing silently. open_trk_with_info opens against the stored HPKE info bytes, since a collecting device holds the grant but not the scope that produced it. A test proves the two open paths agree and that altered info bytes still fail. Also isolates the enrollment tests: they shared one data directory, so adding a second test produced 'database is locked' rather than a useful failure.
The grant, generation, and fleet endpoints live in the server, but releases only ever shipped the Launcher and MCP. v0.2.3 published its headline custody feature as source only, which meant downloading the release did not get you a server that serves it. Each platform build now produces a server binary alongside the Launcher, and the release job fails if any of the three is missing rather than publishing a quietly incomplete set.
The publish job verifies that every platform's server binary made it into the release, but that job is skipped on a dry run, so a packaging regression would only be caught during a real publish. The per-platform build job now asserts the binary reached staging, which every dry run exercises.
…nifest (#37) * v2 update (152 Chrome) * Update runtime.json * Update package.json * upd ver * upd sdk runtime sync * fix bugs, add shardhelper, human type, human mouse, trash, webcamera spoof, trash, bookmarks and etc. * upd * up_readme * Update README.md * Port the team, backup and sync UI onto the v2 frontend The v2 rewrite replaced App.tsx, which held every team control. Rebuilds them as a team entity, a settings card and profile row actions: - Team settings card: server, token and tenant, with Generate for the tenant id so a fleet is not joined through a mistyped UUID, and a button to collect key custody. Says plainly when a device predates the stored HPKE seed and can never open a grant. - Encrypted backup and restore, and push/pull, back on the profile menu. Team entries appear only once the device is enrolled and able to sync. - Passphrase prompt as a store and host, matching the confirm dialog. - Restores safeUiError, lost in the rewrite: it scrubs the API token out of error text before a toast renders it. - Restores the startup toggles the v2 settings page dropped, and makes settings_save preserve any field the caller omits, so saving from a UI that does not know about a setting no longer silently turns it off. * Add the fleet key (FKEK) generation lifecycle and its grants Profile sync needs a per-fleet key, not the tenant root key. The root key authorises custody; the FKEK is what actually wraps snapshots, so the two have separate lifecycles and separate grants, per the frozen plan. Server: - migration 0007 adds fleet key generations and their device grants, with the same PREPARING/ACTIVE/RETIRED lifecycle as root generations and a unique index making the first grant of a generation the only self grant - migration 0008 widens the replay ledger CHECK to accept fleet grants - a fleet generation is anchored to the tenant's active root generation, so fleet custody cannot outlive the root that authorises it - grants are read from the signed container, never the request body, and must claim the fleet.key.receive capability and sit inside their own validity window Shared: - fleet_grants seals and opens the FKEK with HPKE, bound to the grant scope Verified: 102 server tests pass, including 5 new fleet cases. Disabling any of the four fleet invariants in turn makes the suite fail, including the capability check, which needed the test this commit adds. * Sync profiles with the fleet key instead of a shared passphrase A passphrase everyone types is a shared secret passed out of band. The fleet key arrives sealed to each device's own HPKE key, so no secret has to be agreed between people. - team_collect_custody now also collects fleet key grants and caches the opened keys in fleet-keys.json, kept out of team.json so a Settings save cannot rewrite key material - push seals under the active fleet key when the device holds one; pull tries every held generation, newest first, so snapshots pushed before a rotation still restore - the passphrase path remains for devices with no grant yet, and the UI stops asking once a fleet key is held - fleet containers carry their own magic, so feeding one to the passphrase path reports what is actually wrong instead of 'wrong passphrase' Verified with a real two-device round trip against a server process: device A seals under the fleet key, device B opens it with no passphrase, and a device holding a different key cannot open it at all. * Redact secrets in error toasts and explain disabled actions Two issues the audit found in the merged UI. Error toasts printed raw backend errors: 64 call sites passed the caught exception straight through, and those strings can carry a bearer token, a proxy password, key material or a passphrase. Redaction now happens inside toast.err rather than at each call site, so a new caller cannot forget, and the redactor also covers proxy credentials, long hex runs and passphrases. Checked against samples of all five leak shapes. Menu items that need a stopped profile were fully enabled and only failed after being clicked. They are now shown inert with the reason as a tooltip. Sync entries for a device enrolled before sync existed used to vanish from the menu entirely, leaving no hint that re-enrolling is the fix; they now appear with that explanation. * Fix three high-severity advisories in the v2 build dependencies browserslist, nanoid and postcss arrived with the upstream v2 toolchain. Resolved by lockfile update only; no API surface changed and the build and type check still pass. * Restore the e2e mock hook and the states its tests cover The v2 rewrite replaced the entry point, which silently dropped the import that loads src/e2e-mock.ts, so every mocked test hit a real IPC bridge that was not there. Restoring the hook exposed three real regressions the old suite had been guarding: - Browsers showed a blank screen when loading failed. The store tracked status and error but no component read them, so a failed backend call was indistinguishable from an empty account. It now announces loading, shows the error, and offers a retry; init() no longer short-circuits after a failure so the retry can actually re-run. - The empty state always claimed there were no profiles, even when a search or folder filter had simply matched nothing, and offered Create profile as the fix. Filtered-empty is now its own state with a clear-search action. - Icon-only row buttons had no accessible name, so they were unreachable by name for assistive tech and for tests. The mock also answers the read-only commands v2 added. It threw on anything unrecognised, which turned one unmocked list call into a blank screen. * Restore v2 UI states the e2e suite guards The v2 rewrite dropped behaviour the suite asserts. Restore it rather than rewriting the tests around the gaps: - context menu never opened on left click: the window dismiss listener was armed during the opening click, so it closed immediately - the All tab is the sentinel all, not a folder name, so an empty library claimed a folder was empty - folder registry was read at module-eval time, before bootstrap - launch failures only raised a toast, leaving no trace on the row - copy CDP and DevTools URL actions were missing, and the cdp field had been dropped from the process type - search box had no accessible name and no type=search - dead search boxes on pages that cannot filter, which also stole Cmd+K - row actions lost their responsive tier and accessible names * Restore the consent-gated Launcher updater in the v2 shell The v2 sidebar reduced updates to a link to the Releases page, dropping the in-app consent flow: download, signature verification, install and restart, plus the inline errors for a bad signature or an offline check. A launcher that runs browser profiles must not swap its own binary without an explicit click, so port the pill and dialog to the v2 widget layer instead of trusting the browser. Three ui-kit fixes this depends on: - Modal had no accessible name; label it from the title it renders. - Modal relied on transitionend to unmount, so a closed dialog could stay in the accessibility tree; add a timeout fallback. - ProgressBar dropped aria-label, leaving an unnamed progressbar role. * Restore startup, MCP readiness and save state in v2 Settings v2 Settings kept only the two startup switches, so the page no longer said whether the OS actually registered the entry, which of the four MCP prerequisites was missing, or whether edits were saved. A switch that silently fails to register is worse than no switch. Split the restored surface into StartupCard and McpCard, report each MCP prerequisite separately, and offer exactly one primary Codex action for the current state, stating that no config is changed automatically. The save bar now reports unsaved changes and a pending API restart, and it is the single Save settings control on the page. * Stop v2 profile edits dropping data and accepting bad names toStored rebuilt the document from the fingerprint library payload, so any field the editor does not render was erased on save. A profile with custom_fonts lost them by being opened and saved once. Carry the loaded document through the form and layer the library payload over it. The backend already owns the name rule via profile_validate_name, but the v2 frontend never called it, so a rejected name reached profile_save and the folder assignment. Validate first and show the reason beside the form rather than in a toast that scrolls away from the field. * Make v2 keyboard-operable and honest about proxy load state Select and ProxySelect opened on click only: no combobox role, no Arrow/Enter/Escape handling, so both were unreachable without a mouse. Give them the listbox contract and keyboard navigation, and derive the Cmd/Ctrl hint from detectHostOs instead of the deprecated navigator.platform. The proxy store latched status on failure, so a failed load could never be retried, and the page rendered neither the loading nor the error state. Make init retryable, bind the storeBus once, and surface loading, error and retry. Scope the empty panel to the filtered list so a search that matches nothing says so instead of claiming there are no proxies. * Bring the MCP package onto the 2.1.0 version line The version-parity gate compares the root, MCP, Tauri and Cargo versions. MCP was left on the 0.2.4 line when the custom build moved to 2.1.0, so the gate failed even though every suite passed. --------- Co-authored-by: Shkret Igor <kritos.biz@gmail.com> Co-authored-by: ProxyShard <shardxteam@gmail.com>
The upstream merge raised the SDK dependency ranges without regenerating the lockfile, so the release gate's npm ci refused to install. Regenerating it also drops the vulnerable transitive ip-address, clearing the moderate audit gate.
…running profiles (#38) * Restore the two v2 affordances and stop engine installs from evicting a running browser The v2 UI rewrite kept the backend commands but dropped the controls that reached them, so two capabilities were unreachable from the app. devtools_activate raises the page a profile is sitting on. A site parks a CAPTCHA or a 2FA prompt on a background tab and the operator never sees it; without the control the only recourse was hunting for the window by hand. A failure now says why, because silence here is indistinguishable from a click that did nothing. mcp_set_path adopts an MCP server already on disk. The card only offered Download, so anyone who already had a checkout was pushed into keeping a second copy and the two drifted apart. The engine installer was a separate fault, found while reproducing a setup screen that kept reappearing. Installing deletes the engine tree a running profile executes from: on Windows the delete fails partway with os error 32 and leaves a half-installed engine, so the setup screen returns on the next launch and the operator loses the session. The install now refuses while any profile is up and names them. The e2e mock short-circuited every plugin: command to null, which made the directory picker unmockable and the adopt path untestable; the picker is now allowed through to the switch. * Show one MCP section on the settings page instead of two The upstream merge left two cards headed "MCP server": one reporting readiness and one offering the download. Reading top to bottom the page states the server is ready and then, under an identical heading, offers to download it, which reads as a contradiction rather than two halves of one topic. Folding the actions into the status card puts the state and the things you can do about it under a single heading.
The Ctrl/Cmd+K hint sits in a narrow slot beside the search input. It used an ordinary space, so "Ctrl K" wrapped and rendered as a squashed two-line column. Use a non-breaking space and whitespace-nowrap. Adds a regression test that measures every shortcut hint against its own line height; verified to fail before the fix and pass after.
Bumps the launcher, MCP server, Tauri config, and Cargo crate to 2.1.1 so the published artefacts and the MCP package report the same version. Contents since 2.1.0: - Restore the dropped v2 affordances (devtools_activate, mcp_set_path) (#38) - Guard engine installs against running profiles, fixing the repeating "Setting up ShardX" dialog when a profile holds the engine files (#38) - Keep the Ctrl/Cmd+K search hint on one line (#39)
The log stopped at 2.0.1, so an operator updating to 2.1.1 saw a "latest" badge on a release two versions behind and no record of what changed. 2.1.0 covers team enrolment, encrypted profile backup and the pinned engine manifest. 2.1.1 covers the install guard for running profiles, adopting an existing MCP folder, raising a verification tab, and the search shortcut hint fix. Only additions: the 2.0.1 entries are byte-identical.
Ships the 2.1.0 and 2.1.1 patch log entries, which landed on main after the 2.1.1 tag was cut. Without this release the installed app badges 2.0.1 as the latest entry and records nothing about the two releases since. Bumps the launcher, MCP server, Tauri config and Cargo crate together so the release gate's version parity check passes.
Hermes Desktop is now the MCP host for ShardX. This doc carries the same operational content as docs/CODEX_SHARDX_HANDOFF.md with host-specific parts updated: config path, restart instructions, and the startup check command (hermes mcp test shardbrowser). Version references updated to 2.1.2. Paths use %USERPROFILE% / %LOCALAPPDATA% rather than a literal home directory so the doc is not tied to one machine. The Codex doc is kept, marked superseded, because .omx G0 baseline evidence records its SHA256.
Hermes Desktop is the MCP host now, so the QA runbook no longer says Codex: the registration example uses hermes mcp add --command/--args, and the restart step and prompt goal refer to the MCP host generically. The filename is unchanged on purpose: .omx G0 baseline evidence hashes this path, and AGENTS.md-adjacent docs link to it.
Hermes is the MCP host in use now, so it gets its own registration section before the Codex one, including the differing flag shape: Hermes takes --command/--args where Codex uses a bare -- separator. The Codex section stays: src-tauri/src/codex_mcp.rs and the Settings 'Check Codex registration' button are still shipping features, so the docs must keep matching the UI.
Settings could only inspect Codex, so a machine hosting this MCP server through Hermes read as unconfigured even when it worked. Add a parallel hermes_mcp module and surface its result in the same card. Status is read with `hermes config get mcp_servers.shardbrowser`, whose resolved output is parsed for the few shapes this entry can take. Reading the YAML file directly would have pulled a new crate in for one command. The card keeps its single primary action; the Hermes check and its add command live under Advanced, so neither host is presented as the only supported one.
The Settings card reports six distinct states, but the README described only how to add the entry, leaving no way to act on a reported failure. Map each state to its fix, and spell out the mismatches the check can report. Call out the token-in-config case specifically: the server still runs, so nothing looks broken, yet the token belongs in the Windows User environment rather than a file that gets copied or shared.
|
Closing — this one is not landable as a PR, and keeping it open is misleading. It is a The fork's own line (encrypted profile backup, the v2 control plane, fleet sync, HPKE grants, team server packaging) is effectively a separate product track rather than a change set upstream can absorb wholesale. If any individual piece is wanted here, it is far better as its own small PR against current
|
Syncs the v0.2.x work from
anhtahaylove/ShardBrowser(encrypted profile backup, v2 control plane, fleet sync).Opened as a cross-fork PR because the automation account has no push access here.
What this brings
Encrypted profile backup. Snapshots are sealed client-side; the server stores and moves ciphertext only. Canonical deterministic codec that rejects non-canonical input, envelope v2 alongside v1 (v1 archives still restore; a v1 reader refuses v2 rather than misreading it), and RFC 9180 HPKE tenant root-key grants using the OS CSPRNG.
v2 control plane. Additive migration, no v1 rewrite. Every mutation carries a signed authorization record bound to tenant, server instance, restore epoch, domain, issuer and validity window — a session token alone is not sufficient. Atomic idempotency ledger with exact response replay.
Fleet sync. Exclusive checkout with monotonic fencing tokens, chunked ciphertext upload through staging, atomic publication, ranged download. Streaming a 514 MB profile grows peak RSS by 1.8 MB (0.35% of payload); a buffering variant grows ~515 MB and fails the ceiling.
Verification
shared 108, server 84, src-tauri 48, MCP 29/29 (96 tools unchanged), Playwright 21. Reproduced from a clean checkout of tag
v0.2.0(23606e2). Server clippy-D warningsclean.Guards are mutation-tested rather than assumed: fleet 7/7 and G7 7/7 mutants killed. The G7 drill initially killed only 3/7 and was rewritten.
Please read before merging
This will not merge cleanly.
mainhere carries 5 commits the source repo does nothave (
v2 update (152 Chrome), runtime/SDK sync, version bumps), so the lines havediverged from merge base
39745d7. There are 14 conflicting files, concentrated insrc-tauri/sources, the lockfiles/manifests andruntime.json.This PR carries exactly 2 commits: #13 (v0.2.x implementation) and #14 (G7 drill and a
test-race fix).
I have not attempted a resolution — your 48 commits are yours to reconcile, and guessing at them from outside would be worse than leaving the conflicts visible.
Status
v0.2.0is published as a pre-release, source tag only, no installers. TheP-OPproduction-operator gate (named operator plus a completed recovery drill) is still blocked, so this is not cleared for production fleet use.v0.1.29remains the Latest release and the auto-update target.