Skip to content

[oss-candidate] Fixed gam whatis exiting 15 when only the User Invitations Cloud Identity scope is authorized (#1934) - #1

Closed
askalf wants to merge 1 commit into
mainfrom
fix/userinvitations-extra-scopes
Closed

askalf wants to merge 1 commit into
mainfrom
fix/userinvitations-extra-scopes

Conversation

@askalf

@askalf askalf commented Sep 24, 2026 •

Copy link
Copy Markdown

Summary

  • gam whatis <email> on an address that does not exist exits 15 (NO_SCOPES_FOR_API_RC) and prints ERROR: There are no scopes authorized for the API(s): Cloud Identity API - User Invitations, even though the admin has authorized the User Invitations scope. Expected: exit 59 (ENTITY_IS_UKNOWN_RC). Issue 'gam whatis' exit status is incorrect for non-existent entries GAM-team/GAM#1934.
  • Cause: buildGAPIObject (src/gam/__init__.py:5756-5768) accepts a client's scopes for an API only if they appear in the discovery document's auth.oauth2.scopes or in API.EXTRA_SCOPES[api]. The Cloud Identity v1 discovery document (checked at revisions 20260826 and 20260920) does not list cloud-identity.userinvitations or cloud-identity.userinvitations.readonly, and EXTRA_SCOPES has no entry for CLOUDIDENTITY_USERINVITATIONS. So the scope GAM's own gam oauth create menu offers for this API (glapi.py:422-425) never counts, and GAM exits before calling the API.
  • Fix: add the two User Invitations scopes to EXTRA_SCOPES, the dict whose comment reads "Scopes not in the discovery doc that are still valid for the API". Google's REST reference for customers.userinvitations.isInvitableUser lists exactly these two scopes. The diff is 2 lines in src/gam/gamlib/glapi.py.
  • This also fixes gam print|show|info userinvitation(s) and gam send|cancel userinvitation, which build the same API object and failed the same way (case C6 below).
  • The workaround in the issue thread (authorize any other Cloud Identity scope, e.g. 19r Groups) works only because it makes the intersection non-empty. On base and head it gives the same result (C4).
$ python repro_whatis.py <BASE dac28c15>/src discovery
ERROR: There are no scopes authorized for the API(s): Cloud Identity API - User Invitations
API calls: ['GET https://admin.googleapis.com/admin/directory/v1/users/nosuchentity%40example.com', 'GET https://admin.googleapis.com/admin/directory/v1/groups/nosuchentity%40example.com']
Exit status was 15

$ python repro_whatis.py <HEAD e10a6308>/src discovery
Email Address: nosuchentity@example.com, Does not exist
API calls: ['GET https://admin.googleapis.com/admin/directory/v1/users/nosuchentity%40example.com', 'GET https://admin.googleapis.com/admin/directory/v1/groups/nosuchentity%40example.com', 'GET https://cloudidentity.googleapis.com/v1/customers/my_customer/userinvitations/nosuchentity@example.com:isInvitableUser']
Exit status was 59

Upstream

  • Repo: GAM-team/GAM, default branch main
  • Base sha: dac28c15f557a9a7b92546acd11b1f53d431a688 ("chore: upgrade PyPi deps (Upgrade PyPi deps GAM-team/GAM#1990)"), version 7.48.12. Re-fetched at hand-off and still the tip of origin/main.
  • Head sha: e10a630884614f01d0e180a3ac5cecdaf2762a8e on fix/userinvitations-extra-scopes
  • Changed: src/gam/gamlib/glapi.py, EXTRA_SCOPES (line 144)
  • Code path: doWhatIs (src/gam/__init__.py:13427) → _getIsInvitableUser (:49256) → _getCIUserInvitationsEntity (:49242) → buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS) (:5756), which exits at :5768

Bug

The trigger is an OAuth client whose only Cloud Identity scope is cloud-identity.userinvitations (or .readonly), which is what an admin gets when they select only "Cloud Identity API - User Invitations" in gam oauth create. gam whatis on an address that is not a user, alias or group reaches the invitable-user check, calls buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS) and exits 15 with the misleading "no scopes authorized" error. The scope is authorized, so the gam oauth update advice in the issue thread does not help, and the reporter's comment on 7.46.11 confirms this. The same thing happens for every User Invitations command (print|show|info|send|cancel userinvitation(s)). Scripts that branch on gam whatis exit codes (56/59 for "does not exist") get 15 instead. Anyone who authorized the default scope set is not affected, because Cloud Identity Groups is on by default (glapi.py:400-403, no offByDefault), which puts cloud-identity.groups into the intersection. That is why the bug looks "obscure", in the maintainer's word.

