. | `map(string)` | `{}` | no |
| [image\_repository](#input\_image\_repository) | Artifact Registry repository the Reasoning Engine service agent pulls BYOC images from. Null skips the grant. | object({
project = string
location = string
name = string
}) | `null` | no |
| [project\_id](#input\_project\_id) | The project the agents run in. | `string` | n/a | yes |
| [secret\_readers](#input\_secret\_readers) | Extra members that can read the secrets bag, for example a workgroup in nonprod. | `list(string)` | `[]` | no |
diff --git a/google_agent_platform/locals.tf b/google_agent_platform/locals.tf
index 8773c070..efb60a31 100644
--- a/google_agent_platform/locals.tf
+++ b/google_agent_platform/locals.tf
@@ -3,7 +3,7 @@ locals {
deploy_principals = [
for repo in var.deploy_repositories : var.deploy_github_environment != null ?
- "principal://iam.googleapis.com/${local.wip_id}/subject/repo:${repo}:environment:${var.deploy_github_environment}" :
+ "principal://iam.googleapis.com/${local.wip_id}/subject/${lookup(var.github_subject_prefixes, repo, "repo:${repo}")}:environment:${var.deploy_github_environment}" :
"principalSet://iam.googleapis.com/${local.wip_id}/attribute.repository/${repo}"
]
diff --git a/google_agent_platform/main.tf b/google_agent_platform/main.tf
index 9ed91c6e..8fa91e1c 100644
--- a/google_agent_platform/main.tf
+++ b/google_agent_platform/main.tf
@@ -1,3 +1,29 @@
+/**
+ * # google_agent_platform
+ *
+ * ## GitHub immutable OIDC subjects
+ *
+ * GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims.
+ * Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches.
+ * This affects the deployer service account trust when a GitHub environment is required.
+ *
+ * To check a repository and get its prefix, run:
+ *
+ * ```sh
+ * gh api repos///actions/oidc/customization/sub
+ * ```
+ *
+ * When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`:
+ *
+ * ```hcl
+ * github_subject_prefixes = {
+ * "example-org/example-repo" = "repo:example-org@123456/example-repo@456789"
+ * }
+ * ```
+ *
+ * See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs.
+ */
+
data "google_project" "this" {
project_id = var.project_id
}
diff --git a/google_agent_platform/tests/agent_platform.tftest.hcl b/google_agent_platform/tests/agent_platform.tftest.hcl
index 949777d0..cf0b9071 100644
--- a/google_agent_platform/tests/agent_platform.tftest.hcl
+++ b/google_agent_platform/tests/agent_platform.tftest.hcl
@@ -75,6 +75,21 @@ run "prod_requires_github_environment" {
}
}
+run "prod_immutable_subject_prefix" {
+ command = plan
+
+ variables {
+ environment = "prod"
+ deploy_github_environment = "prod"
+ github_subject_prefixes = { "example-org/agents" = "repo:example-org@444/agents@555" }
+ }
+
+ assert {
+ condition = one(google_service_account_iam_binding.deployer_wif[0].members) == "principal://iam.googleapis.com/projects/333333333333/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org@444/agents@555:environment:prod"
+ error_message = "Prod deploys must use the repository's immutable subject prefix when one is set."
+ }
+}
+
run "optional_resources" {
command = plan
diff --git a/google_agent_platform/variables.tf b/google_agent_platform/variables.tf
index 6bbccf2a..3d18e582 100644
--- a/google_agent_platform/variables.tf
+++ b/google_agent_platform/variables.tf
@@ -33,6 +33,21 @@ variable "deploy_github_environment" {
default = null
}
+variable "github_subject_prefixes" {
+ description = <<-EOT
+ OIDC subject prefix per deploy repository, for repositories with GitHub's immutable subject
+ claims enabled. Copy sub_claim_prefix from the repository's actions/oidc/customization/sub API.
+ Repositories not listed use repo:/.
+ EOT
+ type = map(string)
+ default = {}
+
+ validation {
+ condition = alltrue([for prefix in values(var.github_subject_prefixes) : can(regex("^repo:\\S+@\\d+/\\S+@\\d+$", prefix))])
+ error_message = "Valid values are in the following format: repo:org@org_id/repository@repository_id."
+ }
+}
+
variable "caller_repositories" {
description = "GitHub repositories, in org/repository format, that invoke agents from CI without deploying them."
type = list(string)
diff --git a/google_tenant_bootstrap/README.md b/google_tenant_bootstrap/README.md
index 876cbe0f..5b2231b8 100644
--- a/google_tenant_bootstrap/README.md
+++ b/google_tenant_bootstrap/README.md
@@ -60,7 +60,29 @@ module "lightweight_tenant" {
| [public\_ips](#output\_public\_ips) | n/a |
+# google\_tenant\_bootstrap
+## GitHub immutable OIDC subjects
+
+GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims.
+Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches.
+This affects the artifact-writer service account trust when a GitHub environment is required.
+
+To check a repository and get its prefix, run:
+
+```sh
+gh api repos///actions/oidc/customization/sub
+```
+
+When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`:
+
+```hcl
+github_subject_prefixes = {
+ "example-org/example-repo" = "repo:example-org@123456/example-repo@456789"
+}
+```
+
+See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs.
## Example
```hcl
module "tenant" {
@@ -99,6 +121,7 @@ module "lightweight_tenant" {
| [github\_deploy\_repositories](#input\_github\_deploy\_repositories) | GitHub repositories (org/repository) that deploy. Overrides `github_repository` for the
deploy service account. | `list(string)` | `[]` | no |
| [github\_repository](#input\_github\_repository) | GitHub repository (org/repository) that builds and deploys. `github_build_repositories`
and `github_deploy_repositories` override it. | `string` | `null` | no |
| [github\_require\_build\_env](#input\_github\_require\_build\_env) | Require the GitHub Actions environment "build" to use the artifact-writer service account. | `bool` | `true` | no |
+| [github\_subject\_prefixes](#input\_github\_subject\_prefixes) | OIDC subject prefix per build repository, for repositories with GitHub's immutable subject
claims enabled. Copy sub\_claim\_prefix from the repository's actions/oidc/customization/sub API.
Repositories not listed use repo:/. | `map(string)` | `{}` | no |
| [log\_retention\_days](#input\_log\_retention\_days) | Log retention in days | `number` | `90` | no |
| [project\_id](#input\_project\_id) | Tenant project ID | `string` | `null` | no |
| [realm](#input\_realm) | Realm: nonprod, prod, mgmt, or global | `string` | n/a | yes |
diff --git a/google_tenant_bootstrap/locals.tf b/google_tenant_bootstrap/locals.tf
index d1ebd516..6fd11eac 100644
--- a/google_tenant_bootstrap/locals.tf
+++ b/google_tenant_bootstrap/locals.tf
@@ -24,7 +24,7 @@ locals {
github_build_repositories = length(var.github_build_repositories) > 0 ? var.github_build_repositories : compact([var.github_repository])
github_build_principals = [
for repo in local.github_build_repositories : var.github_require_build_env ?
- "principal://iam.googleapis.com/${local.wip_id}/subject/repo:${repo}:environment:build" :
+ "principal://iam.googleapis.com/${local.wip_id}/subject/${lookup(var.github_subject_prefixes, repo, "repo:${repo}")}:environment:build" :
"principalSet://iam.googleapis.com/${local.wip_id}/attribute.repository/${repo}"
if local.writer_service_account_name != null
]
diff --git a/google_tenant_bootstrap/main.tf b/google_tenant_bootstrap/main.tf
index 3f058ca7..2565414e 100644
--- a/google_tenant_bootstrap/main.tf
+++ b/google_tenant_bootstrap/main.tf
@@ -1,3 +1,29 @@
+/**
+ * # google_tenant_bootstrap
+ *
+ * ## GitHub immutable OIDC subjects
+ *
+ * GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims.
+ * Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches.
+ * This affects the artifact-writer service account trust when a GitHub environment is required.
+ *
+ * To check a repository and get its prefix, run:
+ *
+ * ```sh
+ * gh api repos///actions/oidc/customization/sub
+ * ```
+ *
+ * When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`:
+ *
+ * ```hcl
+ * github_subject_prefixes = {
+ * "example-org/example-repo" = "repo:example-org@123456/example-repo@456789"
+ * }
+ * ```
+ *
+ * See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs.
+ */
+
module "google_gke_tenant" {
count = var.disable_shared_integrations ? 0 : 1
source = "../google_gke_tenant"
diff --git a/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl b/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl
index 54fa38dd..2431b7aa 100644
--- a/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl
+++ b/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl
@@ -83,6 +83,28 @@ run "lightweight_prod" {
}
}
+run "immutable_subject_prefix" {
+ command = plan
+
+ variables {
+ function = "aiservices"
+ realm = "prod"
+ environment = "prod"
+ project_id = "app-prod"
+ disable_shared_integrations = true
+ github_build_repositories = ["example-org/app", "example-org/legacy"]
+ github_subject_prefixes = { "example-org/app" = "repo:example-org@111/app@222" }
+ }
+
+ assert {
+ condition = google_service_account_iam_binding.artifact_writer_access[0].members == toset([
+ "principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org@111/app@222:environment:build",
+ "principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org/legacy:environment:build",
+ ])
+ error_message = "Listed repositories must use their immutable subject prefix, others the legacy form."
+ }
+}
+
run "lightweight_nonprod_without_writer" {
command = plan
diff --git a/google_tenant_bootstrap/variables.tf b/google_tenant_bootstrap/variables.tf
index 541cb4d1..14e36a25 100644
--- a/google_tenant_bootstrap/variables.tf
+++ b/google_tenant_bootstrap/variables.tf
@@ -134,6 +134,23 @@ variable "github_require_build_env" {
EOT
}
+variable "github_subject_prefixes" {
+ type = map(string)
+ default = {}
+ description = <<-EOT
+ OIDC subject prefix per build repository, for repositories with GitHub's immutable subject
+ claims enabled. Copy sub_claim_prefix from the repository's actions/oidc/customization/sub API.
+ Repositories not listed use repo:/.
+ EOT
+ validation {
+ condition = alltrue([
+ for prefix in values(var.github_subject_prefixes) :
+ can(regex("^repo:\\S+@\\d+/\\S+@\\d+$", prefix))
+ ])
+ error_message = "Valid values are in the following format: repo:org@org_id/repository@repository_id."
+ }
+}
+
variable "log_retention_days" {
type = number
description = "Log retention in days"