| name | reassign-findings-owners |
| description | Use when the user asks to re-route, reassign, or change the owners for one or more findings packages in progress-tracker/processed-results/. Validates new owners as active Red Hat employees via LDAP, updates all local control files, updates Google Drive folder permissions via the gws CLI, and updates the Glasswing Ownership Tracker Google Doc. |
| user-invocable | true |
Reassign Findings Owners
Re-route one or more findings packages to new owners. Removes the previous
owners from all tracking systems and grants access to the new owners.
When to Use
- User asks to "re-route", "reassign", or "change owners" for a findings package
- User asks to "unassign" a current owner and replace with someone else
- User asks to "add" or "remove" an owner from an existing findings package
- User says a specific person "should own" or "should receive" a findings package
Inputs
$ARGUMENTS contains:
- One or more findings package names (with or without
-findings suffix)
- One or more new owner identifiers — any combination of:
- Full name (e.g.,
Roman Hryniewiecki)
- Red Hat email (e.g.,
rhryniew@redhat.com)
- Kerberos ID (e.g.,
rhryniew)
- GitHub username (e.g.,
roman-hryniewiecki)
- Optionally, explicit instructions to remove specific current owners
If only one new owner is provided, the package will have a single designated
owner (the second owner column is set to —). If two are provided, both are
recorded. More than two is not supported by the tracking schema.
Examples:
/reassign-findings-owners sg-core-findings to Roman Hryniewiecki
/reassign-findings-owners ci-framework repo-setup to vcastell@redhat.com and brjackma@redhat.com
/reassign-findings-owners gitops-findings to cjeanner@redhat.com and aharivel@redhat.com, remove lmadsen
Output
For each reassignment, present a confirmation summary table before making
changes, then report completion status for each system updated.
Procedure
Step 1 — Resolve the Findings Package(s)
For each package argument, resolve to a directory under
progress-tracker/processed-results/:
RESULTS_BASE="progress-tracker/processed-results"
If the argument doesn't end in -findings, append it. Verify the directory
exists. If it doesn't exist, report an error and skip that package.
Look up the current owners in all three tracking files:
grep "<package-name>" progress-tracker/tracking/owners-mapping.md
grep "<package-name>" progress-tracker/tracking/drive-sharing-plan.md
grep "<package-name>" progress-tracker/tracking/routed-results.md
Record the current state:
- Row number in
owners-mapping.md and drive-sharing-plan.md
- Current Owner 1 (name, GitHub, email)
- Current Owner 2 (name, GitHub, email)
- Drive folder ID from
drive-sharing-plan.md
Step 2 — Resolve New Owner Identities
For each new owner, resolve their full identity (name, GitHub username,
Kerberos ID, Red Hat email). The resolution depends on what the user provided:
2a. If an email was provided (user@redhat.com)
Extract the Kerberos ID (local part before @redhat.com) and proceed to
LDAP verification (Step 3).
2b. If a Kerberos ID was provided
Proceed directly to LDAP verification (Step 3).
2c. If a full name was provided
Search existing tracking data first:
grep -i "<name>" progress-tracker/tracking/owners-mapping.md
If found, extract their Kerberos ID and email from the existing entry.
If not found, search GitHub:
- Use
mcp_github_search_users with the full name
- Check the GitHub profile for the
company field
- Fetch a recent commit
.patch to find @redhat.com in From: /
Signed-off-by: lines
If still no Kerberos ID, try common Kerberos patterns in LDAP:
- First initial + last name truncated to 7 chars (e.g.,
rhryniew)
- First initial + full last name (e.g.,
rhryniewiecki)
- First name + last initial (e.g.,
romanh)
- Username from email if found in git commits
Important: Kerberos IDs frequently differ from GitHub usernames. Never
assume they are the same. See the Kerberos ↔ GitHub Username Mapping table
in the assign-findings-owners skill for known mappings.
2d. If a GitHub username was provided
- Search
progress-tracker/tracking/owners-mapping.md for the username
- If not found, fetch a recent commit
.patch from a repo they contribute
to and read the From: line for @redhat.com
- Extract the Kerberos ID from the email local part
- Proceed to LDAP verification (Step 3)
Step 3 — LDAP Verification (Blocking)
Every new owner must be verified as an active Red Hat employee before
any changes are made. This is a blocking check.
python scripts/validate_employee.py <kerberos_id> [<kerberos_id> ...]
The script is at ai-security-harness/scripts/validate_employee.py (relative
to the workspace root). It requires VPN access to reach ldap.corp.redhat.com.
Interpret results:
| Script output | Action |
|---|
<uid> (Full Name): active employee | PASS — proceed with reassignment |
<uid> (Full Name): terminated (rhatTermDate=<date>) | BLOCK — report to user, do not reassign |
<uid>: NOT found | RETRY — likely passed a GitHub username instead of Kerberos ID; try other resolution methods (Step 2c) |
If the user's GitHub profile lists a non-Red Hat company but LDAP confirms
them as active, they are still a valid owner (contractors, recent hires, or
people who haven't updated their profile).
If LDAP is inaccessible (not on VPN), report to the user that
verification cannot be completed and do not proceed with the reassignment.
Unlike the assign-findings-owners skill which allows provisional
assignments, reassignments change live Drive permissions and send
notification emails — they must not be executed without LDAP confirmation.
Step 4 — Confirm with User
Present a summary table before making any changes:
### Reassignment Plan: `<package-name>`
| | Current | New |
|---|---------|-----|
| **Owner 1** | Full Name (`email@redhat.com`) | Full Name (`email@redhat.com`) |
| **Owner 2** | Full Name (`email@redhat.com`) | Full Name (`email@redhat.com`) or — |
**Actions:**
- Remove Drive access: `old-owner1@redhat.com`, `old-owner2@redhat.com`
- Grant Drive access: `new-owner1@redhat.com`, `new-owner2@redhat.com`
- Update: owners-mapping.md, drive-sharing-plan.md, routed-results.md, Google Doc
Wait for user confirmation before proceeding. If the user has already
expressed intent clearly (e.g., "re-route X to Y"), treat that as
implicit confirmation and proceed directly.
Step 5 — Update Local Control Files
Update all three tracking files in progress-tracker/tracking/. Use exact
find-and-replace edits to avoid corrupting surrounding rows.
5a. owners-mapping.md
Replace the existing row with new owner data. The table format is:
| # | Result Directory | Owner 1 | GitHub | Email | Owner 2 | GitHub | Email | Status |
If only one new owner, set Owner 2 fields to —.
5b. drive-sharing-plan.md
Replace the existing row's email columns. The table format is:
| # | Drive Folder | Folder ID | Owner 1 Email | Owner 2 Email |
If only one new owner, set Owner 2 Email to —.
5c. routed-results.md
Append a new section and rows at the end of the file (do not modify
existing rows — they form an immutable audit trail). Use this format:
### Owner reassignment — <package-name(s)> (YYYY-MM-DD)
<Brief explanation of the change.>
| # | Result Package | Routed To | Date |
|---|----------------|-----------|------|
| <next-number> | [<display-name>](processed-results/<package-name>/) | new-owner1@redhat.com, new-owner2@redhat.com | YYYY-MM-DD |
The next row number is one more than the last row number in the file.
Use today's date. If only one owner, list just that one email.
Step 6 — Update Google Drive Permissions
Use the gws CLI to modify Drive folder permissions. The folder ID comes
from drive-sharing-plan.md (recorded in Step 1).
6a. List Current Permissions
gws drive permissions list \
--params '{"fileId": "<FOLDER_ID>", "supportsAllDrives": true, "fields": "permissions(id,emailAddress,role,type)"}'
Find the permission IDs for each old owner's email address.
6b. Remove Old Owner Permissions
For each old owner being removed:
gws drive permissions delete \
--params '{"fileId": "<FOLDER_ID>", "permissionId": "<PERMISSION_ID>", "supportsAllDrives": true}'
Only remove permissions with role commenter that belong to the old
owners. Never remove permissions with role organizer, writer, or
fileOrganizer — those belong to the Glasswing team administrators.
6c. Add New Owner Permissions
For each new owner:
gws drive permissions create \
--params '{"fileId": "<FOLDER_ID>", "supportsAllDrives": true, "sendNotificationEmail": true, "emailMessage": "<NOTIFICATION>"}' \
--json '{"role": "commenter", "type": "user", "emailAddress": "<EMAIL>"}'
Notification message (single line, no control characters in JSON):
You are receiving this because you have been identified as a contributor or maintainer for this component security findings. This folder contains security audit results from Project Glasswing. ACTION REQUESTED: Please review the Engineering Process for Glasswing Findings at https://docs.google.com/document/d/1mjzY8IAAEjab63AKcX-K_NYmVmHZT56j4kUPz5eflTQ/edit IMPORTANT: This is an embargoed security communication.
If a new owner already has an existing permission on the folder (e.g.,
they were previously the second owner and are now becoming the first),
skip the create step for that user.
Step 7 — Update the Google Doc
The Glasswing Ownership Tracker is a Google Doc with a table on the
"Repository to Owner Mappings" tab.
Document ID: 1mjzY8IAAEjab63AKcX-K_NYmVmHZT56j4kUPz5eflTQ
Tab ID: t.pkxlpnoddcho
7a. Fetch Fresh Document
gws docs documents get \
--params '{"documentId": "1mjzY8IAAEjab63AKcX-K_NYmVmHZT56j4kUPz5eflTQ", "includeTabsContent": true}' \
> /tmp/gdoc_reassign.json
7b. Find the Row
Parse the JSON to locate the table row matching the package name. Extract
the startIndex and endIndex for each cell's text content (excluding
the trailing newline character).
python3 -c "
import json
with open('/tmp/gdoc_reassign.json') as f:
doc = json.load(f)
tab = doc['tabs'][1] # t.pkxlpnoddcho
body = tab['documentTab']['body']['content']
for elem in body:
if 'table' in elem:
table = elem['table']
for row_idx, row in enumerate(table.get('tableRows', [])):
cells = row.get('tableCells', [])
row_text = ''
for cell in cells:
for para in cell.get('content', []):
for el in para.get('paragraph', {}).get('elements', []):
row_text += el.get('textRun', {}).get('content', '')
if '<package-name>' in row_text:
print(f'=== Row {row_idx} ===')
for ci, cell in enumerate(cells):
for para in cell.get('content', []):
for el in para.get('paragraph', {}).get('elements', []):
tr = el.get('textRun', {})
content = tr.get('content', '').strip()
if content:
print(f' Cell {ci}: [{el.get(\"startIndex\")}-{el.get(\"endIndex\")}] \"{content}\"')
"
The table has 9 columns (cells 0–8):
- Cell 0: Row number
- Cell 1: Package name
- Cell 2: Owner 1 Name
- Cell 3: Owner 1 GitHub
- Cell 4: Owner 1 Email
- Cell 5: Owner 2 Name
- Cell 6: Owner 2 GitHub
- Cell 7: Owner 2 Email
- Cell 8: Status
7c. Build the batchUpdate Request
Construct deleteContentRange + insertText pairs for cells 2–7 (Owner 1
name through Owner 2 email). Key rules:
- Process from highest startIndex to lowest — edits at higher indices
must come first to avoid invalidating lower indices.
- Exclude the trailing newline — each TextRun's content ends with
\n.
The delete range must stop one character before endIndex (i.e., use
endIndex - 1 as the delete end).
- If only one owner, set cells 5, 6, 7 to
— (em dash, U+2014).
gws docs documents batchUpdate \
--params '{"documentId": "1mjzY8IAAEjab63AKcX-K_NYmVmHZT56j4kUPz5eflTQ"}' \
--json '{"requests": [ ... ]}'
Each cell update is a pair of requests:
{"deleteContentRange": {"range": {"startIndex": START, "endIndex": END, "tabId": "t.pkxlpnoddcho"}}},
{"insertText": {"location": {"index": START, "tabId": "t.pkxlpnoddcho"}, "text": "NEW_VALUE"}}
When updating multiple packages, re-fetch the document between each
package update because the indices shift after each batchUpdate.
Step 8 — Report Results
For each package, report the completion status:
### `<package-name>` — Reassignment Complete
| System | Status |
|--------|--------|
| `owners-mapping.md` | ✓ Updated |
| `drive-sharing-plan.md` | ✓ Updated |
| `routed-results.md` | ✓ Appended row <N> |
| Google Drive | ✓ Removed: old1@, old2@ · Added: new1@, new2@ |
| Google Doc | ✓ Row <N> updated |
**LDAP Verification:**
- `kerberos_id` (Full Name) — active employee ✓
Step 9 — Skill Revision Assessment
After completing the reassignment, briefly assess whether the
assign-findings-owners skill should be revised to have caught this
reassignment automatically. Apply this decision tree:
-
Was the new owner discoverable via the skill's data sources?
(owners.csv, CI OWNERS, org repo team YAML, GitHub contributors)
- If yes → the skill has a gap; describe what heuristic or source
would have surfaced this person
- If no → no skill revision needed; this is an organizational decision
-
Was the original assignment incorrect?
- If the original owner was from a shared dependency (prometheus,
oauth-proxy, etc.) → the skill's filtering may need improvement
- If the original owner was correct but is being replaced for
organizational reasons → no skill revision needed
-
Was there a naming ambiguity? (e.g., package name suggests a
different product than what it actually contains)
- If yes → suggest a warning heuristic in the skill
Report the assessment in 2–3 sentences. Do not suggest changes to
the assign-findings-owners skill unless there is a concrete,
actionable improvement.
Reference
File Locations (Relative to Workspace Root)
| File | Purpose |
|---|
progress-tracker/tracking/owners-mapping.md | Canonical owner assignments (489+ rows) |
progress-tracker/tracking/drive-sharing-plan.md | Folder → Drive ID + owner email mapping |
progress-tracker/tracking/routed-results.md | Immutable distribution audit log |
ai-security-harness/scripts/validate_employee.py | LDAP employee verification script |
Google Workspace Resources
| Resource | ID |
|---|
| Shared Drive | 0AImGJ-PC76XKUk9PVA |
| Ownership Tracker Doc | 1mjzY8IAAEjab63AKcX-K_NYmVmHZT56j4kUPz5eflTQ |
| Ownership Tab | t.pkxlpnoddcho |
gws CLI Quick Reference
gws drive permissions list --params '{"fileId": "ID", "supportsAllDrives": true, "fields": "permissions(id,emailAddress,role,type)"}'
gws drive permissions delete --params '{"fileId": "ID", "permissionId": "PERM_ID", "supportsAllDrives": true}'
gws drive permissions create --params '{"fileId": "ID", "supportsAllDrives": true, "sendNotificationEmail": true, "emailMessage": "MSG"}' --json '{"role": "commenter", "type": "user", "emailAddress": "EMAIL"}'
gws docs documents get --params '{"documentId": "DOC_ID", "includeTabsContent": true}'
gws docs documents batchUpdate --params '{"documentId": "DOC_ID"}' --json '{"requests": [...]}'
Gotchas
-
Kerberos IDs ≠ GitHub usernames. Never pass a GitHub username to
validate_employee.py. Resolve the Kerberos ID first. A NOT found
result usually means you passed a GitHub username.
-
Kerberos ID patterns are inconsistent. Some are first-initial +
truncated-last-name (rhryniew), some are first-initial + full-last
(jsafrane), some match the GitHub username (jcantril). Try multiple
patterns in LDAP when resolving from a name.
-
Drive notification emails cannot be recalled. Once sendNotificationEmail
is true and the create call succeeds, the recipient gets an email
immediately. Always verify LDAP first.
-
Google Doc indices shift after every edit. When updating multiple
rows in the same batchUpdate, process from highest index to lowest.
When updating multiple packages, re-fetch the document between packages.
-
Never remove organizer/writer/fileOrganizer permissions. Those belong
to Glasswing team administrators. Only remove commenter permissions
belonging to the old owners being replaced.
-
routed-results.md is append-only. Never modify existing rows — they
form an audit trail. Always append new sections at the bottom.
-
Ownership changes must sync to the external tracker. The Google Doc
update (Step 7) is mandatory, not optional. It is the externally-shared
reference document that recipients use.
-
GitHub profile company field is unreliable. Contractors, recent hires,
and employees who haven't updated their profiles may show non-Red-Hat
companies. LDAP is the authoritative source for employment status.
-
Single-owner packages. When only one new owner is specified, set the
second owner columns to — (em dash) in all files. This is a valid
state — not all packages require two owners.