Repro

This repo has no unit-test suite; CI runs live gam commands against a real tenant. So the repro runs the real command in-process against a fake Google backend: httplib2.Http.request is replaced so that discovery documents come from the live snapshots and API calls get canned responses. Nothing in GAM is patched. The script and discovery snapshots are in /agent-output/oss/GAM/ (repro_whatis.py, sha256 abe6a674…; discovery/cloudidentity-v1.json revision 20260920, discovery/admin-directory_v1.json).

export HOME=/agent-workspace/tmphome TMPDIR=/agent-workspace/tmp
python repro_whatis.py <GAM checkout>/src discovery [extra-scope ...]
# env: GAMCMD="<gam args>" (default "whatis nosuchentity@example.com noinfo"),
#      INVITABLE=1 (isInvitableUser returns true), UISCOPE=<scope replacing userinvitations>

Verbatim output on base (dac28c1):

--- [BASE] C1 whatis noinfo, scope userinvitations, not invitable
ERROR: There are no scopes authorized for the API(s): Cloud Identity API - User Invitations
API calls: ['GET https://admin.googleapis.com/admin/directory/v1/users/nosuchentity%40example.com', 'GET https://admin.googleapis.com/admin/directory/v1/groups/nosuchentity%40example.com']
Exit status was 15

This matches the issue text exactly (message and exit status 15).

Fix

 # Scopes not in the discovery doc that are still valid for the API.
 EXTRA_SCOPES = {
+  CLOUDIDENTITY_USERINVITATIONS: ['https://www.googleapis.com/auth/cloud-identity.userinvitations',
+                                  'https://www.googleapis.com/auth/cloud-identity.userinvitations.readonly'],
   CLOUDRESOURCEMANAGER: ['https://www.googleapis.com/auth/cloudplatformfolders',

EXTRA_SCOPES is the mechanism upstream already uses for this exact situation (Cloud Resource Manager and Vault scopes missing from discovery; Business Account Management used it until ddda059). Both scopes are needed: _CLIENT_SCOPES offers the scope with 'subscopes': READONLY, so the read-only variant is a real client choice.

Alternatives rejected:

  • Add CLOUDIDENTITY_USERINVITATIONS to SCOPELESS_APIS. This removes the check altogether: a client with no User Invitations scope at all would skip the clear error and go on to a 403 from the API. Mutant M3 below shows it passing C7 (no scope → 59 instead of 15).
  • Map the API onto another Cloud Identity entry or change buildGAPIObject. That is a larger change to shared code, when the data table is the documented extension point.
  • Change doWhatIs to catch the exit. systemErrorExit calls sys.exit. Catching it in one command would hide the problem for whatis only and leave print|send|cancel userinvitation(s) broken.

Test evidence

Upstream has no test suite to extend (the old *_test.py modules were deleted in 03917fb, and the recent outside PRs GAM-team#1912 and GAM-team#1764 touch no tests). So the regression evidence is the executable A/B matrix below. Each case runs the unmodified command path on BASE (dac28c1) and HEAD (e10a630). The script is /agent-output/oss/GAM/ab.sh and the full transcript is /agent-output/oss/GAM/ab-matrix.txt.

Case Input BASE HEAD Role
C1 whatis x noinfo, scope userinvitations, not invitable 15, "no scopes authorized", isInvitableUser not called 59, "Does not exist", isInvitableUser called discriminating (the issue)
C2 same, scope userinvitations.readonly 15 59 discriminating (readonly subscope)
C3 same, scope userinvitations, isInvitableUser → true 15 24 (ENTITY_IS_AN_UNMANAGED_ACCOUNT_RC), "User Invitation: …" discriminating (other branch of the check)
C6 print userinvitations, scope userinvitations 15, no API call 0, list call made discriminating (sibling command, same API object)
C8 send userinvitation x, scope userinvitations.readonly only 15, no API call 12 (API_ACCESS_DENIED_RC), API 403 → "authorized for … scopes: cloud-identity.userinvitations" discriminating (a write with only readonly now reaches the API and gets GAM's specific missing-scope message)
C4 C1 + cloud-identity.groups.readonly (issue workaround) 59 59 control: the workaround path is unchanged
C5 whatis x noinfo noinvitablecheck 59 59 control: the check is skipped, so the build is never reached
C7 whatis x noinfo, no User Invitations scope at all 15 15 control: a genuinely missing scope still gets the "no scopes authorized" error

Verbatim HEAD output for C1:

--- [HEAD] C1 whatis noinfo, scope userinvitations, not invitable
Email Address: nosuchentity@example.com, Does not exist
API calls: ['GET https://admin.googleapis.com/admin/directory/v1/users/nosuchentity%40example.com', 'GET https://admin.googleapis.com/admin/directory/v1/groups/nosuchentity%40example.com', 'GET https://cloudidentity.googleapis.com/v1/customers/my_customer/userinvitations/nosuchentity@example.com:isInvitableUser']
Exit status was 59

Mutants of the fixed glapi.py (/agent-output/oss/GAM/mutants.sh, transcript mutants.txt), run on C1/C2/C7:

Mutant C1 C2 C7 Killed by
M1: only the full scope (drop .readonly) 59 15 15 C2
M2: only .readonly (drop full scope) 15 59 15 C1
M3: no EXTRA_SCOPES entry, API added to SCOPELESS_APIS 59 59 59 C7

Lint/format: the upstream has no linter config or lint CI step (pyproject.toml and .github/workflows/build.yml name none). python3 -m py_compile src/gam/gamlib/glapi.py passes, and the entry follows the 2-space indent and continuation alignment of the neighbouring CLOUDRESOURCEMANAGER entry.

Verification method

executed. The runtime was Linux container, Python 3.14.7, venv holding the pyproject.toml pins (google-api-python-client 2.200.0, google-auth 2.58.0, httplib2 0.32.0, …). The real gam.ProcessGAMCommand path ran with only the HTTP transport faked. The discovery documents are live snapshots from cloudidentity.googleapis.com / admin.googleapis.com taken 2026-09-24, and the scope list was checked against Google's published reference for isInvitableUser.

Not verified here: a call against a real Workspace tenant. That needs an admin with a User-Invitations-only client, which is the reporter's setup in GAM-team#1934. Fork CI has not run: Actions is not yet enabled on sprayberry-code/GAM, and upstream CI needs tenant credentials that fork runs do not have.

Prior art

Policy

Checked at main @ dac28c1 via gh api repos/GAM-team/GAM/contents/<path>: CONTRIBUTING.md, AGENTS.md, .github/PULL_REQUEST_TEMPLATE.md, .github/CONTRIBUTING.md, CODE_OF_CONDUCT.md, AI_POLICY.md, .github/AI_POLICY.md, AI.md, AGENT_POLICY.md and CLAUDE.md are all absent (404) at that ref. .github/ holds only ISSUE_TEMPLATE*, actions, stale.yml and workflows. Upstream has no written contribution policy, no AI policy (silent), no CLA, no DCO, no required formatter/linter and no test command.

Observed convention: maintainer commits are plain sentences ("Fixed bug in gam print orgs allfields that caused a trap."), and the commit here follows that. Maintainers record user-visible changes in src/GamUpdate.txt under a version heading; outside PRs (GAM-team#1912, GAM-team#1764) do not touch it, so this diff leaves it alone for the maintainer's release note.

Disclosure facts for the operator

  • An AI coding agent picked issue 'gam whatis' exit status is incorrect for non-existent entries GAM-team/GAM#1934 from a scouting pass, read the issue and code, and located the cause (EXTRA_SCOPES missing the User Invitations scopes that discovery omits).
  • The agent wrote the 2-line fix and the commit message.
  • The agent wrote the offline repro harness (repro_whatis.py, ab.sh, mutants.sh), ran the base/head matrix and mutants, and wrote this facts sheet.
  • No human has yet run the change against a real Workspace tenant.

Boundaries

The diff adds one dict entry, which feeds one expression: API_Scopes = set(discovery_scopes + EXTRA_SCOPES.get(api, [])) intersected with the credential scopes, then the not ...CURRENT_CLIENT_API_SCOPES truthiness check at __init__.py:5767.

# Input Fixed behaviour Pinned by
B1 Credentials hold userinvitations only (among CI scopes) intersection = {userinvitations} → proceeds C1, C3, C6
B2 Credentials hold userinvitations.readonly only intersection = {…readonly} → proceeds C2, C8
B3 Credentials hold neither User Invitations scope (intersection empty, the falsy case) exits 15, "no scopes authorized", unchanged C7 (control), kills M3
B4 Credentials hold userinvitations + another discovery-listed CI scope non-empty on both arms, unchanged C4 (control)
B5 noinvitablecheck given buildGAPIObject is never called, unchanged C5 (control)
B6 Readonly scope only, write command (send) now reaches the API; the API returns 403 → GAM's existing ClientAPIAccessDeniedExit names the missing full scope (exit 12) C8
B7 isInvitableUser true vs false 24 vs 59 C3 vs C1
B8 Other APIs (CLOUDIDENTITY_GROUPS, _DEVICES, _POLICY, the CRM/Vault entries) EXTRA_SCOPES.get unchanged for every other key (printed in mutants.txt) measured: the dict now has one more key, nothing else changes
B9 DASA mode (enable_dasa) the if not GC.Values[GC.ENABLE_DASA] block is skipped, so EXTRA_SCOPES is never read unreachable by the change: its only reader is inside that block (__init__.py:5761-5763)
B10 A future discovery revision that lists the scopes set(...) dedups, so no behaviour change by construction (set() over the concatenation)
B11 Service-account paths (getSvcAcctCredentials) do not read EXTRA_SCOPES (single reader at :5763) grep: one reader

Suggested upstream PR title

Fixed gam whatis and User Invitations commands failing with "no scopes authorized" when only the User Invitations scope is authorized (GAM-team#1934)

…gam whatis`, failed with "There are no scopes authorized" when the only Cloud Identity scope authorized was User Invitations. GAM-team#1934
@askalf askalf added the oss-candidate Sprayberry Code candidate for upstream label Sep 24, 2026
@askalf
askalf marked this pull request as ready for review September 24, 2026 03:27
@askalf askalf added the verified Adversarially verified by a fresh run label Sep 25, 2026
@askalf

askalf commented Sep 25, 2026

Copy link
Copy Markdown
Author

Verification

Adversarial re-verification at head e10a630884614f01d0e180a3ac5cecdaf2762a8e (unchanged since submission). Diff re-confirmed as src/gam/gamlib/glapi.py +2/-0, adding a CLOUDIDENTITY_USERINVITATIONS entry to EXTRA_SCOPES (only reader: buildGAPIObject, __init__.py:5763).

Superseded check

Upstream issue GAM-team#1934 is still OPEN (stateReason: REOPENED), and git log origin/main --since base -- src/gam/gamlib/glapi.py is empty since dac28c15. Not superseded.

Re-ran the Hunter's evidence myself (all matched)

$ sh rv-ab.sh    # ab.sh with HEAD pointed at my own worktree /agent-workspace/oss/GAM-wt-verify

C1–C7 (see rv-ab-matrix.txt) reproduce the body's matrix exactly: base 15 ("no scopes authorized")/head 59 for C1/C2, 24 for C3 (isInvitableUser=true), print (C6) base 15/head 0, control C4/C5/C7 unchanged both arms.

C8 (send userinvitation, readonly-only scope) re-run directly: base 15, head 12 (API_ACCESS_DENIED_RC, GAM's own missing-scope message) — matches.

Mutants (rv-mutants.sh against a scratch copy of my own head worktree):

  • M1 (drop .readonly from the entry): C2 regresses to 15 — killed by C2.
  • M2 (drop the full scope, keep only .readonly): C1 regresses to 15 — killed by C1.
  • M3 (rejected alternative: no EXTRA_SCOPES entry, add API to SCOPELESS_APIS): C7 wrongly proceeds to 59 instead of 15 — killed by C7.
  • Unaffected-keys check: CLOUDIDENTITY_GROUPS/_DEVICES/_POLICY EXTRA_SCOPES.get() still None at head — B8 holds.

New adversarial checks (not in the original body, both hold)

  1. B9 (DASA) measured, not just argued. Built a gam.cfg with enable_dasa = true (+ customer_id/domain/admin_email set, since enable_dasa requires them) and ran whatis on both arms: both fail identically at "Service Account OAuth2 File … Does not exist" (exit 16), never reaching EXTRA_SCOPES — confirms buildGAPIObject's if not GC.Values[GC.ENABLE_DASA]: guard really does skip the changed code under DASA, on both arms alike.
  2. B11 (service-account path) confirmed by grep, not inference. grep -n EXTRA_SCOPES src/gam/__init__.py → exactly one hit, inside buildGAPIObject; buildGAPIServiceObject (the DASA/service-account builder) never reads it.
  3. New command surface, same boundary (B1). gam <user> check isinvitable (checkCIUserIsInvitable, a caller of buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS) not in the original C1–C8 matrix) with isInvitableUser=true: base exits 15 ("no scopes authorized"), head exits 0 and prints the invitable user. Same boundary as B1/B7, exercised through a fourth command — no new row needed, but it rules out a command-specific fix.

Rejected-alternatives and control audit

  • mutate-the-rejected-alternatives: all three prose-rejected alternatives (drop-readonly, drop-full, SCOPELESS_APIS) built as mutants and each killed by a named case (see above). None killed by nothing, none killed by everything.
  • no-control-cases-in-the-suite / ledger-row-needs-its-fixture: C4, C5, C7 (declared controls) all measured PASSING on base as claimed — not silently wrong.
  • prior-art-recheck-at-gate: re-run at this gate (git log on origin/main since base, touching glapi.py) — empty. No competing fix landed.

Lint / compile

python3 -m py_compile src/gam/gamlib/glapi.py — clean. No linter/formatter configured upstream (checked pyproject.toml, .github/workflows/build.yml). Fork Actions not enabled on sprayberry-code/GAM (gh pr checks 1 → no checks reported); this is fork infrastructure, not a defect in the change.

Verification method

executed. Env: export HOME=/agent-workspace/tmphome TMPDIR=/agent-workspace/tmp; /agent-workspace/oss/gamvenv/bin/python (3.14.7). Arms: base worktree /agent-workspace/oss/GAM-base-dac28c15 (dac28c1), head worktree /agent-workspace/oss/GAM-wt-verify (e10a630). All repro/mutant scripts and transcripts are in /agent-output/oss/GAM/ (rv-ab.sh, rv-ab-matrix.txt, rv-mutants.sh, rv-mutants.txt, rv-dasa-repro.py, rv-checkisinvitable.py).

Not verified here (as the body already discloses): a call against a real Workspace tenant — needs an admin with a User-Invitations-only client, the reporter's setup in GAM-team#1934.

Rules: mutate-the-rejected-alternatives=covered (M1/M2/M3 above) | no-control-cases-in-the-suite=covered (C4/C5/C7 re-run on base) | ledger-row-needs-its-fixture=covered (B9 measured with its own cfg, not argued) | prior-art-recheck-at-gate=covered (git log re-run at this gate) | static-row-vs-alias-stub=unreachable (no static rows in this body; upstream has no unit-test infra to alias into)

@sprayberry-redline sprayberry-redline 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.

Automated review from the Sprayberry Labs fleet code reviewer.
Reviewed by the gating lane (gating review).

Verdict: approve. Ready for the operator to submit upstream. Reviewed at head e10a630884614f01d0e180a3ac5cecdaf2762a8e against base dac28c15f557a9a7b92546acd11b1f53d431a688.

What I checked

Bug is real on the base. I fetched src/gam/__init__.py at the base sha and read buildGAPIObject (src/gam/__init__.py:5756-5768):

    discovery_scopes = list(service._rootDesc.get('auth', {}).get('oauth2', {}).get('scopes', {}).keys())
    extra_scopes = API.EXTRA_SCOPES.get(api, [])
    API_Scopes = set(discovery_scopes + extra_scopes)
    ...
    GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API_Scopes.intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
    if api not in API.SCOPELESS_APIS and not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
      systemErrorExit(NO_SCOPES_FOR_API_RC, Msg.NO_SCOPES_FOR_API.format(API.getAPIName(api)))

I downloaded the live Cloud Identity v1 discovery document myself (revision 20260923, newer than the body's 20260920 snapshot). Its auth.oauth2.scopes block lists 12 scopes (allowlisteddomains, devices, groups, inboundsso, policies, their .readonly variants, devices.lookup, cloud-platform) and contains zero occurrences of cloud-identity.userinvitations; the isInvitableUser method carries no scopes list either. On the base, EXTRA_SCOPES (src/gam/gamlib/glapi.py:144-153) has only CLOUDRESOURCEMANAGER and VAULT. So for a client whose Cloud Identity scopes are exactly {cloud-identity.userinvitations} the intersection is empty, CLOUDIDENTITY_USERINVITATIONS is not in SCOPELESS_APIS, and buildGAPIObject exits 15 before _getIsInvitableUser (:49256) ever issues the isInvitableUser call. That is the issue's symptom exactly.

The fix. The two added lines put both User Invitations scopes into the set the intersection is taken over. _CLIENT_SCOPES offers this API with 'subscopes': READONLY (glapi.py:422-425), and the oauth flow adds f'{scope["scope"]}.readonly' for such entries (__init__.py:11428), so the .readonly variant is a real credential scope and needs to be listed too. The entry sits in alphabetical position and matches the neighbouring entry's indentation. A client with neither scope still has an empty intersection and still gets the 15 (the body's C7 control, which also kills the SCOPELESS_APIS alternative M3). set() over the concatenation means a future discovery revision that adds the scopes changes nothing.

Test evidence. Upstream has no unit-test suite (CI runs live gam against a tenant), so the evidence is the in-process A/B harness with the HTTP transport faked. The body carries verbatim base and head output for C1 (exit 15 / exit 59, with the isInvitableUser call visible only on head), a five-discriminating-case plus three-control matrix, and three mutants each killed by a named case. The Breaker's verification comment at this head re-ran the matrix and mutants and matched, measured the DASA row (B9) with a real gam.cfg instead of arguing it, confirmed by grep that EXTRA_SCOPES has a single reader, and drove a fourth command (gam <user> check isinvitable) through the same boundary.

Boundaries. The diff changes one dict lookup feeding one truthiness check. Ledger rows B1 (full scope only), B2 (readonly only), B3 (neither, falsy), B4 (plus another CI scope), B5 (noinvitablecheck), B6 (readonly-only write now reaches the API and gets GAM's own 403 message, exit 12), B7 (invitable true/false), B8 (other API keys untouched), B9 (DASA skips the block), B10 (future discovery listing), B11 (service-account path) cover every input I could construct for that check.

Prior art, re-run. gh search prs --repo GAM-team/GAM for userinvitations, no scopes authorized, EXTRA_SCOPES, 1934, whatis: only merged 2023 PRs (GAM-team#1329, GAM-team#1339, GAM-team#1355, GAM-team#1368, GAM-team#1481, GAM-team#214). Open upstream PRs are GAM-team#1978 (MCP server) and GAM-team#1877 (homebrew workflow). compare dac28c15...main shows 3 commits, all touching only .github/workflows/build.yml; EXTRA_SCOPES at main is unchanged. Issue GAM-team#1934 is open (state_reason: reopened). No competing fix.

Policy. I confirmed CONTRIBUTING.md, AGENTS.md, .github/PULL_REQUEST_TEMPLATE.md and .github/CONTRIBUTING.md all 404 at main. No AI policy, no DCO, no linter. The single commit message ("Fixed bug where Cloud Identity User Invitations commands, including gam whatis, failed with ... GAM-team#1934") matches the maintainers' plain-sentence "Fixed bug in ..." style. No AI attribution in the commit, branch or title.

Hygiene and generated-text pass. One bug, +2/-0, no unrelated changes, no comments added. No em dashes or filler in the diff, commit message, title or body.

Fork CI. gh pr checks reports no checks (Actions not enabled on the fork); upstream CI needs tenant credentials a fork cannot have, so this is absence, not failure.

Notes for the operator

  • Not verified against a real Workspace tenant; the body says so. If you have a client authorized only for "Cloud Identity API - User Invitations", gam whatis nosuchentity@yourdomain noinfo should exit 59 after this change instead of 15.
  • Maintainers usually commit directly and record user-visible fixes in src/GamUpdate.txt themselves; outside PRs do not touch it, so leaving it alone is right.

@askalf askalf added ready-for-operator Gated; operator submits upstream submitted Submitted upstream labels Sep 25, 2026
@askalf

askalf commented Sep 25, 2026

Copy link
Copy Markdown
Author

Submitted upstream for review.

@askalf askalf closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

oss-candidate Sprayberry Code candidate for upstream ready-for-operator Gated; operator submits upstream submitted Submitted upstream verified Adversarially verified by a fresh run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unauthorized_client error when running whatis for non-existent account

2 participants