# 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](https://github.com/williamsmt), [Charles He](https://github.com/googlecharles)**

## 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](https://docs.cloud.google.com/gemini/enterprise/docs/reference/rest/v1beta/SubscriptionTier).

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`.

```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
- `roles/run.builder` - (Optional) When using Cloud Build to build the artifacts
- `roles/storage.objectUser` - (Optional) When using Cloud Build, to store the built artifact

**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)
- Cloud Run Admin API (Optional)
- Cloud Build API (Optional)
- Compute Engine API (Optional)
- Secret Manager API (Optional)
- Cloud Scheduler API (Optional)

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

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

## Running tests

```bash
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](https://docs.cloud.google.com/run/docs/create-jobs).
```bash
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 \
  --service-account [desired_service_account_email_address]
```

## 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:

```json
{"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 per exhausted pool and the job continues:

```json
{"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}
```

If the billing account has multiple active subscriptions for the same SKU, users who could not be seated in one pool are automatically carried forward to the next. A warning fires for each exhausted pool, but the `licenses_soft_failed` count in the final summary reflects only users who remained unseated after all pools for that SKU were tried.

```json
{"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.
