Skip to content

Commit ef015f0

Browse files
authored
feat: add agent identity support for Agent Engine deployments (GoogleCloudPlatform#720)
* feat: add agent identity support for Agent Engine deployments - Add --agent-identity flag to deploy.py for opt-in agent identity - Auto-grant required IAM roles to agent identity principal - Use v1beta1 API when agent identity is enabled - Add AGENT_IDENTITY=true parameter to Makefile deploy target * chore: update dependency lock files * refactor: address code review feedback for agent identity - Add docstring explaining etag-based optimistic concurrency control - Move IAM roles list inside grant_agent_identity_roles function - Fix import ordering and line length issues for linting * fix: split long import line for adk_live template Use conditional multi-line import for vertexai types when AgentServerMode is included in adk_live templates.
1 parent ad464fb commit ef015f0

File tree

12 files changed

+6114
-6669
lines changed

12 files changed

+6114
-6669
lines changed

agent_starter_pack/base_templates/python/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ build-inspector-if-needed:
263263
# Deploy the agent remotely
264264
{%- if cookiecutter.deployment_target == 'cloud_run' %}
265265
# Usage: make deploy [IAP=true] [PORT=8080] - Set IAP=true to enable Identity-Aware Proxy, PORT to specify container port
266+
{%- elif cookiecutter.deployment_target == 'agent_engine' %}
267+
# Usage: make deploy [AGENT_IDENTITY=true] - Set AGENT_IDENTITY=true to enable per-agent IAM identity (Preview)
266268
{%- endif %}
267269
deploy:
268270
{%- if cookiecutter.deployment_target == 'cloud_run' %}
@@ -291,7 +293,8 @@ deploy:
291293
--source-packages=./{{cookiecutter.agent_directory}} \
292294
--entrypoint-module={{cookiecutter.agent_directory}}.agent_engine_app \
293295
--entrypoint-object=agent_engine \
294-
--requirements-file={{cookiecutter.agent_directory}}/app_utils/.requirements.txt
296+
--requirements-file={{cookiecutter.agent_directory}}/app_utils/.requirements.txt \
297+
$(if $(AGENT_IDENTITY),--agent-identity)
295298
{%- endif %}
296299

297300
# Alias for 'make deploy' for backward compatibility

agent_starter_pack/deployment_targets/agent_engine/python/{{cookiecutter.agent_directory}}/app_utils/deploy.py

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@
2424
import click
2525
import google.auth
2626
import vertexai
27+
from google.cloud import resourcemanager_v3
28+
from google.iam.v1 import iam_policy_pb2, policy_pb2
2729
from vertexai._genai import _agent_engines_utils
28-
from vertexai._genai.types import AgentEngine, AgentEngineConfig{%- if cookiecutter.is_adk_live %}, AgentServerMode{%- endif %}
30+
{%- if cookiecutter.is_adk_live %}
31+
from vertexai._genai.types import (
32+
AgentEngine,
33+
AgentEngineConfig,
34+
AgentServerMode,
35+
IdentityType,
36+
)
37+
{%- else %}
38+
from vertexai._genai.types import AgentEngine, AgentEngineConfig, IdentityType
39+
{%- endif %}
2940
{%- if cookiecutter.is_adk_live %}
3041

3142
from {{cookiecutter.agent_directory}}.app_utils.gcs import create_bucket_if_not_exists
@@ -129,6 +140,36 @@ def print_deployment_success(
129140
{%- endif %}
130141

131142

143+
def grant_agent_identity_roles(agent: Any, project: str) -> None:
144+
"""Grant required IAM roles to the agent identity principal.
145+
146+
Uses get-modify-set pattern with etag for optimistic concurrency control.
147+
The policy object returned by get_iam_policy includes an etag that is
148+
automatically validated by set_iam_policy to prevent race conditions.
149+
"""
150+
roles = [
151+
"roles/aiplatform.expressUser",
152+
"roles/serviceusage.serviceUsageConsumer",
153+
"roles/browser",
154+
]
155+
principal = f"principal://{agent.api_resource.spec.effective_identity}"
156+
click.echo(f"\n🔐 Granting IAM roles to agent identity: {principal}")
157+
proj_client = resourcemanager_v3.ProjectsClient()
158+
# Policy includes etag for optimistic locking - set_iam_policy will fail
159+
# if policy was modified between get and set operations
160+
policy = proj_client.get_iam_policy(
161+
request=iam_policy_pb2.GetIamPolicyRequest(resource=f"projects/{project}")
162+
)
163+
for role in roles:
164+
policy.bindings.append(policy_pb2.Binding(role=role, members=[principal]))
165+
proj_client.set_iam_policy(
166+
request=iam_policy_pb2.SetIamPolicyRequest(
167+
resource=f"projects/{project}", policy=policy
168+
)
169+
)
170+
click.echo(" ✅ Granted IAM roles")
171+
172+
132173
@click.command()
133174
@click.option(
134175
"--project",
@@ -220,6 +261,12 @@ def print_deployment_success(
220261
default=1,
221262
help="Number of worker processes (default: 1)",
222263
)
264+
@click.option(
265+
"--agent-identity",
266+
is_flag=True,
267+
default=False,
268+
help="Enable agent identity for per-agent IAM access control (Preview feature)",
269+
)
223270
def deploy_agent_engine_app(
224271
project: str | None,
225272
location: str,
@@ -238,6 +285,7 @@ def deploy_agent_engine_app(
238285
memory: str,
239286
container_concurrency: int,
240287
num_workers: int,
288+
agent_identity: bool,
241289
) -> AgentEngine:
242290
"""Deploy the agent engine app to Vertex AI."""
243291

@@ -279,6 +327,8 @@ def deploy_agent_engine_app(
279327
click.echo(f" Container Concurrency: {container_concurrency}")
280328
if service_account:
281329
click.echo(f" Service Account: {service_account}")
330+
if agent_identity:
331+
click.echo(" Agent Identity: Enabled (Preview)")
282332
if env_vars:
283333
click.echo("\n🌍 Environment Variables:")
284334
for key, value in sorted(env_vars.items()):
@@ -287,9 +337,12 @@ def deploy_agent_engine_app(
287337
source_packages_list = list(source_packages)
288338

289339
# Initialize vertexai client
340+
# Use v1beta1 API when agent identity is enabled (required for identity_type)
341+
http_options = {"api_version": "v1beta1"} if agent_identity else None
290342
client = vertexai.Client(
291343
project=project,
292344
location=location,
345+
http_options=http_options,
293346
)
294347
vertexai.init(project=project, location=location)
295348

@@ -333,6 +386,7 @@ def deploy_agent_engine_app(
333386
gcs_dir_name=display_name,
334387
agent_server_mode=AgentServerMode.EXPERIMENTAL, # Enable bidi streaming
335388
resource_limits={"cpu": cpu, "memory": memory},
389+
identity_type=IdentityType.AGENT_IDENTITY if agent_identity else None,
336390
)
337391

338392
agent_config = {
@@ -361,6 +415,7 @@ def deploy_agent_engine_app(
361415
{%- if cookiecutter.is_adk and not cookiecutter.is_a2a and not cookiecutter.is_adk_live %}
362416
agent_framework="google-adk",
363417
{%- endif %}
418+
identity_type=IdentityType.AGENT_IDENTITY if agent_identity else None,
364419
)
365420
{%- endif %}
366421

@@ -372,32 +427,44 @@ def deploy_agent_engine_app(
372427
if agent.api_resource.display_name == display_name
373428
]
374429

430+
# Handle agent identity: create empty agent if needed, then grant roles
431+
if agent_identity:
432+
if not matching_agents:
433+
click.echo(f"\n🔧 Creating agent identity for: {display_name}")
434+
empty_agent = client.agent_engines.create(
435+
config={"identity_type": IdentityType.AGENT_IDENTITY}
436+
)
437+
matching_agents = [empty_agent]
438+
grant_agent_identity_roles(matching_agents[0], project)
439+
375440
# Deploy the agent (create or update)
376441
if matching_agents:
377-
click.echo(f"\n📝 Updating existing agent: {display_name}")
378-
else:
379-
click.echo(f"\n🚀 Creating new agent: {display_name}")
380-
381-
click.echo("🚀 Deploying to Vertex AI Agent Engine (this can take 3-5 minutes)...")
382-
442+
click.echo(f"\n📝 Updating agent: {display_name}")
443+
click.echo(
444+
"🚀 Deploying to Vertex AI Agent Engine (this can take 3-5 minutes)..."
445+
)
383446
{%- if cookiecutter.is_adk_live %}
384-
if matching_agents:
385447
remote_agent = client.agent_engines.update(
386448
name=matching_agents[0].api_resource.name,
387449
agent=agent_instance,
388450
config=config,
389451
)
452+
{%- else %}
453+
remote_agent = client.agent_engines.update(
454+
name=matching_agents[0].api_resource.name, config=config
455+
)
456+
{%- endif %}
390457
else:
458+
click.echo(f"\n🚀 Creating new agent: {display_name}")
459+
click.echo(
460+
"🚀 Deploying to Vertex AI Agent Engine (this can take 3-5 minutes)..."
461+
)
462+
{%- if cookiecutter.is_adk_live %}
391463
remote_agent = client.agent_engines.create(
392464
agent=agent_instance,
393465
config=config,
394466
)
395467
{%- else %}
396-
if matching_agents:
397-
remote_agent = client.agent_engines.update(
398-
name=matching_agents[0].api_resource.name, config=config
399-
)
400-
else:
401468
remote_agent = client.agent_engines.create(config=config)
402469
{%- endif %}
403470

0 commit comments

Comments
 (0)