Repository navigation
Conversation
added 6 commits
October 28, 2026 08:05
Add an optional canonical parent-root grant to project-share OAuth subjects. The grant is carried on AuthContext, populated from the Server's WEBCODEX_OAUTH2_PROJECT_SHARE_ROOTS environment for project-share OAuth tokens only, and is bounded to eight roots. Roots are absolute, canonicalized, existing directories; relative, missing, non-directory and symlinked spellings resolve to the real target (or are dropped, which only removes authority). Every other credential class keeps an empty list, preserving the exact-path fence. Test fixtures construct the new field as empty.
…oundary Accept an optional project_root_scopes field on project resolution and managed-worktree preparation. Malformed, empty or relative scopes fail closed; ordinary requests keep their previous exact payload shapes. Before any registration or worktree side effect, the Runner re-derives containment from its own canonical resolution of the committed target against the granted roots. A source that resolved differently at Server-fence time than at the Runner write boundary is rejected. The managed-worktree grant binds the source only; the derived sibling destination remains governed by allowed_roots. Move resume handling after source identity validation so resume is anchored to the registered source identity as well as its stored path.
Add repeatable --project-roots and --no-project-roots to share --auth oauth, pass the grant to the spawned Server, and persist it next to the protected share OAuth client state. An explicit list replaces the stored grant, re-sending the same list is a no-op, omitting the flag restores the last grant, and --no-project-roots removes it so removed roots cannot revive on a later restart. The grant file is replaced atomically in the same protected directory with owner-only permissions; failures never destroy the previous value or write through a planted symlink. --project-roots requires OAuth and an existing absolute directory.
…ath fence For project-scoped credentials, an exact registered path keeps its previous behavior. When the share carries parent-root grants, an existing canonical directory inside one of them may now resolve or register through the ordinary idempotent project-op path instead of being denied before the Runner sees it. The Server forwards the canonical target plus the granted roots, and re-verifies the Runner's returned path against the grant, reporting state_changed from the Runner's registered flag. Missing or malformed effect metadata stays operation_indeterminate rather than claiming no change. Symlink escapes, textual-prefix siblings, relative paths and missing directories remain denied and are never created. Credentials without a grant keep the exact previous payload and fence.
Document --project-roots and --no-project-roots in both CLI peers. Add a loopback-only E2E that runs real dogfood Server and Runner processes behind a real share: OAuth authorization, on-demand admission, concurrency, symlink and prefix boundaries, grant replacement, restoration and clearing across nine launches. Because this branch intentionally excludes OAuth restart-persistence, each launch re-authorizes and exercises only its own fresh token.
The Windows runner job rejected a target inside an explicitly granted root: the grant check compared the Runner's canonical target against the root text it received, and a Windows verbatim namespace prefix (or any unresolved component) made the component comparison fail, so admission was denied with project_path_outside_granted_roots. Resolve each granted root on the Runner before comparing, using the same component-wise, case-insensitive path_is_within helper the existing allowed_roots boundary uses. A root this filesystem cannot resolve stays denied, and the target is still resolved locally, so the commit-boundary binding is unchanged. Both the exact-path and the managed-worktree grant checks use it. Regression: resolve_or_register_project_accepts_unresolved_granted_root grants "<root>/../<root>" (accepted) and a missing root (denied), independent of the platform's canonical text form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A project-scoped share (
webcodex share --auth oauth) is fenced to the exact Project path that was registered. A repository cloned after the share starts, or a sibling repository the user wants to open, is denied withproject_registry_scope_deniedand has to be registered out of band. Widening the Runner's filesystem policy does not solve this: Runner execution roots must not become Project-registry authority.User-visible change
webcodex share --auth oauthnow accepts repeatable--project-roots <DIR>(up to 8) and--no-project-roots.work_on_projectentry point, including managed worktrees, without per-project registration.--no-project-rootsremoves it so removed roots cannot revive after a restart.Authority and binding model
AuthContext::project_root_scopes), populated from the Server'sWEBCODEX_OAUTH2_PROJECT_SHARE_ROOTS; it is never a Runner filesystem policy...spellings that resolve inside are fine, while textual-prefix siblings (<root>-extra), symlink escapes, relative/empty entries and missing directories are denied. The Runner'sallowed_rootsstays an independent execution constraint.state_changedfollows the Runner'sregisteredflag; missing or malformed effect metadata returnsoperation_indeterminaterather than claiming no change.allowed_roots. Source identity anchors (project id plus root fingerprint) are validated before resume is considered, so retries and resumes neither widen nor duplicate authority.project_root_scopesarrays fail closed in both request handlers. No parallel project-registration protocol is introduced; admission uses the existing idempotent project-op path.Compatibility with credentials that have no grant
{"path": ...}, and managed-worktree payloads keep their 4- or 6-field shapes.resolve_workspaceare unchanged.main. The E2E therefore re-authorizes each launch instead of reusing a previous token.Validation
Linux x86_64, rustc 1.99.1. Tested commit:
aaa6e8e74964846a88af6b890f2e569c7d0c130d.Library filters (
cargo test --locked -p webcodex --lib <filter>):project_root_scopeproject_share_root_scoperesolve_or_register_projectverify_runner_pathmanaged_worktree_project_grant_admissionproject_grant_authority_is_identicalshare_project_roots_Runner filters (
cargo test --locked -p webcodex-runner <filter>):managed_worktree_share_grantresolve_or_register_project_grant_scopesmanaged_worktree_expected_source_identitymanaged_worktree_without_share_grantOther gates:
cargo fmt --all -- --check-> exit 0git diff --check-> exit 0cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner-> exit 0python3 scripts/e2e_project_share_roots.py --bin-dir target/dogfood->E2E_PROJECT_SHARE_ROOTS=PASS,lifecycle_launches=9 client_id_stable=true. The loopback E2E runs real Server and Runner processes behind a real share, completes the OAuth Authorization Code + PKCE flow, exercises on-demand admission, concurrency, prefix/symlink/missing-path boundaries, grant replacement, restoration and clearing across restarts.Environment lanes not run:
Design questions for maintainers
project-rootsfile next to the share OAuth client state the right persistence home, or would you prefer this grant on the project config? The separate file keeps project config stable and makes--no-project-rootsa clear delete, but it is one more piece of share state to protect.allowed_roots; a stricter rule may be preferable if you want the grant to bound the whole worktree lifecycle.CI follow-up: Windows runner containment
The Windows
webcodex-runnerjob failed onresolve_or_register_project_accepts_target_inside_granted_roots_idempotentlywithproject_path_outside_granted_roots: the Runner compared its canonical target (a verbatim\\?\path on Windows) against the root text it received, so a text-form difference decided admission.The grant check now resolves each granted root on the Runner before comparing, using the same
path_is_withinhelper the existingallowed_rootsboundary uses (component-wise, case-insensitive on Windows). A root the Runner cannot resolve stays denied, and the target is still resolved locally, so the commit-boundary binding is unchanged. Both the exact-path and managed-worktree grant checks use it.Regression:
resolve_or_register_project_accepts_unresolved_granted_rootgrants<root>/../<root>(accepted) and a missing root (denied), independent of the platform's canonical text form.