Who talks to whom
Gemini Enterprise is Google's enterprise assistant: a chat, connectors (Drive, Gmail…) and agents. Our agents live in a separate project. The app calls them with its own service account and, when needed, with the user's token.
The chat
{{PROJET_APP}}
{{APP_ID}} ({{GE_LOCATION}}){{GE_SA}}or Agent Runtime
{{PROJET_AGENTS}}
{{K}}-a2a-shell (A2A){{K}}-shellThe user
Their Acme account. They only see an agent if they have the Agent User role. With OAuth, the agent acts with their own rights.
The app's account
{{GE_SA}} calls the agent. It needs ① run.invoker on each Cloud Run service, ② discoveryengine.serviceAgent on the agents project (Agent Runtime).
The agent's account
{{K}}-a2a-shell-sa: calls Gemini, writes its logs, goes through Model Armor. Nothing else.
Two ways to host an agent
| A2A on Cloud Run | Agent Runtime (Vertex AI) | |
|---|---|---|
| What you give the app | The agent card (JSON) | The path projects/…/reasoningEngines/… |
| App permission | ① roles/run.invoker on the service | ② roles/discoveryengine.serviceAgent on the project |
| OAuth | One authorization per agent | Several (one per provider) |
| Updates | New revision, same address | New resource: add the new path to the app |
| When to choose it | Default; open, reusable protocol | Several OAuth providers; sessions managed by Google |
The agent catalogue
One folder per agent in agents/trio/, described in catalogue.json (name, description, sample questions, and the oauth block). Scripts and Terraform read this file.
The Gemini Enterprise API (streamAssist) cannot call an A2A or ADK agent added to the app (Google documentation). A custom agent is tested in the chat, or by calling the agent directly the way the app does (Troubleshooting tab).
Deploy an agent
Everything runs from a Mac or Linux terminal. No Docker: the image is built by Cloud Build, inside Google Cloud. The app doesn't need to exist yet.
Your machine
gcloudsigned in:gcloud auth loginandgcloud auth application-default login- Python 3.12 to 3.14
- Mac without install rights:
pip install --no-deps -r agents/trio/requirements-poste.txt
Your permissions
Read-only check, groups included:
./scripts/verifier_droits.sh
The configuration
One file, config/12-1-agents-dans-projet-exp.env. The app fields can stay empty until you publish.
Load the configuration
source scripts/config.sh config/12-1-agents-dans-projet-exp.env
Prints a summary: projects, agents, and “(pas encore : agents seuls)” if the app is empty.
Prepare (once per project)
./scripts/deployer_shell.sh preparer # Model Armor templates, image repository, build accountDeploy the agent
./scripts/deployer_shell.sh cloudrun {{K}} # A2A: image (Cloud Build), account, private service
./scripts/deployer_shell.sh runtime {{K}} # and/or Agent Runtime== {{K}} (Cloud Run) adresse {{URL}}
-- 1/3 l'image, construite par Cloud Build (environ 1 min 20 s) … SUCCESS
-- 2/3 son compte de service et ses 3 rôles
-- 3/3 le service Cloud Run (privé)
Service URL: {{URL}}Test (3 to 5 minutes later)
./scripts/deployer_shell.sh tester {{K}}“OK : les agents répondent en A2A v0.3, avec mémoire de conversation”. For an OAuth agent, the reply “cliquez sur Autoriser” is expected: the test sends no user token.
The sheet for Gemini Enterprise
./scripts/deployer_shell.sh carte {{K}}Writes carte-{{K}}.json and prints everything to paste in the app: name, description, one-line card, Agent Runtime path, Authentication step, suggested questions, permissions.
The card comes from carte_a2a.py, in the format Google expects. Not the one from /.well-known/agent-card.json: it has extra fields, and Gemini Enterprise rejects it.
Re-running is safe: what exists is kept. cloudrun rebuilds the image: new revision, same address. To delete: python scripts/nettoyer.py --agent {{K}} --supprimer (shows the inventory first).
Pick the agents in the configuration
AGENTS="{{K}}" # what must EXIST (empty = the whole catalogue)
AGENTS_OAUTH="{{K}}={{AUTH}}" # only for an OAuth agentApply
source scripts/config.sh config/12-1-agents-dans-projet-exp.env cd 01-bis-cloud-run-a2a/terraform # Agent Runtime: 02-bis-agent-runtime/terraform terraform init && terraform plan # 1 agent, no app: 22 to add; +7 per agent terraform apply
for_each over the catalogue: one image per agent, and adding an agent touches no other. OAuth agents also get their Google APIs enabled.
Read what the app needs
terraform output cards # the card of each A2A agent terraform output oauth # expected authorizations + scopes terraform output agents # (02-bis) the path of each Agent Runtime agent
With PUBLIER=oui and the app values, Terraform also adds the agents to the app, authorization included.
The image
Cloud Build is started from the terminal (no console button takes our folder): gcloud builds submit … --config=01-bis-cloud-run-a2a/cloudbuild.yaml. Guide 12-1-3 has the full command.
The Cloud Run service
🔎 Cloud Run → Deploy container → the image → name {{K}}-a2a-shell → region {{REGION}} → Require authentication → the agent's service account → variables (file env-{{K}}.yaml).
Check
The service URL must be exactly {{URL}}: it is written in the card.
Click-by-click details: docs/12-1-3-deployer-agents-console.md.
Plug the agent into the app
Three things: the app account's permissions (in the agents project), adding the agent (in the app), and the users' access. Wait 2 to 3 minutes after the permissions.
Cloud Run Invoker on the service
🔎 Cloud Run → {{K}}-a2a-shell → Security → Add principal → {{GE_SA}} → role Cloud Run Invoker → Save.
gcloud run services add-iam-policy-binding {{K}}-a2a-shell \
--region={{REGION}} --project={{PROJET_AGENTS}} \
--role=roles/run.invoker --member=serviceAccount:{{GE_SA}}Discovery Engine Service Agent on the project
🔎 IAM → Grant access → {{GE_SA}} → role Discovery Engine Service Agent. Once, for all agents.
gcloud projects add-iam-policy-binding {{PROJET_AGENTS}} --condition=None \
--role=roles/discoveryengine.serviceAgent --member=serviceAccount:{{GE_SA}}See the permissions (read-only): IAM → tick “Include Google-provided role grants” → filter gcp-sa-discoveryengine. Or ./scripts/connecter_app.sh verifier. Both permissions in one command, with confirmation: ./scripts/connecter_app.sh droits {{K}}.
The card, on one line
./scripts/deployer_shell.sh carte {{K}} # or: cat carte-{{K}}.jsonIn the app
🔎 Gemini Enterprise → the app → Agents → + Add agent → Custom agent via A2A → paste the whole line, from the first { to the last } → Preview agent details → Next.
Authentication
Agent without OAuth: Skip & Finish. OAuth agent: do not click Skip, see tab 3.
The name
The agent → Edit → {{NOM}} (A2A). Or in one command, with suggested questions: tab 5.
The agent's path
./scripts/deployer_shell.sh carte {{K}} # “[Agent Runtime] projects/{{NUM_AGENTS}}/locations/{{REGION}}/reasoningEngines/…”In the app
+ Add agent → Custom agent via Agent Runtime → Authorizations: nothing (no OAuth) → Next → Agent name {{NOM}}, catalogue description, Agent Runtime resource path → Create.
New version?
Agent Runtime creates a new resource. Remove the old entry from the app and add the new path.
User access
The agent → User permissions → Add → a group (recommended) or an address → role Agent User → Save. Without this role, the agent doesn't show in the chat.
The test
Overview → web app link → Agents → {{NOM}} → “{{EX}}”. 20 to 40 s the first time (cold start).
What to send to teams that own their agents
Their agent lives in their project. They grant the permission, then send us the card or the path:
To plug your agent into the Gemini Enterprise app ({{PROJET_APP}}), please:
1. grant the account {{GE_SA}}
- roles/run.invoker on your Cloud Run service (A2A agent), or
- roles/discoveryengine.serviceAgent on your project (Agent Runtime agent);
2. send us the A2A card (JSON, one line) or the path projects/…/reasoningEngines/…;
3. if the agent acts with the user's account (OAuth): the client ID and the scopes you need (the secret through a secure channel).
Agent region: europe-* (app in {{GE_LOCATION}}).An agent acting with the user's consent
Examples: Rapport e-mails (courriels, Gmail read-only) and Espace de travail (workspace: Gmail, Calendar, Drive read-only, Google Sheets read and edit, no deletion). The user clicks “Authorize” once. After that, the agent gets their token with every question.
The flow, step by step
Google publishes no API to create an OAuth client: 2 minutes in the console, and the script prints the link. Everything else goes through scripts and Terraform: Google APIs, deployment, sheet, authorization, adding, customization.
Enable the agent's APIs, see its scopes
source scripts/config.sh config/12-1-agents-dans-projet-exp.env
./scripts/deployer_shell.sh oauth {{K}}== courriels
▶ gcloud services enable gmail.googleapis.com --project={{PROJET_AGENTS}}
portées (A2A) : https://www.googleapis.com/auth/gmail.readonly
portées (Agent Runtime) : … + https://www.googleapis.com/auth/cloud-platform
Client OAuth (une fois, console, 2 min) : https://console.cloud.google.com/auth/clients?project={{PROJET_AGENTS}}APIs and scopes come from the agent's "oauth" block in catalogue.json.
The OAuth client
- 🔎 Google Auth Platform → if needed Get started: app name, email, audience Internal.
- Clients → + Create client → Web application → name
ge-{{K}}. - Authorized redirect URIs, exactly these two:
https://vertexaisearch.cloud.google.com/oauth-redirect https://vertexaisearch.cloud.google.com/static/oauth/oauth.html
→ Create → note the client ID (put it in “Your values”) and the secret. Never put them in the repository.
Deploy the agent with its authorization
AGENTS_OAUTH="{{K}}={{AUTH}}" ./scripts/deployer_shell.sh cloudrun {{K}}
./scripts/deployer_shell.sh tester {{K}} # 3 to 5 min later: “cliquez sur Autoriser” = expectedThe agent gets AUTH_ID={{AUTH}}. In A2A, the authorization name in the app may differ; on Agent Runtime it must be identical.
The sheet, with the full authorization URL
AGENTS_OAUTH="{{K}}={{AUTH}}" CLIENT_ID={{CLIENT_ID}} ./scripts/deployer_shell.sh carte {{K}} 3. Authentication (ne PAS cliquer Skip) :
Nom de l'autorisation : {{AUTH}}
Authorization URL : {{AUTH_URL}}
Token URL : https://oauth2.googleapis.com/token
Scopes : {{SCOPES}}
PKCE / HTTP Basic : décochésThe app's permission on the service
Permission ① on {{K}}-a2a-shell, as for any A2A agent (tab 2).
Add the agent with the authorization
Console: Custom agent via A2A → the card → Authentication: fill in the fields from the sheet. Do not click Skip.
| Field | Value |
|---|---|
| Authorization name | {{AUTH}} (readable: users see it) |
| Client ID / secret | Those of the OAuth client |
| Authorization URL | The one from the sheet (with access_type=offline and prompt=consent, both required) |
| Token URL | https://oauth2.googleapis.com/token |
| Scopes | {{SCOPES}} |
| PKCE / HTTP Basic | Unchecked for Google |
Or by script, with the app values passed in front of the commands (the config file is not touched):
export PROJET_APP={{PROJET_APP}} NUM_APP={{NUM_APP}} GE_LOCATION={{GE_LOCATION}}
export GE_PROJECT={{PROJET_APP}} GE_APP={{APP_ID}}
./scripts/autorisation_oauth.sh create {{AUTH}} ~/Downloads/client_secret_XXX.json "{{SCOPES}}" "{{NOM}}"
AUTH_ID={{AUTH}} ./scripts/add_agent_to_ge.sh a2a carte-{{K}}.json "{{NOM}} (A2A)" # + run.invoker permission
./scripts/personnaliser_agent.sh "{{NOM}} (A2A)" {{K}} # suggested questions, languageThe test
Agent User for you → open the agent → “{{EX}}” → Authorize box → Google window → Allow → the answer, with your data. After that, no more box: access is kept.
What the “Espace de travail” agent can do, and will never do
| Service | Scope | Can | Cannot |
|---|---|---|---|
| Gmail | gmail.readonly | Search, read, summarize | Send, delete (Google refuses) |
| Calendar | calendar.events.readonly | See meetings | Create, edit, delete |
| Drive | drive.readonly | Search, read Docs and Slides | Edit, delete a file |
| Sheets | spreadsheets | Read, write, formulas, append rows, tabs, formatting, sort, charts, create a spreadsheet | Delete or clear nothing: blocked by the code, before any call to Google |
Google has no “Sheets without deletion” scope. So the tools only accept a list of lossless changes, and sheets_ecrire refuses to empty a filled cell. Tested: asked to “delete the Archives tab”, the agent refuses and offers to hide it.
Several credential providers
One authorization = one provider. A single Google authorization covers Gmail, Calendar, Drive, Sheets and BigQuery: scopes add up in a single consent. Another provider, such as Atlassian, Airtable or Databricks, needs another authorization.
One authorization per agent
The app sends one token (agentAuthorization). Several providers? One agent per provider: “Espace Google (A2A)” and “Jira (A2A)”. The chat switches between them.
A list of authorizations
The app sends one token per authorization (toolAuthorizations). Each tool takes its own. The user clicks “Authorize” once per provider.
AUTH_ID="acces-google,acces-atlassian": Google tools take the token of the first authorization, Atlassian tools the one whose ID contains “atlassian”. So: Google first, and the provider's word in the ID.
The Atlassian OAuth app
- developer.atlassian.com/console/myapps → Create → OAuth 2.0 integration.
- Permissions: Jira API (
read:jira-work,read:jira-user), Confluence API (search:confluence,read:confluence-content.summary). - Authorization → Callback URL:
https://vertexaisearch.cloud.google.com/static/oauth/oauth.html. - Settings: Client ID and Secret. Distribution → Sharing: otherwise only you can authorize.
Deploy on Agent Runtime with both authorizations
AGENTS_OAUTH="workspace=acces-google,acces-atlassian" ./scripts/deployer_shell.sh runtime workspace
AGENTS_OAUTH="workspace=acces-google,acces-atlassian" CLIENT_ID={{CLIENT_ID}} ./scripts/deployer_shell.sh carte workspaceIn the app: two authorizations
Custom agent via Agent Runtime → Authorizations → Add authorization twice. The names must be identical to AGENTS_OAUTH.
| acces-google | acces-atlassian | |
|---|---|---|
| Authorization URL | The one from the sheet (Google, + cloud-platform, required on Agent Runtime) | https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=…&scope=read%3Ajira-work%20read%3Ajira-user%20search%3Aconfluence%20read%3Aconfluence-content.summary%20offline_access&redirect_uri=…&response_type=code&prompt=consent |
| Token URL | https://oauth2.googleapis.com/token | https://auth.atlassian.com/oauth/token |
By script: AUTH_ID=acces-google,acces-atlassian ./scripts/add_agent_to_ge.sh adk <path> "Espace de travail". In A2A, the script refuses: “UNE seule autorisation par agent”.
In the chat
“My open Jira tickets?” → a second Authorize button (Atlassian) → Accept → the list. “Prepare my week with meetings, emails and tickets” → all three sources in one answer.
Other providers: the fields
| Provider | Authorization URL | Token URL | PKCE | HTTP Basic |
|---|---|---|---|---|
accounts.google.com/o/oauth2/auth?…&access_type=offline&prompt=consent | oauth2.googleapis.com/token | no | no | |
| Atlassian | auth.atlassian.com/authorize?audience=api.atlassian.com&… | auth.atlassian.com/oauth/token | no | no |
| Airtable | airtable.com/oauth2/v1/authorize | airtable.com/oauth2/v1/token | yes | yes |
| Databricks | <workspace>/oidc/v1/authorize | <workspace>/oidc/v1/token | yes | if secret |
Everywhere: register Gemini Enterprise's two redirect addresses with the provider. Atlassian, Airtable and Databricks have not been tested for real yet.
Polish the experience
The “The agent requires additional authorization for: …” box and its Authorize button are Google's interface: their look can't be changed. What is written around them, however, can be set.
| Element | How | |
|---|---|---|
| Button color, shape, position | no | Google's interface, the same for everyone |
| Language (“Authorize” or “Autoriser”) | indirect | Gemini Enterprise language setting; no hl=en_US in the URL |
| The name after “authorization for:” | yes | The authorization's display name (displayName), or a readable ID (not the one the console generates) |
| Agent name and icon | yes | displayName, icon |
| Suggested questions, input placeholder | yes | starterPrompts, customPlaceholderText (taken from the catalogue) |
| Google window: app name, logo, support email | yes | Google Auth Platform → Branding, in the OAuth client's project |
| Agent message when the token is missing or refused | yes | The agent's instruction (in French, with what to do) |
The Google window (no risk)
OAuth client's project → 🔎 Google Auth Platform → Branding: name (“Assistant Acme”), 120×120 PNG logo, team email. Then Data access → declare the scopes: they show in plain words.
The authorization name
export PROJET_APP={{PROJET_APP}} NUM_APP={{NUM_APP}} GE_LOCATION={{GE_LOCATION}}
./scripts/autorisation_oauth.sh list # ID | display name
./scripts/autorisation_oauth.sh renommer {{AUTH}} "{{NOM}}"If the box still shows the ID after renaming, recreate the authorization with a readable ID, then reattach the agent.
The agent: name, questions, placeholder, language, icon
GE_PROJECT={{PROJET_APP}} GE_APP={{APP_ID}} GE_LOCATION={{GE_LOCATION}} \
./scripts/personnaliser_agent.sh "<current name in the app>" {{K}} --icone icone.pngAVANT : {"displayName": "test gmail remote agent with auth"}
APRÈS : {"displayName": "{{NOM}} (A2A)", "starterPrompts": [{"text": "{{EX}}"}, …], "languageCode": "fr"}
Modifier cet agent dans l'app {{APP_ID}} (projet {{PROJET_APP}}) ? Tapez « oui » : oui
modifié : … (retour arrière : ./scripts/personnaliser_agent.sh --annuler avant-<ID>.json)Only these fields change (updateMask): neither the card nor the authorization.
What protects, at every level
Private services
Cloud Run without public access: only accounts with run.invoker can call the agent, i.e. the app and you.
Least privilege
Each agent has its own account with 3 roles: Gemini, logs, Model Armor. The app account only has ① and ②.
Model Armor
Questions and answers are filtered: prompt injection, jailbreak, malicious URLs, sensitive data, dangerous content.
The user's rights
With OAuth, the agent only sees what the user sees. The token is never stored (temp: state) nor written to logs.
Minimal scopes
Read-only everywhere except Sheets. No deletion tool. Internal consent: Acme accounts only.
Injection through content
An email may say “ignore your rules”. The agent treats that content as data. Even if fooled, it can't delete or send anything: scopes and code prevent it.
The OAuth client: which project?
| Agents project (recommended) | App project | |
|---|---|---|
| Gmail, Drive… APIs to enable | agents project (script, Terraform) | the app's production project |
| Quotas, monitoring | agents project | app project |
| Google window | the team's name and logo | one name for everyone |
| Secret and rotation | agents team | Gemini Enterprise team |
| Production change for an agent | none, except adding the agent | APIs + client in production |
Both work. Agents project: one client per team or per agent family. App project: only for a shared client, run by the Gemini Enterprise team. Either way, the Google Workspace admin may need to approve the client ID (Security → API controls).
Read first (connecter_app.sh verifier, autorisation_oauth.sh list, nettoyer.py without --supprimer). Any change to the app asks for confirmation (type the project ID, or “oui”). personnaliser_agent.sh keeps the previous values for rollback.
Troubleshooting
| What you see | Cause | What to do |
|---|---|---|
| The card is rejected when adding | Card from /.well-known/agent-card.json, or truncated line | ./scripts/deployer_shell.sh carte {{K}}, then copy the whole line |
KeyError: '{{K}}' | catalogue.json not up to date on the machine | Copy the whole file again |
| “The agent couldn't respond” | Permissions not active yet, or missing | Wait 3 min; check ① and ② for {{GE_SA}} |
part_metadata parameter is only supported in Gemini Developer API mode | Old commun.py | Update commun.py, then deployer_shell.sh cloudrun {{K}} |
| Agent missing from the chat | No Agent User role, or no license | The agent → User permissions → Agent User |
redirect_uri_mismatch | OAuth client redirect URIs | The 2 URIs, character for character |
| “Access blocked: your administrator…” | The Workspace admin blocks the app | Get the client ID approved (API controls) |
refus (403) … has not been used in project | Google API not enabled | ./scripts/deployer_shell.sh oauth {{K}} (OAuth client's project) |
refus (403) … insufficient authentication scopes | Scope missing from the Authorization URL | Use the sheet's URL; revoke access, then authorize again |
refus (401) | Expired token | Ask again; otherwise revoke access at myaccount.google.com/permissions |
| No Authorize button | Agent added without authorization (Skip) | Remove the agent from the app, add it again with the Authentication step |
| Agent Runtime: always “click Authorize” | Authorization name ≠ AGENTS_OAUTH | Same ID on both sides |
A2A : UNE seule autorisation par agent | Two providers in A2A | Agent Runtime, or one agent per provider |
| “Pas de jeton Atlassian” | ID without the word “atlassian”, or A2A | Tab 4: naming rule |
| Answer blocked by Model Armor | Agent account permissions not active yet, or sensitive data | Wait 2 to 5 min; narrow the request |
does not have storage.objects.get access | Build account missing | ./scripts/deployer_shell.sh preparer |
Service account … does not exist | Brand-new account, not visible yet | The script retries; otherwise run again |
pip install fails (watchdog) on Mac | No prebuilt package for Python 3.14 | pip install --no-deps -r agents/trio/requirements-poste.txt; adk run instead of adk web |
Call the agent the way the app does
curl -s -X POST "{{URL}}/" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" -H "A2A-Version: 0.3" \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","role":"user","messageId":"t1","parts":[{"kind":"text","text":"{{EX}}","metadata":{"source":"curl"}}]}}}' \
| python3 -m json.tool“completed” and an answer: the agent works. To simulate the app end to end, with its service account: ./scripts/simuler_gemini_enterprise.sh tout (guide 12-1-5).
From zero to an OAuth agent in the chat
Each box stays ticked in your browser. The order is the one that worked on the Acme instance.
Going further: the guides (in French)
docs/12-1-1-deployer-agents-shell.md: deploy by script, step by stepdocs/12-1-2-deployer-agents-terraform.md·12-1-3(console)docs/12-1-4-connecter-agents-app-gemini-enterprise.md: permissions, messages to teamsdocs/12-1-5-simuler-appel-gemini-enterprise.md: simulate the app, without the appdocs/12-1-6-ajouter-agent-console-gemini-enterprise.md: the app console, click by clickdocs/12-1-7-agents-oauth-identite-outils.md: OAuth, identitydocs/12-1-8-agent-espace-de-travail-multi-oauth.md: Gmail, Calendar, Drive, Sheets; several providers; experiencescripts/README.md: the map of all scripts