v12.8 — a machine can prove who it is without holding a secret
Full release tree at commit 5decaca38d5f8a886ba7883e06dae3867739327e of the source repository, published by the product's own gh_commit reading each blob straight out of its own git store. MANIFEST.txt lists every file and its digest. 103 files. Seven paths move; the other 96 are byte identical to 12.7.
An agent no longer pastes a session key
This install's MCP connectors are account-level: one OAuth bearer serves every chat, so the server has no signal that tells one chat from another, and a chat proves which strain it is by presenting a server-minted session key as the agent argument on every call. That is right for a person in a chat and it does not change here. It is wrong for a machine that is one strain forever, because it makes that machine carry and rotate a shared secret to say a thing Google can already attest.
So: a caller presenting a Google-signed service-account ID token now resolves to the strain that service account is bound to, with nothing pasted.
The verifier is not new. oaStrainFromOidc has always checked the token through Google's tokeninfo, pinned the audience to MCP_PUBLIC_URL, required a .iam.gserviceaccount.com subject and mapped it to an active strain. It was unreachable in practice for one reason: nothing ever wrote the field it reads. This release adds the writer and teaches the resolver to tell the two bearer kinds apart.
/api/strains/provisiontakes an optionalsa_email— shape-guarded to a service-account address, lower-cased to match tokeninfo, empty string clears it.oaBearerRolesplits intooaBearerIdentityreturning{role, oidc}. An OAuth connector token isoidc:false; a Google-attested token isoidc:true; onlyoidc:trueskips the paste. An account-level connector bearer still gets exactly the denial it got before, because one bearer shared across every chat must never resolve a keyless call to a live toolset.
It grants nothing new
The resolved strain is admitted through the same buildMcpServerAdmitted path as any pasted key, tool_classes and all. A presented key still wins, and a presented key that is wrong still fails closed rather than falling back — that is [FAIL-CLOSED-ON-BAD-KEY] and it is untouched. What changed is only what counts as proof: possession of a minted key for a person; possession of an identity IAM already vouches for, for a machine.
Proven as an A/B, on both lanes, before any traffic moved
The same token and the same call, sent to the new revision and to the one it replaces:
new revision → ROLE: <the bound strain>
old revision → DENIED: This chat has not established an identity yet.
A tagged Cloud Run revision keeps the previous code serving on its own URL, so the control is one request away and the claim is a measurement rather than an inference. Run on dev, then again on production.
What it is for
An agent deployed on a managed runtime — Google's Agent Runtime, Cloud Run, a build step — runs as a service account. Bind that account to a strain and the agent is that strain: it holds exactly the tool classes the strain holds, it is attributed as the strain in the journal, and there is no key in its image, its config, or its chat transcript to leak or rotate. The first agent built on this release answers whoami as its strain in three seconds with nothing pasted anywhere.
Verification
Cut deterministically: gen.py run twice into separate directories, diff -r reports zero. The same image digest served the dev lane before it served production. Bus gate 0 (negative control 4/4); passkey gate 0 (negative control 7/7, four asserted allowances). check-apis, check-python-names, check-no-embedded-assets and bash -n on all three scripts green against the emitted tree, not the source.
Apache-2.0. Your project, your bill, your key.