Skip to content

feat: gitvs repo_collaborators resource — census, class N, gated effector - #1423

Merged
4444J99 merged 1 commit into
mainfrom
feat/gitvs-collaborators
Jul 23, 2026
Merged

feat: gitvs repo_collaborators resource — census, class N, gated effector#1423
4444J99 merged 1 commit into
mainfrom
feat/gitvs-collaborators

Conversation

@4444J99

@4444J99 4444J99 commented Jul 23, 2026

Copy link
Copy Markdown
Member

PR 2 of the partner-partition plan (builds on #1407's ACCESS registry).

  • estate.yaml: repo_collaborators resource row — approval-gated delegate (L-PARTNER-GRANTS), so gitvs reconcile never auto-runs the effector.
  • gitvs.py: _collaborator_census() (bounded probe set: granted ∪ never-grant repos via affiliation=outside, plus each org's outside-collaborator roll — logins/roles only) feeding doctor class N: undeclared access or an over-ceiling role is RED; a declared-but-absent grant is a staged invite CITED on the lever (outbound stays his hand).
  • scripts/collab-sync.py: the effector — --plan prints drift plus the exact gh api commands for the operator; --apply removes UNDECLARED access only (exposure-reducing direction), double-dark behind LIMEN_COLLAB_APPLY=1.
  • parameters.yaml: LIMEN_COLLAB_APPLY declared; his-hand-levers.json: L-PARTNER-GRANTS homed.
  • access.yaml: eauco-mads why-text folds the completed audit result (the login holds exactly the one declared repo).
  • gitvs.test.sh: cases 26–29 (offline N-rung listing, unarmed-apply refusal, offline SKIP, absent-registry SKIP) — 31/31 pass.

Verification: gitvs.test.sh PASS (31 checks); verify-scoped.shScoped verification passed (gitvs-parity, gitvs-test, check-params, check-gates all green).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added collaborator access monitoring for governed repositories and organization accounts.
    • Added a synchronization tool that reports undeclared access, pending invitations, role mismatches, and missing grants.
    • Added a safe dry-run mode and a separately gated cleanup mode for removing only undeclared access.
    • Added drift reporting to the access health checks, including offline and unavailable-data handling.
    • Clarified the rationale for an existing repository access grant.

…ctor

The partner-partition enforcement loop: estate.yaml gains the
repo_collaborators resource (approval-gated delegate, so reconcile
never auto-runs it); observe() gains a bounded collaborator census
(granted ∪ never-grant repos, affiliation=outside, plus each org's
outside-collaborator roll); doctor class N diffs the census against
the ACCESS registry — undeclared access or an over-ceiling role is
RED, a declared-but-absent grant is a staged invite cited on
L-PARTNER-GRANTS (outbound stays his hand). collab-sync.py is the
effector: --plan prints the exact commands, --apply removes
UNDECLARED access only, double-dark behind LIMEN_COLLAB_APPLY=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

Copy link
Copy Markdown

Multi-agent review roll call
@codex review

(CodeRabbit and Claude review automatically. Reviewers: post substantive findings only. Authors/agents: address every thread, push fixes to this branch, reply and resolve, then re-request review.)

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a governed repo_collaborators resource, GitHub collaborator census and Class N drift detection, plus a gated CLI that removes undeclared access while staging role changes and invitations for human action.

Changes

Collaborator access governance

Layer / File(s) Summary
Collaborator governance contract
institutio/github/access.yaml, institutio/github/estate.yaml, institutio/governance/parameters.yaml, his-hand-levers.json
Declares the repo_collaborators resource, approval lever, double-dark apply gate, and updated grant rationale.
Census and drift detection
scripts/gitvs.py
Collects repository and organization collaborator observations and adds Class N comparisons for undeclared access, role ceilings, invitations, and missing grants.
Gated collaborator reconciliation
scripts/collab-sync.py, scripts/tests/gitvs.test.sh
Adds plan/apply reconciliation that removes undeclared access only when gated, stages role and invite actions, and tests offline, missing-registry, and apply-gate behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant collab-sync
  participant gitvs
  participant GitHub
  Operator->>collab-sync: run --plan or --apply
  collab-sync->>gitvs: load ACCESS and collaborator census
  gitvs-->>collab-sync: live collaborators and declared grants
  collab-sync->>GitHub: DELETE undeclared access when apply gate is enabled
  collab-sync-->>Operator: staged role and invitation commands
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change set: repo_collaborators resource, census, class N checks, and the gated effector.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gitvs-collaborators

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
his-hand-levers.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

institutio/github/access.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

institutio/github/estate.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fba0d678d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/gitvs.py
Comment on lines +842 to +844
grants = access.get("grants") or {}
policy = access.get("policy") or {}
probe = sorted({str(r) for r in grants} | {str(r) for r in (policy.get("never_grant_repos") or [])})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scan all repos for partner overexposure

Because the probe set is only grants ∪ never_grant_repos, class N never queries other owned repos. If a login is correctly declared for repo A but accidentally still has outside-collaborator access on repo B, the org roll suppresses it as a declared login and repo B is absent from by_repo, so both doctor and collab-sync --apply can report/act as if drift is clean while the extra access remains. The new desired state says each partner sees exactly their granted project repos, so the census needs to enumerate all governed repos (or otherwise enumerate every declared collaborator's actual repo access) before declaring green.

Useful? React with 👍 / 👎.

Comment thread scripts/gitvs.py
Comment on lines +1691 to +1696
pending = {str(i.get("login") or "").lower() for i in (obs.get("invitations") or [])}
for login_l, g in sorted(declared.items()):
if login_l in live:
continue
if login_l in pending:
cites.append(f"[N collaborator-drift] {repo}: invite pending for {g.get('login')} → {n_atom}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate pending invitation roles

Pending invitations are reduced to login strings here, discarding the role captured by the census. If a declared partner has a pending invite at push/admin while access.yaml declares only pull, class N cites the invite as acceptable instead of failing or staging a cancellation/downgrade, leaving an over-privileged invite ready to be accepted. Compare pending invitation permissions through ROLE_NAME_TO_GRANT the same way live collaborator roles are checked before treating the grant as satisfied.

Useful? React with 👍 / 👎.

Comment thread scripts/gitvs.py
Comment on lines +850 to +853
"api",
f"/repos/{repo}/collaborators?affiliation=outside&per_page=100",
"--jq",
"[.[] | {login: .login, role: .role_name}] | sort_by(.login)",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate collaborator census calls

These REST list calls set per_page=100 but do not pass --paginate; the gh manual says --paginate is what makes additional requests for all pages. For any repo/org with more than 100 outside collaborators or invitations, entries after page 1 are invisible, so undeclared or over-ceiling access can be missed and class N can go green incorrectly. Apply pagination consistently to the repo collaborators, invitations, and org outside-collaborators calls and merge the pages before diffing.

Useful? React with 👍 / 👎.

Comment thread scripts/gitvs.py
Comment on lines +1011 to +1012
# Collaborator census — the partner-partition layer (class N's input; ACCESS is desired-state).
led["collaborators"] = _collaborator_census(estate, load_access(), token, online)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't publish undeclared collaborator logins

The census result is written into docs/github-estate-ledger.json, so putting raw collaborator and invitation logins in led["collaborators"] also publishes any undeclared login discovered during drift detection, not just the declared rows already in access.yaml. In the scenario this rung is meant to catch — unexpected outside access — the first gitvs census would commit that person's login into the tracked ledger; keep the detailed collaborator facts in a gitignored runtime sink or redact to counts in the public ledger.

Useful? React with 👍 / 👎.

Comment thread scripts/gitvs.py
Comment on lines +889 to +891
try:
# personal accounts 404 here — degrade to None; class N skips that roll, never guesses
out["org_outside"][org] = json.loads(r.stdout or "[]") if r.returncode == 0 else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat unreadable org rolls as incomplete

Only the per-repo calls affect ok, so if /orgs/{org}/outside_collaborators fails while the repo probes succeed, complete remains true and collab-sync later treats None as an empty list. With an App/token that can read the probed repos but not the org outside-collaborator roll, collab-sync can return a clean verdict without checking org-level undeclared collaborators; mark these failures incomplete for the effector, or make the effector refuse when any org roll is unreadable.

Useful? React with 👍 / 👎.

Comment thread scripts/collab-sync.py
print(cmd)
if his_hand:
print(f"→ {len(his_hand)} staged command(s) above are L-PARTNER-GRANTS work — fire them deliberately.")
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return success after successful applies

When --apply is armed and the only drift is machine-runnable removals, this always exits 1 even if every gh DELETE succeeds because failed is accumulated but never used. Automation that invokes the effector will keep treating an already-fixed collaborator cleanup as failed instead of recording a green receipt; return nonzero only when a deletion failed or human-staged commands remain.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/gitvs.py`:
- Around line 1643-1702: Scope declared collaborator logins by owning
organization rather than using one global set, so cross-org grants do not
suppress undeclared-collaborator findings. In scripts/gitvs.py lines 1643-1702,
update the N-rung’s declared-login computation and org_outside comparison to use
each repository’s organization; apply the identical scoping in
scripts/collab-sync.py lines 107-115 for the --apply removal path. Reuse a
shared declared_logins_by_org helper if the project structure supports it.
- Around line 835-896: Update _collaborator_census to paginate all collaborator,
invitation, and organization outside-collaborator API requests using gh api
--paginate. Add a slurp/merge step such as jq -s so the multi-page output
becomes one JSON array before each json.loads call, preserving the existing
sorting and fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dcf220a-1fa3-4a26-9286-ff2954dab48a

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdd878 and fba0d67.

📒 Files selected for processing (7)
  • his-hand-levers.json
  • institutio/github/access.yaml
  • institutio/github/estate.yaml
  • institutio/governance/parameters.yaml
  • scripts/collab-sync.py
  • scripts/gitvs.py
  • scripts/tests/gitvs.test.sh

Comment thread scripts/gitvs.py
Comment on lines +835 to +896
def _collaborator_census(estate: dict, access: dict | None, token: str | None, online: bool) -> dict:
"""Class N's Lens: outside collaborators + pending invites over the bounded probe set
(granted repos ∪ never-grant repos), plus each owner org's outside-collaborator roll.
Logins + roles only — the same public facts the ACCESS registry already carries."""
out: dict = {"complete": False, "by_repo": {}, "org_outside": {}}
if not online or not isinstance(access, dict) or not access:
return out
grants = access.get("grants") or {}
policy = access.get("policy") or {}
probe = sorted({str(r) for r in grants} | {str(r) for r in (policy.get("never_grant_repos") or [])})
ok = True
for repo in probe:
row: dict = {"outside": None, "invitations": None}
r = _gh(
[
"api",
f"/repos/{repo}/collaborators?affiliation=outside&per_page=100",
"--jq",
"[.[] | {login: .login, role: .role_name}] | sort_by(.login)",
],
token,
timeout=30,
)
if r.returncode == 0:
try:
row["outside"] = json.loads(r.stdout or "[]")
except ValueError:
ok = False
else:
ok = False
r = _gh(
[
"api",
f"/repos/{repo}/invitations?per_page=100",
"--jq",
"[.[] | {id: .id, login: .invitee.login, role: .permissions}] | sort_by(.login)",
],
token,
timeout=30,
)
if r.returncode == 0:
try:
row["invitations"] = json.loads(r.stdout or "[]")
except ValueError:
ok = False
else:
ok = False
out["by_repo"][repo] = row
for org in owners(estate):
r = _gh(
["api", f"/orgs/{org}/outside_collaborators?per_page=100", "--jq", "[.[].login] | sort"],
token,
timeout=30,
)
try:
# personal accounts 404 here — degrade to None; class N skips that roll, never guesses
out["org_outside"][org] = json.loads(r.stdout or "[]") if r.returncode == 0 else None
except ValueError:
out["org_outside"][org] = None
out["complete"] = ok
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does gh api --paginate correctly concatenate multiple paginated JSON array responses when combined with --jq?

💡 Result:

No, gh api --paginate does not automatically concatenate multiple paginated responses into a single, valid JSON array when used with --jq [1][2][3]. When you use --paginate, gh api performs multiple HTTP requests to fetch all pages, but it outputs each page as an individual, discrete JSON document to standard output [4][1][3]. Consequently, the resulting stream is a sequence of separate JSON objects or arrays rather than one cohesive JSON array [4][5][3]. If you use --jq alongside --paginate, the jq expression is applied independently to each individual page [5]. This means you will receive a sequence of the filtered results, not a single merged result [5]. To produce a single, concatenated JSON array, you should use the --slurp flag in conjunction with --paginate [4]. Note the following limitations and usage requirements: 1. Compatibility: The --slurp flag is designed to wrap all pages of JSON arrays or objects into one outer JSON array [4]. However, it cannot be used at the same time as --jq or --template because those flags are mutually exclusive with --slurp [6]. 2. Workarounds: If you need to filter and aggregate results, the common community-recommended approach is to use gh api --paginate --jq '...' (to filter per page) and then pipe the output to jq -s (the slurp flag for the standalone jq utility) to merge the streams into one final JSON array [2][7][8]. Example: # Using pipe to jq for merging results gh api --paginate --jq '.[]' | jq -s '.' For REST API calls, recent versions of gh have improved handling to output a single JSON array by massaging the stream (omitting redundant brackets), but this behavior is distinct from the general --slurp mechanism [1]. For complex GraphQL responses, manual piping to jq -s remains the standard practice [2][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate gitvs.py and inspect relevant functions =="
fd -a 'gitvs\.py$' . | sed 's#^\./##'
python3 - <<'PY'
from pathlib import Path
p = next(Path('.').rglob('gitvs.py'))
text = p.read_text()
print('FILE', p)
lines = text.splitlines()
for needle in ['def _collaborator_census', 'def _gh', "_collaborator_census("]:
    try:
        idx = next(i for i,l in enumerate(lines,1) if needle in l)
        start=max(1, idx-5); end=min(len(lines), idx+120)
        print(f'\n--- {p}:{start}-{end} ({needle}) ---')
        for n in range(start, end+1):
            print(f'{n:4d}\t{lines[n-1]}')
    except StopIteration:
        print(f'not found: {needle}')
PY

echo
echo "== gh api docs behavior if available locally =="
man gh-api >/tmp/man-gh-api.txt 2>/tmp/gh-api-man.err || true
if [ -f /tmp/man-gh-api.txt ]; then
  rg -n "paginate|pag|jq|slurp|JSON" /tmp/man-gh-api.txt | head -80
else
  cat /tmp/gh-api-man.err || true
fi

echo
echo "== read-only behavioral probe: inspect current CLI docs from installed gh if present =="
if command -v gh >/dev/null 2>&1; then
  gh --version | head -3
  gh api help | sed -n '1,220p' | rg -n "paginate|pagination|JSON|jq|slurp|--jq" -C 2 || true
else
  echo "gh not installed"
fi

Repository: organvm/limen

Length of output: 18426


🌐 Web query:

GitHub API /repos/owner/repo/collaborators pagination per_page 100 maximum

💡 Result:

The GitHub REST API endpoint for listing collaborators supports a per_page query parameter, which has a maximum allowed value of 100 [1][2]. The default value for this parameter is 30 [1][2]. If you provide a per_page value greater than 100, the API will silently clamp the value to the maximum (100) rather than returning an error [3]. This means that requests for more than 100 items per page will still return a 200 OK status, but they will only return 100 items [3]. To retrieve more than 100 collaborators, you must paginate through the results using the page parameter or by following the URLs provided in the link header of the response [4][5][6].

Citations:


🌐 Web query:

GitHub API /repos/owner/repo/invitations pagination per_page 100 maximum

💡 Result:

The GitHub REST API endpoint for listing repository invitations (GET /repos/{owner}/{repo}/invitations) supports pagination using the per_page query parameter [1][2]. The maximum value allowed for the per_page parameter is 100 [1][3]. The default value for this parameter is 30 [1][4]. You can retrieve additional pages of results by using the page query parameter or by following the pagination links provided in the response headers [5].

Citations:


🌐 Web query:

GitHub API /orgs/organization/outside_collaborators pagination per_page 100 maximum

💡 Result:

The GitHub REST API endpoint for listing an organization's outside collaborators (/orgs/{org}/outside_collaborators) supports the per_page query parameter, which allows you to specify the number of results returned per page [1][2]. The maximum value allowed for this parameter is 100 [1][3][4]. The default value is 30 [1][5][3]. To retrieve results beyond the first page, you can use the page query parameter in conjunction with per_page [1][6]. For more extensive data retrieval, it is recommended to follow the pagination links provided in the Link header of the API response, or utilize tools like the octokit.paginate method, which can automatically handle fetching subsequent pages [6][7][8].

Citations:


Paginate collaborator/invitation/outside-collaborator lookups and slurp the per-page JSON arrays.

per_page=100 is the maximum allowed for these GitHub REST endpoints, so >100 results are split across pages and without pagination the current code ignores later pages. With gh api --paginate, each page is emitted separately; add a slurp/merge step before json.loads (for example | jq -s .) so later pages are included in the governance check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/gitvs.py` around lines 835 - 896, Update _collaborator_census to
paginate all collaborator, invitation, and organization outside-collaborator API
requests using gh api --paginate. Add a slurp/merge step such as jq -s so the
multi-page output becomes one JSON array before each json.loads call, preserving
the existing sorting and fallback behavior.

Comment thread scripts/gitvs.py
Comment on lines +1643 to +1702
# N — collaborator drift (the partner-partition rung): live outside collaborators vs the
# ACCESS registry. Undeclared access or an over-ceiling role is RED (removal is the machine-
# runnable direction, via collab-sync --apply); a declared-but-absent grant is a STAGED INVITE —
# outbound is his hand, so it is cited on the lever, never auto-sent.
access = load_access()
coll = led.get("collaborators") or {}
n_atom = "L-PARTNER-GRANTS"
if access is None:
skips.append("[N collaborator-drift] no ACCESS registry (institutio/github/access.yaml absent)")
elif not access:
pass # unparseable registry is already a class-H parity defect this run
elif not coll.get("complete"):
skips.append("[N collaborator-drift] census incomplete (gh errors)")
else:
grants = access.get("grants") or {}
declared_logins = {
str(g.get("login", "")).lower()
for rows_ in grants.values()
if isinstance(rows_, list)
for g in rows_
if isinstance(g, dict)
}
for org, roll in sorted((coll.get("org_outside") or {}).items()):
if roll is None:
skips.append(f"[N collaborator-drift] {org}: outside-collaborator roll unreadable")
continue
undeclared = sorted(login for login in roll if str(login).lower() not in declared_logins)
if undeclared:
fails.append(
f"[N collaborator-drift] {org}: undeclared outside collaborator(s): {', '.join(undeclared)}"
)
for repo, obs in sorted((coll.get("by_repo") or {}).items()):
declared = {str(g.get("login", "")).lower(): g for g in (grants.get(repo) or []) if isinstance(g, dict)}
outside = obs.get("outside")
if outside is None:
skips.append(f"[N collaborator-drift] {repo}: collaborator list unreadable")
continue
for c in outside:
login = str(c.get("login") or "")
role = ROLE_NAME_TO_GRANT.get(str(c.get("role")), str(c.get("role")))
g = declared.get(login.lower())
if g is None:
fails.append(f"[N collaborator-drift] {repo}: {login} ({role}) has no grant row")
elif GRANT_ROLE_RANK.get(role, 99) > GRANT_ROLE_RANK.get(str(g.get("role")), -1):
fails.append(
f"[N collaborator-drift] {repo}: {login} live role {role} exceeds declared {g.get('role')}"
)
live = {str(c.get("login") or "").lower() for c in outside}
pending = {str(i.get("login") or "").lower() for i in (obs.get("invitations") or [])}
for login_l, g in sorted(declared.items()):
if login_l in live:
continue
if login_l in pending:
cites.append(f"[N collaborator-drift] {repo}: invite pending for {g.get('login')} → {n_atom}")
else:
(cites if n_atom in homed else fails).append(
f"[N collaborator-drift] {repo}: {g.get('login')} declared but absent — staged invite → "
+ (f"{n_atom} (owned, open)" if n_atom in homed else f"{n_atom} (UNHOMED)")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Undeclared-collaborator check is not scoped per org, letting access leak across orgs. Both the doctor() N-rung and the collab-sync.py --apply removal path build a single flat set of every declared login across all repos/orgs, then use it to judge whether a login found in one org's outside_collaborators roll is "declared." A partner granted access to a repo in Org A is therefore never flagged (or removed) when they turn up as an undeclared outside collaborator of Org B — exactly the partition guarantee L-PARTNER-GRANTS/class N is meant to enforce, and the only backstop for repos outside the bounded probe set (grants ∪ never_grant_repos).

  • scripts/gitvs.py#L1643-L1702: scope declared_logins by the owning org (e.g. repo.split("/", 1)[0]) before comparing against each org's roll at Line 1669, instead of the flat global set built at Lines 1658-1664.
  • scripts/collab-sync.py#L107-L115: apply the identical org-scoping fix to the declared_logins set built at Lines 60-67 before it gates the --apply removal loop for org_outside.

Consider extracting one shared declared_logins_by_org(access) helper in gitvs.py that both files call, eliminating the duplicated (and now-diverging) computation.

📍 Affects 2 files
  • scripts/gitvs.py#L1643-L1702 (this comment)
  • scripts/collab-sync.py#L107-L115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/gitvs.py` around lines 1643 - 1702, Scope declared collaborator
logins by owning organization rather than using one global set, so cross-org
grants do not suppress undeclared-collaborator findings. In scripts/gitvs.py
lines 1643-1702, update the N-rung’s declared-login computation and org_outside
comparison to use each repository’s organization; apply the identical scoping in
scripts/collab-sync.py lines 107-115 for the --apply removal path. Reuse a
shared declared_logins_by_org helper if the project structure supports it.

@4444J99
4444J99 added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit 2a5d306 Jul 23, 2026
20 checks passed
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.

1 participant