Repository navigation
feat(gfs): per-user home folders under /Users with owner access control (#1168) - #1231
alfredolopez80 wants to merge 127 commits into
Conversation
…privileges The pre-existing invoker-rights trigger password_credential_generation_fence updates password_identifier_state on every users INSERT. The SECURITY DEFINER enrollment witness role now holds exactly that, and still no privilege on gfs_user_homes or the enrollment function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the user-home provisioner, state policy, naming and the background reconciler with backfill for #1168. Each user gets /Users/<name> on the configured GFS_HOME_DRIVE with an owner manage_acl grant; path, ancestor grant and ownership conflicts are recorded with id-only conflict detail for the operator, and an existing binding whose path drifted is kept and marked path_conflict. Home lifecycle events are audited in gfs_audit (user_home.* ops, subject user:<id>) plus one structured log line. auditMutation accepts a null resource for events that have no GFS resource yet and stores a NULL gfs_uri for them. Attempt accounting after a lost connection is best effort (#1203). Retirement here is a marked boundary that the retirement slice replaces. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gfs_resources.path_cache is nullable, and the operator conflict detail route returns it as is. Type the path as nullable and show "Not available" instead of an empty value. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explain unbound and bound home path conflicts on the user card with the existing fields, send Retry without a name for a bound home, and show the bound-home explanation for a 409 home_not_retryable answer. Remove the driveMismatch field, counter and notice: every environment uses drive main, so it never applies. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…usals A bound home root is protected from moves and renames, so the bound-home guidance now asks for a database correction by an administrator with cluster access instead of moving the home in the GFS browser. The grant panel explains a 409 ancestor_home_exposure: inherited access cannot be set on the drive root or /Users. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove driveMismatch, describe the ban on inheritable root and /Users entries for every caller, the bound-home Retry rule, operator restore through the grant panel, the bound-home database correction, today's 401 sign-out for retired users, and the agent-grant rollback gap. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Handle retirement cleanup and startup repair, enforce ancestor and owner grant rules, and share owner restoration logic with operator routes.
Add the Desktop Playwright journeys for #1168 (J1, J1b, J2, J3, J4, J6 and the refused-link, logout and retirement guards) with per-test users, a Control UI access panel page object for the operator steps of J6, and fixtures that read the home registry. Move the agent chat helpers out of the agent delegation spec so both specs share them. The retirement guard asserts today's behaviour: the next action signs the retired user out and no stale listing remains. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integrate slice S4 (operator home routes, Retry, conflict detail, authority selector, governed-root error mapping) onto S2 and S3. Conflicts were list unions in ci-public.yml and the password-recovery suite contract. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-1168 Integrate slice S5 Desktop: home navigation, states, Your access, protected owner row, foreign-agent rows (D25), share lowering (D26) and bounded revalidation (D36). Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * t3/desktop-gfs-share-controls: fix(desktop): constrain GFS access controls fix(desktop): restore bounded GFS revalidation test(desktop): cover inherited home subject profiles feat(desktop): add per-user GFS home navigation
Integrate slice S5 Desktop E2E journeys J1, J1b, J2, J3, J4, J6 and the guards. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * t3/gfs-user-homes-desktop: test(desktop): add per-user GFS home journeys
Integrate slice S6 Control UI: home inventory, user-card home block, Retry and Restore, conflict guidance (D28) and the ancestor-exposure message (D32). Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * t3/gfs-home-guidance: fix(control-ui): explain bound home recovery and root inheritance refusals feat(control-ui): guide operators through home path conflicts fix(control-ui): accept a missing cached path in home conflict details fix(control-ui): describe resolved home conflicts accurately feat(control-ui): manage user home folders
Integrate slice S7 runbook docs/deploy/gfs-user-homes.md. Refs #1168 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * t3/gfs-user-homes-runbook-1: docs(gfs): align the user home runbook with the review decisions docs(gfs): add per-user home operations runbook
|
@claude please run a closing review of this PR at head Context for the review:
Please post your verdict (approve / request changes) with any blocking findings by file:line. |
|
Claude finished @alfredolopez80's task in 10m 59s —— View job Closing review — head
|
| Item | Status at HEAD | Evidence |
|---|---|---|
| R2-L4 runbook startup-repair wording | ❌ Not resolved | docs/deploy/gfs-user-homes.md:253-258 still says repair "runs once… is not run on every tick", but gfsUserHomeReconciler.ts:117-123,202-221 retries failed rows every tick. Fold into the body-reconciliation pass. |
R2-L8 '' drive not fail-closed at IPC |
uriHandler.ts:596 correctly distinguishes omitted vs ''; but ipc.ts:774,781,791 still collapse payload?.drive ? … : undefined, so an explicit '' from the renderer defaults to main instead of rejecting. No live caller sends '' today — depth-only. |
|
R2-L2/L3 home-root hiding depends on home.resourceId |
isHomeRoot keys on ctrl.home?.resourceId (FilesPage.tsx:2613), which is null while getHome errors, so rename/delete affordances can appear on the home root; rename/delete catch paths push the raw governed_root_immutable string (:2548-2551,2562-2565). Move is terminal correctly (moveCommit.ts:347). Cosmetic — single-shot, no retry loop. |
|
CodeQL unused nav (2026-10-31) |
✅ Stale | 17 const nav declarations, all used at HEAD (likely fixed by the last two test-alignment commits). Test-only regardless. |
| CodeQL backslash-escape (migration test) | ✅ False positive | db.gfsUserFileActivityMigration.test.ts:111 char class includes \\; inputs are two hard-coded signatures with no backslashes. |
| CodeQL rate-limit / user-controlled-bypass / missing-await | ✅ False positives | userHomes.ts:379-390 applies rateLimitMiddleware; externalAuthority.ts only ever reduces authority; reconciler compares promise identity after await. |
Core backend re-confirmed intact at this head
- Governed roots (
db.ts:3256-3274):SECURITY DEFINER, pinnedsearch_path,REVOKE ALL FROM PUBLIC; blocks delete/rename/move/soft-delete of/Usersand bound home roots. - Owner-grant protection (
grants.ts:924-965,1276-1287): non-operator narrow/revoke (incl. inherit-drop and bulk) →home_owner_grant_protected. - Retirement (
userHomeProvisioner.ts:150-208): removes the user's own entries everywhere + every non-operator entry in the home tree; other subjects' entries on/and/Usersand all operator entries untouched. - Desktop row actions use each row's own drive (
FilesPage.tsx:2581,2737-2875,3010;FileExplorerTree/index.tsx:139,793,944).
Bottom line
The code is ready. No blocking defect in the ACL core, the identity reversal is a net security improvement, and Recent files is well-scoped. Two things should happen before this merges, neither of which touches the ACL core:
- Reconcile the PR body + runbook with the shipped design — the "Identity inside a home" section, migration
0132_gfs_entry_introducer, claims docs: OSS release doc-prep — scrub, MPL, accuracy corrections, coverage #12/ci(deps): bump docker/setup-qemu-action from 3 to 4 #19, and the R2-L4 startup-repair wording all describe code that isn't here. On a security PR this is a merge-gate, not a nit. - Run a certification pass that actually covers this head — the approved T2 (
a9e67a1) and APPROVED review (6af74ad0e) predate the identity reversal, Recent files, and the EvenDrive restructuring. The 5/5 + 10/10 E2E you cite onec9886834is good signal; a T2 on this exact head (or an explicit statement of why the T2 ata9e67a1still binds) would close the gap.
The three Low carry-overs (R2-L8, R2-L2/L3) and the two Recent-files notes are fine as follow-ups.
(This is a review summary, not a formal GitHub approval — I can't submit PR approvals.)
- Gather context: diff vs
origin/dev, delta since6af74ad0e - Validate round-2 items + still-open mediums
- Review focus areas: grant provenance/revocation, provisioning atomicity, root governance, authority separation
- Review new surfaces since last approval (identity reversal + Recent files)
- Check CodeQL alerts on this head
- Post verdict
Requiere a Review by UI/UX expert Miguel
Recent files (per-user EvenDrive file activity) is not part of issue #1168. It moves to its own stacked branch, feat/gfs-recent-files-1168, so this PR carries only the per-user homes. Reverts the control-api part of: - 1beff06 feat(control-api): record per-user EvenDrive file activity - 892d473 fix(test): register recent-files Postgres suite and clean CodeQL findings Removed: migration 0132_gfs_user_file_activity (it never reached a merged branch or a deployed database; images build only from dev), the gfs_user_file_activity table and its SECURITY DEFINER recorders, /external/gfs/recent, the recent-files reader, the activity rate-limit bucket, the direct-run attribution binding fields, the rpc-access additions, their tests, the CI suite registration, the password-recovery suite contract entry and the runtime-access profile row. 0131_gfs_user_homes and its legacy receipt are untouched here. orphans checked: recentFiles, userFileActivity, recordUserFileActivity, readableResourceIds, directRunAttributionBinding, externalGfsRateLimit; removed: everything listed above, nothing left without a caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts 684c6bb (relay recent files and the open intent): the /me/gfs/recent relay and the intent=open forwarding on downloads leave with Recent files, which moves to feat/gfs-recent-files-1168. orphans checked: recent relay route, intent forwarding; removed: both and their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts the user and agent file-activity recording that only serves Recent files, which moves to feat/gfs-recent-files-1168: - 5c4e1fb feat(gfs-controller): record user and agent file activity - 03bce78 fix(gfs-controller): record agent writes and copies on the publication transaction Serve, copy, upload finalizer and the publication transaction return to their previous behaviour: no activity write, so no rollback on a failed activity write. orphans checked: agentActivity (attributionRunOf, AGENT_ACTIVITY_LOG_INTERVAL_MS), recordUserFileActivity, recordAgentFileActivity; removed: all of them and their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts the gfs-attribution-run header on Desktop chat GFS content calls, which only serves Recent files agent attribution: - bebbba1 feat(mcp-host): send the attribution run on Desktop chat GFS content calls - d4d0261 test(mcp-host): avoid boundary false positive in GFS attribution env name orphans checked: gfsClient attribution header, nativeToolRegistry and taskExecutor run plumbing; removed: all of them and their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… follow-up PR (#1168) Recent files and the EvenDrive view restructuring built around it move to feat/gfs-recent-files-1168. Desktop returns to its pre-Recent Files UX on top of the dev merge b5335a7. Reverts: - f215b96 feat(desktop): show Recent files on the EvenDrive overview - b122ca9 test(desktop): cover Recent files in the EvenDrive journeys - a9e1979 test(desktop): require one agent for recent files journey - 57aa034 test(desktop): assert recent file order by accessible names - f424824 feat(desktop): add a focused recent files view - 563f3df refactor(desktop): unify EvenDrive views and recent navigation - 91b6628 refactor(desktop): adapt EvenDrive overview to available space - eb56bbe fix(desktop): preserve file navigation during overview changes Also removes the Recent-files adaptations made while resolving the dev merge b5335a7 (recordOpen props on previews, the gfsRecentFiles query key, the 'recent' query invalidation after a preview download, the listRecent client and its test). Kept: the AbortSignal bridge fix f55922d (registerAbort), expressed against the pre-Recent download signature without recordOpen; the journey alignment with dev's EvenDrive UI (10e1cfe, ec98868); the folder-menu page object (ebaf0a3, fd4ea0a); requiredAdminPassword (3197142). Journeys J7a and J7b leave with Recent files. orphans checked: GfsRecentFilesCard, useRecentOverviewLimit, gfsListingGrid, listRecent, recordOpen, gfs:listRecent IPC, DesktopFilesNav recent helpers, DesktopResourceView.deleteResource, electronDialogs; removed: all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f55922d fixed Stop on a folder ZIP export: AbortSignal loses its methods across contextBridge, so the renderer now registers an abort callback. Its only end-to-end proof lived in journey J7b, which left with Recent files. Unit tests mock the bridge and cannot see that failure. J1 now uploads 24 files into a folder of the owner home, starts a ZIP export, presses Stop, and asserts the progress disappears and no archive is written; the same folder then exports in full (Downloaded ... (24 files), archive on disk with size > 0), which is the liveness witness for the negative assertion. The OS save dialog is the only stub (external boundary). DesktopResourceView.downloadZip and helpers/electronDialogs.stubSaveDialog return for this step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the branch up to date with 1,079 dev commits. Conflict resolutions: - Migrations: dev now ends at 0138_legacy_password_security_epoch_backfill and runs every migration after LAST_DEV_MIGRATION_VERSION through applyPendingPr1Migrations. The per-user homes migration is renumbered to 0139_gfs_user_homes, registered last in CONTROL_API_MIGRATIONS and in PR1_MIGRATION_VERSIONS (as migrationRunner.ts requires for any later migration), and loses its legacy receipt 0130_gfs_user_homes: neither 0130 nor 0131_gfs_user_homes reached a merged branch or a deployed database (images build only from dev). db.gfsUserHomesMigration.test.ts drops the two legacy-receipt tests; dev's db.migrationsOrder.test.ts now expects the homes migration right after 0138 and last. - routes/gfs/grants.ts: the owner-grant restore path keeps writeGrants and takes dev's ORDER BY input.subject_type, input.subject_id on the grant upsert. - routes/external/gfs.ts: both imports (validateExternalGfsAuthoritySelector, signCurrentExternalGfsUserToken). - Runtime access: dev's contract compares the control-api profile with the workflow-recipes profile, so gfs_user_homes is added to workflow-recipes-runtime-access-profiles.tsv as none (the migration grants it only to control_api_runtime), and both shell contract tests keep the gfs_user_homes upsert sentinel in dev's sentinel lists. - E2E: the password pace lives in helpers/passwordPace.ts on this branch; it takes dev's 13 s pace for the five-attempts-per-source-minute limit. App.deepLinks.test.tsx takes dev's version, which includes this branch's wait. - Package versions resolved to the higher side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d session contract Follow-up to the dev merge 1c56bb0: - db.initDb.test.ts: initDb now runs one more runner transaction because 0139_gfs_user_homes is a PR1_MIGRATION_VERSIONS entry; the test expects five BEGIN/COMMIT pairs and the homes receipt after 0138. - routes.externalGfs.test.ts: the home-owner grant marker test switched callers with the old verifyExternalSessionToken return shape; dev authenticates through authenticateExternalUserSession, so it now resolves { status: authenticated, contract: v1, claims } like the rest of the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dev merge Follow-up to the dev merge 1c56bb0: - routes.externalGfsAuthority.test.ts mocks the session middleware, so the user-token mint now goes through dev's signCurrentExternalGfsUserToken; the suite mocks it the way routes.externalGfs.test.ts does (delegating to the signGfsToken mock with principalType user). - migrationExecutionPolicy.test.ts: the runner list now ends with 0139_gfs_user_homes; the alias test marks it applied so it still proves that only 0138 is recorded through its alias. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings 21 dev commits (HCC NetworkPolicy pass fix, #1339): host-context-controller, mcp-host, control-ui, CI steps and the minikube mcp-host ConfigMap. No new control-api migration; 0139_gfs_user_homes stays last. Only conflict: the control-ui version line, resolved to the higher value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release version check (validate-release-version-bumps.mjs, run by CI on the PR merge ref against dev) failed on 70619ff: external-rest-api and profile-ui differ from dev but kept dev's versions, because the dev merges resolved their version lines to dev's higher value without bumping past it. external-rest-api 0.1.181 -> 0.1.182 (and releaseManifest), profile-ui 0.1.205 -> 0.1.206 (its only change from dev is the untracked next-env.d.ts, 6d09651, #733). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R2-L8. The GFS IPC handlers turned an explicit '' drive into an omitted one (`payload?.drive ? ... : undefined`), so the service applied the default drive `main`; a whitespace-only drive reached the client as ''. The dev merge b5335a7 had also replaced the client's requireDrive with `drive ?? DEFAULT_DRIVE`, so an explicit '' was sent as `drive=` and the server default turned it into `main` as well. - ipc.ts: one helper, optionalDriveArg, for all 22 channels that take a drive. Omitted or null stays omitted; a blank string throws drive_required before the service is called. - uriHandler.ts: requireDrive/resolveDrive for all 12 GfsClient calls that send a drive. Omitted keeps the merge's main default (lists keep omitting the query); a blank string throws drive_required before any request. - Tests: table-driven over the 22 IPC channels and the 12 client calls, each with a named-drive liveness witness. The listChildren test the merge had changed to accept '' expects drive_required again. Removing the blank check fails 35 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R2-L4. The runbook still described the planned repair: one run per process that "is not run on every tick", conditional on a T1 cost gate. gfsUserHomeReconciler.ts runs one full pass in the first tick that holds the reconciler lock and then retries only the failed rows on every tick, logging user_home_startup_repair_failed once a minute for a row that keeps failing. The cost bound is enforced by gfs.userHomeStartupCost.realPostgres.integration.test.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no open folder and no home drive, the Files controller's drive is ''. createFolder and createFileFromPath already sent `DRIVE || undefined`, but createFile, replaceFile and replaceFileFromPath sent the '' as is. The IPC used to collapse it to "omitted"; since d8d59a4 it refuses a blank drive, so these three calls would fail with drive_required where the others use the default drive. They now omit the drive like their siblings. The new controller test fails when `|| undefined` is removed from the three calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/dev: fix(workflow-recipes): never report a pod that is being deleted as ready test(workflow-recipes): return the PodReadiness kind from readiness mocks fix(workflow-recipes): fail the pass when patchStatus cannot read the live recipe fix(workflow-recipes): give no readiness verdict while the mcp-host pod is absent fix(workflow-recipes): start the mcp-host readiness window at the later of run or pod start docs(crds): state that backoffSeconds retry waits are capped at 300 s test(workflow-recipes): probe healthCheck against the real runner server, drop the abort pin test(workflow-recipes): cover resume with a snippet step persisted as running docs(crds): state the maxRetries backoff formula fix(workflow-recipes): release the snippet-runner health response after reading its status fix(workflow-recipes): wait for the snippet runner only when a remaining step needs it chore(workflow-recipes): forward workflow step tuning env to recipe mcp-host pods fix(workflow-recipes): wait for the snippet runner before dispatching snippet steps
Summary
Every user of an Evenfire cluster now gets a personal GFS folder,
/Users/<name>, on the configured drive (main). The user owns it through an inheritable grant ofread, write, delete, share, manage_acl. From Desktop, users manage access to their home and anything inside it, for people, teams and their own agents. Operators browse homes as ordinary/Usersfolders in Control UI EvenDrive and recover a home from the member's Contact tab. Grant and share lists inside a home follow dev's rule for every EvenDrive folder: rows carry no names or email addresses, and Desktop names a subject only from the viewer's team directory and own agents.Closes #1168. Fixes #1209, #1212, #733 and #1228 (see "Fixes included").
Recent files for EvenDrive, which grew on this branch, is not part of this PR. It is parked in the closed, stacked PR #1486 (see "Recent files moved out").
What changes
control-api
Migration
0139_gfs_user_homes, the next free version after dev's0138. It never reached a merged PR or a deployed database under its earlier names, so it carries no legacy alias. It is listed inPR1_MIGRATION_VERSIONS, so dev's runner applies it in its own bounded transaction. It adds:gfs_user_homesregistry;usersinsert;/Usersor a bound home root (409 governed_root_immutable).Home provisioning. One reconciler does enrollment, backfill of existing users and provisioning, every 60 s, with no CLI, Job or feature flag. Each home is installed in one transaction: folder plus owner grant. An existing folder at the computed path is never adopted; it becomes
path_conflict. Name collisions between users get a suffix; reserved names are refused.Conflicts (
path_conflict,ancestor_grant_conflict,ownership_conflict) are stored as ids only. The operator detail route resolves them to paths and subjects; ordinary users never see that detail.Operator routes:
GET /api/v1/gfs/user-homes: inventory with state counts and cursor paging;GET /api/v1/gfs/user-homes/:userId: detail;POST …/retry:202; a bound home cannot change name,409 home_not_retryable;POST …/restore-owner.Owner grant protection. Only an operator can revoke or narrow a home owner grant; anyone else gets
403 home_owner_grant_protected. An operator grant update that restores all five owner bits with inheritance also restores the home. It shares one implementation withrestore-owner.No grant or share of any kind for a user, team, agent or context subject on the drive root or
/Users(409 ancestor_home_exposure), operators included. Operator-subject entries are unaffected.Retirement and hard delete keep the data, macOS "keep home folder" style. They remove:
On
/and/Usersthey remove only the retired user's own entries; other subjects' entries there are untouched. An audit row and a structured log line with counts are always written; an admin-driven retirement records the admin id.Startup repair. Each process runs one full pass in its first reconciler tick that holds the lock. It cleans homes of users retired or deleted during a rollback window and marks narrowed owner grants as
ownership_conflict. Rows that fail are retried on each later tick until they succeed.Home lifecycle audit in the existing hash-chained
gfs_audit(user_home.*). An event with no resource storesgfs_uri = NULL.Desktop GFS requests always use user authority (
X-Evenfire-Gfs-Authority: user), even with an operator link. The selector can only reduce authority.Grant and share lists keep dev's row shape, with no subject profile. The owner grant is flagged
homeOwner: true.gfs-controller: maps the governed-root refusal to its error. No authorization change.
Desktop
read/writeonly).drive_required) by every GFS IPC channel and by the GFS client before any request; an omitted drive still uses the defaultmain.Control UI (operators only)
/Users/<name>folders in EvenDrive, where the existing ACL panel applies. There is no separate home inventory page or deleted-user page; the inventory API remains for the rollout procedure.home_not_foundfrom Refresh or a poll clears the previous home, conflict and pending line and shows "No home folder yet";ancestor_home_exposurein the grant panel, and forgoverned_root_immutablein EvenDrive.Docs: runbook for operator home management, startup repair and rollback; drive
mainmust stay the HCC default.Recent files moved out
Recent files (per-user file activity, agent attribution, the EvenDrive overview card and view, and the EvenDrive restructuring built on it) is not in the #1168 scope. It was removed from this branch before the dev merge and parked, with its Desktop journeys J7a and J7b, in PR #1486, which is stacked on this branch and closed without merge. Its migration will need a new number after dev's latest migration when it is revived.
The folder ZIP cancellation fix that came with it stays here, and its E2E step moved into J1.
Design decisions
manage_acland B shares with C, revoking B does not revoke C. The owner sees C's entry and removes it. fix(control-api): revoke delegated GFS grants when the grantor loses authority #761 stays a separate issue. Moving an item into a home makes it inherit the owner's grant, includingmanage_aclandshare; a Desktop Editor already holds those bits, so only a narrower operator-created grant gains them.manage_acl.mainonly (D16, D21, D40). No evenfire-infra change:GFS_HOME_DRIVE: mainlives in the public base ConfigMap and reaches dev and prod unchanged;/Usersis created by the provisioner. The governed-root guard and theancestor_home_exposurecheck are drive-agnostic: they protect the root and a top-levelUsersfolder on whichever drive a request targets. The core serves one drive, so this is the home drive.Deviations from the #1168 text
/Usersand every home from Control UI with operator authorityWhy one PR
The side fixes below (#733, #1212, #1228 and the branch-profile forward name) each stopped this branch's own
make minikube-t2or E2E lanes. Separate PRs would have had to be stacked under this one and certified with it. Each is its own commit and is listed here, so it can be read apart from the feature.Fixes included
rolecolumn. The same column qualification also landed in dev through fix: support 40 successful password logins #1197; both sides merged cleanly.control-ui/next-env.d.tsandprofile-ui/next-env.d.tsare generated bynext build. They were tracked, so T2 dirtied its own worktree in T0. They are now ignored and untracked.pre-gate-sync(pf-all-stack.sh) recorded the approval-reader forward asworkflow-approval-request-reader, whilebranch-profile.shusesworkflow-approval-reader.branch-profile-stop-pfrefused the record one tool wrote and the other could not adopt (PORT_FORWARD_OWNERSHIP_ERROR). Both now useworkflow-approval-reader; stop-pf still clears a record left under the old name. The lifecycle test checks that every forward namepf-all-stack.shwrites is one stop-pf verifies (mutation proven).E2E harness changes
helpers/passwordPace.ts): Control API admits one password verification every 7.5 s server-wide and five attempts per source in each clock minute./Userskeeps the home of every retired user, so on a profile that has run the lanes many times a new test home sits past the first page. The operator panels in both lanes now click "Load more" until the folder is listed, as an operator would. The Desktop spec's Control UI contexts bound actions and navigations to one minute. Before, Playwright Test's default of 0 let a missing row wait for the whole 35-minute journey timeout.HUMAN_E2E_RECORDED=1, each page-object step pauses, the Control UI browser runs headed withslowMo, and every browser and Desktop window is recorded. Without the variable every helper is a no-op, so certification runs keep their timing. Outside recorded runs, the Control UI Playwright config keeps a trace and a video only for failed tests (retain-on-failure), as the Desktop lane already does; before, it never kept them, becauseon-first-retrywithretries: 0records nothing.Review round 1
Findings from the round-1 reviews, after independent verification against the code:
clerum.gfs.list({ resourceId })again lets the server default the drive, as documented.governed_root_immutableis no longer treated as a name conflict.gfs:createShareIPC rejects an empty permission list.ancestor_home_exposureandgoverned_root_immutableshow readable messages, and a governed-root move is not retried under numbered names.José García→jose-garcia)./and/Users, two owner-grant bypass shapes and inventory paging.gfs_user_homesrow.adminDeleteUserrecords retirement audits without the admin id #1237 (adminDeleteUseraudit actor), Control UI does not mark the home owner grant in the operator grant panel #1238 (Control UI owner-grant marker).Review round 2
Changes after the round-1 review:
home_not_foundclears the field; the recovery action stays under the cause; focus moves to the status line after a rename; the field mounts only after the member context loads. R1-L9 is resolved by removing the page-level lookup.827f47b8edropssubjectProfile, its reader and theintroduced_bywriters; the introducer migration never left this branch and is deleted.76dbf08e8makes Desktop name subjects from the team directory, as on dev. Lists in a home now return dev's row shape, so a home owner cannot read a stranger's name or email by granting to their UUID.Changes after review round 2
origin/dev. The home migration is renumbered0139_gfs_user_homesand registered in dev's migration runner; theworkflow-recipesruntime access profile listsgfs_user_homes; tests follow dev's migration runner and session contract. Package versions are above dev's.d8d59a43f): a blank drive from the renderer no longer falls through to the default drive. Every GFS IPC channel that takes a drive, and every GFS client call that sends one, refuses a blank value withdrive_required; an omitted drive keeps themaindefault. The client check had been lost in the earlier dev mergeb5335a7d5.1881f74e7makes file create and replace omit the drive when no home drive is known, as folder create already did, so they keep usingmaininstead of sending a blank drive.c3e35f94d): the runbook describes the shipped startup repair: one full pass per process, then a retry of the failed rows on every tick, withuser_home_startup_repair_failedlogged once a minute for a row that keeps failing.Not in this PR
Follow-ups
gfs.liststill readsdrive: ''as omitted and listsmain, unlike the IPC and GFS client after R2-L8. Tracked in Plugin SDK gfs.list treats an empty drive as omitted #1508.gfs-upload-v2breadcrumb,gfs-runtime-grants"Manage access",gfs-user-journey"Directories" → "Files",gfs-operator-journeygrant rows, andusers-roles-adminlocalStorage token. Not filed yet.gfs-agent-delegationtest 1 (gfs_downloadversusgfs_read) is unverified on dev.Rollout and rollback
0139starts writingpendingrows. Nothing provisions until the new control-api starts. The governed-root guard is active from here.pendingfalling to 0.conflictrows are reviewed in the member's Contact tab; the inventory API still lists them by state.X-Evenfire-Gfs-Authority, so during that window the linked initial admin using the new Desktop has operator authority in Files again. Against an older control-api the new Desktop shows My home as unavailable; Shared with me and its row actions keep working with the drive each row reports.Testing
Certification status. The earlier
MINIKUBE_T2_PASSon269f8c694and the E2E lanes listed under "Earlier runs" predate the identity removal, the Recent files split and both dev merges, so they do not certify this head. A blind review, then T2 and the E2E lanes on the final head, are pending and will be added here.Current head (
1881f74e7):3d74efbf1, the head before the R2-L8 and R2-L4 commits.1881f74e7: 478 files / 5985 tests and the build pass. Removing the blank-drive check fails the 35 new IPC and client tests; removing the create/replace drive omission fails its controller test.After the first dev merge (
8cbcb7ba6; the second merge left the control-api, gfs-controller, external-rest-api and desktop-app trees unchanged):scripts/minikube/t0.sh:T0_EXIT=0. control-api 634 files / 8913 tests, control-ui 264 / 3147, desktop-app 478 / 5948, external-rest-api 55 / 561, gfs-controller 46 / 652; builds pass.EXIT=0under/bin/bash3.2 and bash 5.Earlier runs (before the identity removal and the dev merges):
269f8c694MINIKUBE_T2_PASS,T2_EXIT=0, full reconcile; T1 real PostgreSQL 101/101 files, 855 testsgfs-user-home.test.ts269f8c694gfs-user-home.spec.ts43edf39eb269f8c694CodeQL: 7 of the 9 alerts raised on this branch were dismissed as false positives, each with its reason:
js/missing-rate-limitingonuserHomes.ts. The routes apply the repo'srateLimitMiddlewareand a test proves the 429.js/user-controlled-bypassonexternalAuthority.ts:179. The header can only reduce authority, and both branches assert that the user is active.js/missing-awaiton the reconciler. The code compares promise identity after awaiting.The other 2 alerts were the unused code fixed in
9612dcb53.Known flake, not in this PR: issue #757. The Desktop Playwright preflight restarts
chatllmwhen it re-applies the Airtable allowlist after a T2 reconcile. On its next resync, HCC rotates the runtime token (deployment_restarted_after_secret_issued) and rolls the Host again, which cancels an in-flight agent turn assystem_shutdown.Desktop (
desktop-app/test/e2e-playwright/gfs-user-home.test.ts, 8 journeys):Control UI (
control-ui/e2e/gfs-user-home.spec.ts, 5 journeys):/Userscannot be shared with a member (409 and the operator-only message).Verified claims
Each claim below was checked against the code at this head:
/Users/<name>, created automatically (enrollment, reconciler, backfill), with the five-bit inheritable owner grant. An existing folder at the path is never adopted.granted_byrecords the last assigner;gfs_auditkeeps the history./or/Users(409 ancestor_home_exposure). Agent shares are refused earlier with403 share_to_agent_forbidden./Usersand bound home roots cannot be deleted, renamed or moved by anyone./and/Users, and every non-operator entry in the home tree. Other subjects' entries on/and/Usersare untouched.gfs_audit.contextrows.home_not_foundhandling, focus after rename, and mount after the member context loads. There is no separate inventory or deleted-user page.main.Checklist
🤖 Generated with Claude Code