Skip to content

feat(gfs): per-user home folders under /Users with owner access control (#1168) - #1231

Open
alfredolopez80 wants to merge 127 commits into
devfrom
feat/gfs-user-homes-1168
Open

alfredolopez80 wants to merge 127 commits into
devfrom
feat/gfs-user-homes-1168

Conversation

@alfredolopez80

@alfredolopez80 alfredolopez80 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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 of read, 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 /Users folders 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's 0138. It never reached a merged PR or a deployed database under its earlier names, so it carries no legacy alias. It is listed in PR1_MIGRATION_VERSIONS, so dev's runner applies it in its own bounded transaction. It adds:

    • the gfs_user_homes registry;
    • an enrollment trigger on users insert;
    • a governed-root trigger: nobody can delete, rename or move /Users or 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 with restore-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:

    • every non-operator grant and share in the home tree;
    • the user's own grants and shares anywhere;
    • grants of the user's first-party agents that no other active user or team holds.

    On / and /Users they 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 stores gfs_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

  • "My home" in Files, with conflict states shown honestly. "Shared with me" discovery works when the home is pending, missing or failing.
  • Sharing from the home or anything inside it, to users and teams from the team directory and to the user's own agents (agents get read/write only).
  • Access lists name subjects from the viewer's team directory and own agents, as on dev. The home owner's "Owner" row is kept in the owner's own home and has no role or remove control for any viewer. Another user's agent shows its role as text and keeps Remove. An Editor-strength share can be lowered to Read.
  • After a share write, the Files page decides what to refresh. The controller only forwards the write, as on dev; a result the page must ignore no longer triggers extra home, grant and affordance reads. Lowering a share refreshes grants and shares from the page, like a role change.
  • The "Your access" line is removed from the home view until the UI/UX team decides how Desktop should present a user's access to a folder. The journeys prove write access through the New folder action and the active breadcrumb.
  • A blank drive is refused (drive_required) by every GFS IPC channel and by the GFS client before any request; an omitted drive still uses the default main.
  • Cancelling a folder ZIP download works across the isolated preload bridge: the renderer's abort reaches the main process.

Control UI (operators only)

  • Homes are browsed as ordinary /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.
  • The Contact tab of a member has one Home folder field, styled like its sibling fields:
    • the path or a short state line; for a conflict, the cause, the facts needed to fix it and the recovery action: Retry, Retry with another name… (a dialog, only for a home that was never bound) or Restore owner access;
    • the recovery action sits under the cause, before the longer guidance and the conflict details;
    • silent polling of a pending home. Refresh that finds a pending home starts a new polling window, so "Being created…" is shown only while the field is checking;
    • a home_not_found from Refresh or a poll clears the previous home, conflict and pending line and shows "No home folder yet";
    • after an accepted "Retry with another name", focus moves to the status line, not the document body;
    • the field mounts only for the member whose context loaded, so a member 404 never reads a home.
  • A readable message for ancestor_home_exposure in the grant panel, and for governed_root_immutable in EvenDrive.

Docs: runbook for operator home management, startup repair and rollback; drive main must 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

  • Unix/macOS permission model, no grantor cascade (D14). A permission belongs to the resource, not to whoever granted it. If A gives B manage_acl and 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, including manage_acl and share; a Desktop Editor already holds those bits, so only a narrower operator-created grant gains them.
  • The owner controls every entry in their tree (D9), including entries added by someone they gave manage_acl.
  • Access lists follow dev's identity rule. Grant and share rows carry no names or email addresses in any folder, homes included. Desktop names subjects from the viewer's team directory and own agents and hides other user and team rows behind the existing notice; an operator removes out-of-team entries in Control UI.
  • Desktop Files always uses user authority (D3). The initial admin, the only possible Desktop operator link, is an ordinary user in Desktop Files. Restoring the Desktop operator root view is Desktop GFS operator root view silently dropped by the PR #356 merge #814, out of scope.
  • Control UI is operator-only (D11). Users manage folders and permissions in Desktop.
  • Homes on drive main only (D16, D21, D40). No evenfire-infra change: GFS_HOME_DRIVE: main lives in the public base ConfigMap and reaches dev and prod unchanged; /Users is created by the provisioner. The governed-root guard and the ancestor_home_exposure check are drive-agnostic: they protect the root and a top-level Users folder on whichever drive a request targets. The core serves one drive, so this is the home drive.

Deviations from the #1168 text

Issue text This PR Evidence
"Control UI needs a user-scoped path for this journey" Control UI is operator-only (D11) J6, J7, J8
The user completes home → share → revoke in both applications The user completes it in Desktop; operators manage /Users and every home from Control UI with operator authority J1, J1b, J6
A grant created in Control UI is visible and manageable in Desktop, and vice versa Kept, operator ↔ user J6 both directions

Why one PR

The side fixes below (#733, #1212, #1228 and the branch-profile forward name) each stopped this branch's own make minikube-t2 or 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

E2E harness changes

  • Desktop logins in the E2E lanes follow dev's pace of one attempt every 13 s (helpers/passwordPace.ts): Control API admits one password verification every 7.5 s server-wide and five attempts per source in each clock minute.
  • The Control UI EvenDrive browser lists a folder up to 100 items and shows "Load more". /Users keeps 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.
  • Recorded demo runs: with HUMAN_E2E_RECORDED=1, each page-object step pauses, the Control UI browser runs headed with slowMo, 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, because on-first-retry with retries: 0 records nothing.

Review round 1

Findings from the round-1 reviews, after independent verification against the code:

  • Desktop:
    • Root and sidebar row actions use the row's own drive, so they work while the home is pending or missing.
    • clerum.gfs.list({ resourceId }) again lets the server default the drive, as documented.
    • The home root offers no rename, move or delete, and governed_root_immutable is no longer treated as a name conflict.
    • The gfs:createShare IPC rejects an empty permission list.
  • Control UI:
    • A bound home's folder name is read-only, so Retry no longer leads to the "database correction" message.
    • ancestor_home_exposure and governed_root_immutable show readable messages, and a governed-root move is not retried under numbered names.
    • R1-L9 (home lookup on the user page): resolved by removing the page-level lookup.
    • The home block polling has tests for unmount and for poll errors.
  • control-api:
    • Share DELETE reads the drive, takes the drive structure lock and re-reads the row, as grant DELETE does.
    • Home names keep base letters (José García → jose-garcia).
    • Admin-driven retirement records the admin id.
    • Startup repair retries only the rows that failed.
    • New real-Postgres tests cover retirement on / and /Users, two owner-grant bypass shapes and inventory paging.
  • Scripts and docs:
    • The runtime access contract checks require the gfs_user_homes row.
    • The runbook covers the rollback header skew, the backfill pace, moves into a home and the drive-agnostic guards.
  • Follow-up issues: Derive Desktop and Control UI GFS test fixtures from the control-api producers #1235 (producer-derived test fixtures), ACL writes serialize on the drive structure lock before authorization #1236 (ACL write lock granularity), adminDeleteUser records retirement audits without the admin id #1237 (adminDeleteUser audit 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:

  • Control UI: homes are browsed in EvenDrive, with no separate inventory or deleted-user pages. The member Contact tab keeps one Home folder field with the conflict cause, the facts needed to fix it and the recovery action. Refresh and polling no longer leave a stale "Being created…" line; home_not_found clears 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.
  • Desktop: the Files page owns refresh after a share write, and a denied draft no longer causes extra reads for a result the page ignores. The "Your access" line is removed pending the UI/UX design; the journeys prove write access through the New folder action and the active breadcrumb.
  • control-api and Desktop (R1-M1): the subject profile on home grant and share lists is removed. 827f47b8e drops subjectProfile, its reader and the introduced_by writers; the introducer migration never left this branch and is deleted. 76dbf08e8 makes 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

  • Scope: Recent files and the EvenDrive restructuring built on it moved to the closed, stacked PR feat(gfs): EvenDrive Recent files, parked on top of #1231 #1486 (see "Recent files moved out").
  • dev merges: two merges of origin/dev. The home migration is renumbered 0139_gfs_user_homes and registered in dev's migration runner; the workflow-recipes runtime access profile lists gfs_user_homes; tests follow dev's migration runner and session contract. Package versions are above dev's.
  • R2-L8 (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 with drive_required; an omitted drive keeps the main default. The client check had been lost in the earlier dev merge b5335a7d5. 1881f74e7 makes file create and replace omit the drive when no home drive is known, as folder create already did, so they keep using main instead of sending a blank drive.
  • R2-L4 (c3e35f94d): the runbook describes the shipped startup repair: one full pass per process, then a retry of the failed rows on every tick, with user_home_startup_repair_failed logged once a minute for a row that keeps failing.
  • J1: the Desktop journey now cancels a 24-file folder ZIP download, checks that no file is written, and downloads it again.

Not in this PR

Follow-ups

  • Write-side subject validation for grants and shares. Non-operator callers can still name any well-formed user or team UUID, including one outside their teams. Tracked in GFS grant/share writes accept any user/team UUID as subject for non-operator callers #1253.
  • Blank drive in the plugin SDK. gfs.list still reads drive: '' as omitted and lists main, unlike the IPC and GFS client after R2-L8. Tracked in Plugin SDK gfs.list treats an empty drive as omitted #1508.
  • Desktop access design for shared folders. A UI/UX design for showing read-only versus editor access on shared folders is requested from the UI/UX team. Until it lands, nothing is shown inside the owner's home for this; the "Your access" line is removed.
  • Stale dev E2E specs found during regression: gfs-upload-v2 breadcrumb, gfs-runtime-grants "Manage access", gfs-user-journey "Directories" → "Files", gfs-operator-journey grant rows, and users-roles-admin localStorage token. Not filed yet.
  • gfs-agent-delegation test 1 (gfs_download versus gfs_read) is unverified on dev.

Rollout and rollback

  • Rollout:
    1. The migration runs while the old binaries serve. 0139 starts writing pending rows. Nothing provisions until the new control-api starts. The governed-root guard is active from here.
    2. The new control-api enrolls, backfills and provisions.
    3. Per cluster, the inventory API shows pending falling to 0. conflict rows are reviewed in the member's Contact tab; the inventory API still lists them by state.
  • Rollback: an older control-api image starts with the new schema. While it runs, homes are not provisioned, retirement skips the cleanup, and owner-grant protection is off; the runbook lists these. After roll-forward, the startup repair cleans homes of users retired or deleted in that window. Agent grants of users hard-deleted by an older image cannot be identified; the runbook lists this as an operator review item. An older control-api also ignores 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_PASS on 269f8c694 and 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):

  • CI: 63/63 checks green on 3d74efbf1, the head before the R2-L8 and R2-L4 commits.
  • desktop-app at 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.
  • control-api real PostgreSQL (PostgreSQL 16), run as the CI job runs it with the observed and latest dev producers: 138 files / 1173 tests, 0 skipped, all 72 pinned suites present. gfs-controller real PostgreSQL: 2 files / 9 tests.
  • Runtime access contract script: EXIT=0 under /bin/bash 3.2 and bash 5.

Earlier runs (before the identity removal and the dev merges):

Run Head Result
Local Minikube T2, branch profile 269f8c694 MINIKUBE_T2_PASS, T2_EXIT=0, full reconcile; T1 real PostgreSQL 101/101 files, 855 tests
Desktop gfs-user-home.test.ts 269f8c694 8/8 passed (9.0 min)
Control UI gfs-user-home.spec.ts 43edf39eb 5/5 passed (6.5 min), recorded
CodeQL 269f8c694 0 open alerts

CodeQL: 7 of the 9 alerts raised on this branch were dismissed as false positives, each with its reason:

  • 5 js/missing-rate-limiting on userHomes.ts. The routes apply the repo's rateLimitMiddleware and a test proves the 429.
  • js/user-controlled-bypass on externalAuthority.ts:179. The header can only reduce authority, and both branches assert that the user is active.
  • js/missing-await on 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 chatllm when 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 as system_shutdown.

Desktop (desktop-app/test/e2e-playwright/gfs-user-home.test.ts, 8 journeys):

  • J1: home appears without interaction; the active breadcrumb names it and New folder is available (write access); the owner shares with their own agent (Read → Editor → revoke); a 24-file folder ZIP download is cancelled with no file written, then downloaded again.
  • J1b: sharing with users follows the Unix model. The owner sees every entry and lowers an Editor share another user made; entries for people outside the owner's teams are hidden and an operator removes them.
  • J2: a conflicting home path is shown honestly; shared files still open.
  • J3: other users and unassociated agents are refused on the owner's file. The share picker lists only own agents.
  • J4: inherited entries display correctly and a role change persists with its scope. A user holding Editor sees the owner row protected and the owner's agent without a role control.
  • J5: the linked initial admin uses only user authority in Desktop Files.
  • J6: operator and Desktop converge on the same grants in both directions.
  • Guards: refused link, logout and retirement never show stale Files content.

Control UI (control-ui/e2e/gfs-user-home.spec.ts, 5 journeys):

  • J7 Retry: the operator retries a path conflict from the member's Contact tab and the user opens the new home.
  • J7 Restore: revoked owner grant.
  • J8: retirement keeps the file for operators and removes member and agent access.
  • /Users cannot be shared with a member (409 and the operator-only message).
  • Control UI guard: a member home redirects to login and works after login.

Verified claims

Each claim below was checked against the code at this head:

  1. Every active user gets exactly one home /Users/<name>, created automatically (enrollment, reconciler, backfill), with the five-bit inheritable owner grant. An existing folder at the path is never adopted.
  2. One naming policy; collisions get a suffix; reserved names are refused.
  3. Conflicts are stored as ids only. Only the operator detail route resolves them.
  4. Retry and Restore owner access, and an operator grant update that restores the owner bits, use one restore implementation.
  5. Only an operator can revoke or narrow the home owner grant. granted_by records the last assigner; gfs_audit keeps the history.
  6. No grant or share of any kind for a user, team, agent or context subject on / or /Users (409 ancestor_home_exposure). Agent shares are refused earlier with 403 share_to_agent_forbidden.
  7. /Users and bound home roots cannot be deleted, renamed or moved by anyone.
  8. Retirement removes the user's own entries everywhere, including on / and /Users, and every non-operator entry in the home tree. Other subjects' entries on / and /Users are untouched.
  9. Home lifecycle events are audited in gfs_audit.
  10. The startup repair retries each failed row on every tick until it succeeds.
  11. Desktop always uses user authority; the selector can only reduce it.
  12. Grant and share lists carry no subject profile, inside homes or elsewhere.
  13. The owner row is kept in the owner's own home without controls. The name is shown only when the viewer's directory knows it.
  14. Share lowering only narrows (never raises, never empty). There is no role control on other users' agents or on context rows.
  15. "Shared with me" works when the home is not ready: listing, and the rename, delete, move and sidebar drag-and-drop row actions, which use each row's own drive.
  16. Control UI: homes browsed in EvenDrive; the member Contact field with path or state, conflict cause, recovery actions, polling, refresh and home_not_found handling, focus after rename, and mount after the member context loads. There is no separate inventory or deleted-user page.
  17. control-api: team-manager member routes fail with ambiguous column "role" since fcbe8288f3 #1209 fixed.
  18. New real-PostgreSQL suites are registered in CI and in the password-recovery suite contract.
  19. A blank GFS drive is refused in Desktop's IPC and GFS client; an omitted drive defaults to main.

Checklist

  • This is not a new third-party MCP server / WorkflowRecipe (those go to the registry)

🤖 Generated with Claude Code

alfredolopez80 and others added 30 commits October 27, 2026 21:28
…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
@alfredolopez80

Copy link
Copy Markdown
Contributor Author

@claude please run a closing review of this PR at head ec9886834eae442b582bddf9e8ce1609ab19f160.

Context for the review:

  • Scope: feat(gfs): provision default user homes with owner ACLs and agent delegation #1168 per-user GFS homes under /Users/<username> with inheritable owner ACL (manage_acl) and own-agent delegation. Desktop = ordinary-user journey; Control UI = operators/admins only; profile-ui gains no Files surface.
  • CI: 60/60 checks SUCCESS on this head.
  • Local E2E on the branch-owned Minikube profile (cluster marker gitHead = ec98868): Control UI gfs-user-home.spec.ts 5/5 passed (exit 0); Desktop gfs-user-home.test.ts 10/10 passed (exit 0) via the canonical scripts/e2e/playwright-dev.sh launcher after seeding test logins with scripts/e2e/seed-e2e-data.sh. E2E static audit PASS.
  • The earlier formal APPROVED review is on 6af74ad0e and does not cover this head; please review the current diff (111 commits vs dev) with focus on: grant provenance/revocation semantics (fix(control-api): revoke delegated GFS grants when the grantor loses authority #761 slice), home provisioning atomicity/no-restoration, root governance for /Users and home roots, and Desktop/Control UI authority separation.
  • Note: no fresh full T2 certification was run on this head (approved T2 was at a9e67a1); the E2E journeys above are their own lane.

Please post your verdict (approve / request changes) with any blocking findings by file:line.

@claude

claude Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Claude finished @alfredolopez80's task in 10m 59s —— View job


Closing review — head ec9886834

I reviewed the current diff vs origin/dev, focusing on what changed since the last formal approval (6af74ad0e, jozer-rami APPROVED) and the approved T2 (a9e67a1). That delta is substantial — it is not just test alignment.

⚠️ Scope note up front: the formal APPROVED review does not cover this head. Three things landed after it: (1) the identity approach was reversed (827f47b8e, 76dbf08e8), (2) a new "Recent files" feature (1beff060c, 5c4e1fb27, f215b969a, …), and (3) the EvenDrive view restructuring. None of these were in the approved diff, and per your note no fresh T2 ran on this head.


Verdict: ✅ No blocking code defects — but merge should be gated on two non-code items

The backend ACL core is intact and the identity model is now strictly safer than the version prior reviewers approved. I found no blocking correctness or security defect in the code. However, I'm not calling this an unconditional approve, because:

  1. The PR body is materially stale on its central security section (documentation accuracy on a security-sensitive feature), and
  2. The new surfaces on this head are uncertified (no T2/E2E lane in the approved evidence covers the identity reversal or Recent files).

Both are addressable without code changes to the ACL core. Details below.


1. 🔴→✅ Identity model was reversed — PR body is now wrong (non-blocking code, blocking for body/runbook accuracy)

The PR body's entire "Identity inside a home (security fix)" section, migration 0132_gfs_entry_introducer, and Verified claims #12 and #19 describe a server-side introducer rule (subjectProfile, addHomeSubjectProfiles, introduced_by, "31 mutations of the introducer rule") that no longer exists at this head. It was deleted in 827f47b8e:

"Migration 0132_gfs_entry_introducer never left this branch, so it is deleted… Remove subjectProfile from the caller grant/share lists, the addHomeSubjectProfiles reader and the four introduced_by writers."

Verified at HEAD: git grep introduced_by|subjectProfile|addHomeSubjectProfiles → zero matches; migration 0132 is now 0132_gfs_user_file_activity (control-api/src/db.ts:6561), and the home migration sequence ends at 0131_gfs_user_homes.

The shipped design is cleaner and eliminates the R1-M1 leak at the source: the server returns dev's standard ACL row shape — subject {type, id} + the homeOwner marker, no name/email/team-name (grants.ts:1100-1143, shares.ts:82-107). Desktop names subjects only from the viewer's own team directory + own agents (GfsGrantList/index.tsx:96-118); everything else is hidden behind the existing notice. The owner-row exemption is per-row and gated on isCallerHomeRoot (callerHome.ts:6-17, FilesPage.tsx:1081-1084), so an out-of-team viewer gets no anonymous "Owner" row for someone else's home. manage_acl_required, structure locks and home_owner_grant_protected are all intact (grants.ts:941,963,1176-1179,1286; shares.ts:116-119,143,276).

So the long-running R1-M1 "subjectProfile exposes name+email by UUID" debate is moot — the server no longer emits identities at all. Good outcome.

Action before merge: reconcile the PR body + docs/deploy/gfs-user-homes.md with the shipped no-identity design. Delete/replace the "Identity inside a home" section, the 0132_gfs_entry_introducer rollout step, and claims #12/#19. For a security-labeled PR, leaving the body describing a deleted mitigation is a real review hazard, not cosmetics.


2. New "Recent files" feature — clean, two non-blocking notes

Scoping, SQL safety and least-privilege all check out: the /external/gfs/recent route never accepts a user id — it uses authorityOf(req).desktopUserId (external/gfs.ts:757-770), filters WHERE a.user_id = $1 (recentFiles.ts:61), and re-filters results by the caller's current read authority via readableResourceIds (recentFiles.ts:66-73), so a lost-access or agent-read file stays hidden. Both record functions are SECURITY DEFINER with pinned search_path and schema-qualified (db.ts:3314,3340); agent attribution is bound to the verified host subject, not a parameter (db.ts:3337-3366). No path leak (row shape has no path).

  • [Non-blocking] Soft-retired users' activity rows are never purged. The ON DELETE CASCADE (db.ts:3305) only fires on the hard-delete retirement branch (services/directory/users.ts:518-525); the soft-retire branch (:584-610) keeps the users tombstone, so gfs_user_file_activity rows persist. Data is unreachable via the API (retired users 403 at externalAuthority.ts:77-78) but not deleted — confirm against the retention/erasure policy.
  • [Non-blocking] File-open availability is coupled to recency bookkeeping. external/gfs.ts:1120,1209-1224 fails an authorized read (500) if the non-essential recordUserFileActivity write throws. A transient DB blip on the recents write blocks opening a file the user may fully read. The write-path coupling (roll back the file if recording fails, uploadFinalizer.ts:181, serve.ts:638) is intentional; the read-path coupling is the sharper tradeoff.

3. Round-2 carry-over items (all Low / non-blocking)

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 ⚠️ Partial 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 ⚠️ Still present 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, pinned search_path, REVOKE ALL FROM PUBLIC; blocks delete/rename/move/soft-delete of /Users and 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 /Users and 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:

  1. 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.
  2. 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 on ec9886834 is good signal; a T2 on this exact head (or an explicit statement of why the T2 at a9e67a1 still 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 since 6af74ad0e
  • 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

@alfredolopez80
alfredolopez80 dismissed jozer-rami’s stale review November 2, 2026 13:54

Requiere a Review by UI/UX expert Miguel

alfredolopez80 and others added 7 commits November 14, 2026 13:47
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>
The EvenDrive Recent files card and view, and the Recent-files rollback notes
in the user-homes runbook, leave with Recent files. Both documents return to
their text before f215b96, f424824, 563f3df and 91b6628.

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>
alfredolopez80 and others added 8 commits November 14, 2026 14:22
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants