Files

Gemini Box Office

A Cloud Run Job that automates Gemini Enterprise license lifecycle management through scheduled batch reconciliation. It bridges the gap between Google Cloud Identity group membership and Discovery Engine license assignment, providing group-based SKU mapping, automatic provisioning, and stale-license cleanup.

Authors: Matt Williams, Charles He

How it works

Two Cloud Run Job definitions are deployed from a single container image, each triggered by Cloud Scheduler on its own schedule:

Job JOB_TYPE Schedule Description
gemini-box-office-joiner joiner Daily (24hr) Pages through all configured group members and grants missing licenses. If a user belongs to multiple SKU-mapped groups, the highest-precedence SKU wins.
gemini-box-office-gc garbage_collection Every 6 hours Pages through all licensed users and revokes licenses from users who are stale (no login within the configured threshold) or no longer a member of any entitled group.

Environment variables

Variable Description Default
JOB_TYPE Selects the workflow to run (joiner or garbage_collection). Required.
DRY_RUN When true, the full evaluation runs but no write API calls are made. Can be overridden per execution via the Cloud Scheduler request body. false
CLOUD_RUN_TASK_INDEX Injected by Cloud Run. 0-based index of this task instance. 0
CLOUD_RUN_TASK_COUNT Injected by Cloud Run. Total number of concurrent task instances. 1

SKU names and precedence

The current list of Gemini Enterprise subscription tier SKUs is here.

When a user qualifies for multiple SKUs, the highest-ranked one is assigned:

  1. SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT
  2. SUBSCRIPTION_TIER_ENTERPRISE
  3. SUBSCRIPTION_TIER_SEARCH
  4. SUBSCRIPTION_TIER_NOTEBOOK_LM
  5. SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS
  6. SUBSCRIPTION_TIER_AGENTSPACE_STARTER
  7. SUBSCRIPTION_TIER_FRONTLINE_WORKER
  8. SUBSCRIPTION_TIER_FRONTLINE_STARTER
  9. SUBSCRIPTION_TIER_ENTERPRISE_EMERGING
  10. SUBSCRIPTION_TIER_EDU_PRO
  11. SUBSCRIPTION_TIER_EDU
  12. SUBSCRIPTION_TIER_EDU_PRO_EMERGING
  13. SUBSCRIPTION_TIER_EDU_EMERGING

Configuration

Configuration is stored in GCP Secret Manager and mounted as a file volume into the job at /run/secrets/entitlements.json.

{
  "billing_account_id": "ABCDE-12345-FGHIJ",
  "projects": {
    "customer-project-alpha": [
      {
        "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE",
        "location": "global",
        "groups": [
          "group-data-scientists@example.com",
          "group-senior-devs@example.com"
        ]
      },
      {
        "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS",
        "location": "global",
        "groups": [
          "group-marketing@example.com",
          "group-general-staff@example.com"
        ]
      }
    ]
  },
  "settings": {
    "staleness_threshold_days": 30
  }
}
Field Description
billing_account_id The GCP billing account ID associated with the managed projects. Required.
projects Map of GCP project ID → list of entitlement entries (one per SKU/location combination).
projects[].subscription_tier The Gemini SKU for this entry. See SKU precedence below for all valid values.
projects[].location Geographic region for license management. Must be one of: global, us, eu.
projects[].groups List of Google Group email addresses whose members are entitled to this SKU.
settings.staleness_threshold_days Users inactive beyond this many days are revoked. Set to 0 (or omit) to disable staleness checking — only entitlement is evaluated.

Multiple projects can be managed from a single job by adding additional entries under projects. For very large project portfolios, set --tasks N on the job execution to shard the project list across N parallel task instances.

IAM, OAuth, API requirements

The job's service account requires the following:

IAM roles:

  • roles/discoveryengine.admin — list and update user licenses
  • roles/cloudidentity.groups.viewer — list group members and verify membership
  • roles/secretmanager.secretAccessor — read the mounted configuration secret
  • roles/billing.viewer — read the purchased Gemini Enterprise subscription configurations

OAuth scopes:

  • https://www.googleapis.com/auth/cloud-platform
  • https://www.googleapis.com/auth/admin.directory.group.member.readonly

API requirements:

  • Discovery Engine API (GCP)
  • Resource Manager API (GCP)
  • Admin SDK API (Cloud Identity / Workspace)

The job uses Application Default Credentials. No service account key files are required.

Trigger authorization: The Cloud Scheduler service account must be granted roles/run.invoker on each Cloud Run Job resource.

Project structure

cmd/job/             # Job entry point
internal/
  adapters/
    cloudidentity/   # Cloud Identity Admin API adapter
    discoveryengine/ # Discovery Engine API adapter
  config/            # Config loading and validation (entitlement config + job settings)
  middleware/        # Structured logging middleware
  models/            # Domain types, DTOs, errors, constants, enums
  ports/             # Interface definitions (IdpClient, GeminiClient)
  services/          # Business logic (JoinerService, GCService)
docs/
  PRD.md             # Product requirements
  TDD.md             # Technical design document
Dockerfile           # Multi-stage build → distroless runtime image

Building

docker build -t gemini-box-office:latest .

Running tests

go test ./...

All tests are unit tests and require no external services or credentials.

Deploying to Cloud Run as Job

An example command to deploy as a job on Cloud Run is below. For more information on creating Cloud Run jobs, reference documentation here.

gcloud run jobs deploy [name_of_job] \
  --source . \
  --region [desired_cloud_run_region] \
  --update-secrets=/run/secrets/entitlements.json=[name_of_secret_in_secret_manager]:latest \
  --set-env-vars JOB_TYPE=[joiner_or_garbage_collection],DRY_RUN=false

Job output

On completion the job exits 0 (success) or 1 (failure). Results are emitted as structured JSON log entries to stdout and ingested automatically by Cloud Logging:

{"time":"...","level":"INFO","workflow":"joiner","task_index":0,"msg":"joiner workflow complete","duration_ms":4821,"licenses_granted":42,"licenses_soft_failed":0,"groups_processed":5,"dry_run":false}

If the license pool for a SKU is exhausted mid-run, a warning is emitted and the job continues:

{"time":"...","level":"WARN","workflow":"joiner","task_index":0,"msg":"license pool exhausted, soft-failing remaining users","project_id":"customer-project-alpha","license_config_path":"projects/123/locations/global/licenseConfigs/ent-config","available":3,"soft_failed":17}
{"time":"...","level":"INFO","workflow":"garbage_collection","task_index":0,"msg":"garbage collection workflow complete","duration_ms":9134,"licenses_revoked":12,"users_evaluated":500,"dry_run":false}

Logging

All log output is structured JSON emitted to stdout for automatic ingestion by Cloud Logging. Every entry includes workflow and task_index fields. PII (user email addresses) is never logged.