Skip to content

Multimodal Cloud Run sandbox

Status: Current | Last reviewed: 2026-10-08

Deploy the standalone multimodal service to Google Cloud Run and connect a GECX agent to its MCP endpoint. Use this runbook for the sandbox companion-form demo; use persistent Postgres and Redis before deploying production workloads.

Resources

Setting Value
Project us-con-gcp-sbx-0000500-032725
Project number 288250959474
Region us-central1
Cloud Run service trueserve-multimodal-demo
Private service origin https://trueserve-multimodal-demo-288250959474.us-central1.run.app
Artifact Registry repository multimodal-service
Runtime identity 288250959474-compute@developer.gserviceaccount.com
Catalogues standard, insurance-auto, acg-auto
OAuth client ID gecx-multimodal-demo
Verified build 27e2cd18-515b-4649-be17-2263fb7a281a
Verified revision trueserve-multimodal-demo-00004-ggw

The app, Postgres 16 and Redis 7 run in one Cloud Run instance. The service keeps one warm instance, caps instances at one, and accepts requests for up to one hour. Each container has one vCPU; memory allocations are 768 MiB, 512 MiB and 256 MiB respectively. Only the app's port 8080 is exposed. Postgres uses a 256 MiB memory volume; Redis is limited to 128 MiB with noeviction.

Warning: Instance replacement and deployments reset all experiences and submissions. Catalogues are embedded in the image and survive replacement. Do not split traffic across revisions or increase replicas while using independent sidecar stores.

Cloud Run requires Google IAM authentication on every route. The project already grants run.routes.invoke to the CES service agent through roles/ces.serviceAgent. GECX uses Service Agent ID Token authentication. The app independently verifies the Google signature, issuer, audience, expiry and allowlisted service-account email before allowing MCP calls. Operator session creation and browser APIs retain their separate bearer/session tokens. Unauthenticated requests to the service URL return HTTP 403.

The runtime identity already has access to the application secrets. No IAM policy changes are required. On another project, grant the runtime access to its application secrets and grant the CES service agent invocation permission on the private Cloud Run service.

The operator account cannot change the Cloud Run invoker policy. Keep this service private; use the authenticated browser preview below. A public customer deployment needs an approved ingress or customer backend that authenticates to Cloud Run.

Secrets

The trueserve-multimodal-demo- prefix has these Secret Manager entries:

Suffix Content
api-token Operator bearer token for creating experiences
mcp-token Agent bearer token
session-secret Session JWT signing secret
db-password Postgres sidecar password
database-url Postgres connection string using that password and localhost
oauth-secret Confidential OAuth client secret
oauth-signing-secret OAuth signing secret

Versions are pinned to 1 in the manifest. Rotate the corresponding version references together. Generate independent values of at least 32 bytes. Create secrets with gcloud secrets create --data-file=- and pass values on private stdin; never put credentials in command arguments, Git, build substitutions or build logs.

The initial deployment also saved generated credentials in the devcontainer at ~/.local/state/trueserve/multimodal-cloud-run/credentials.json, mode 0600. The operator account can create secrets but cannot read secret versions; the launcher uses this existing local file. It does not impersonate another account. Keep this file private. On a different workstation, use an authorized operator credential file or request access to the specific operator secret.

Build and deploy

Run these commands from the repository root. The upload exclusions keep virtual environments, local environment files and test/example data out of Cloud Build. The image's Dockerfile exclusions also exclude deployment material from the application image.

gcloud builds submit apps/multimodal-service \
  --project=us-con-gcp-sbx-0000500-032725 \
  --region=us-central1 \
  --config=apps/multimodal-service/deploy/cloudbuild.yaml \
  --substitutions=_IMAGE_PREFIX=us-central1-docker.pkg.dev/us-con-gcp-sbx-0000500-032725/multimodal-service,_RELEASE=gecx-20261008-v5 \
  --async

Use a new release tag for each build. The build mirrors the Postgres and Redis images into the same repository. Wait for SUCCESS, then use its build ID:

python apps/multimodal-service/deploy/render_cloud_run.py \
  --project=us-con-gcp-sbx-0000500-032725 \
  --region=us-central1 \
  --build-id=BUILD_ID \
  --output=/tmp/multimodal-cloud-run.yaml

gcloud run services replace /tmp/multimodal-cloud-run.yaml \
  --project=us-con-gcp-sbx-0000500-032725 \
  --region=us-central1

The renderer requires a successful build and pins all three images by digest. It uses the deterministic Cloud Run origin for MULTIMODAL_PUBLIC_URL, which is also the MCP Host allowlist and OAuth issuer. Use that origin consistently instead of the alternative hash-based Cloud Run hostname.

The renderer defaults to private access. Its optional --public flag requires run.services.setIamPolicy and is not used for this deployment. MULTIMODAL_GOOGLE_SERVICE_ACCOUNTS allowlists only the project's CES service agent. Google ID token audiences are limited to the configured service origin, its /mcp endpoint, and the canonical Cloud Run URL aliases explicitly supplied in MULTIMODAL_GOOGLE_ID_TOKEN_AUDIENCES. No wildcard audiences are accepted. For a new service, rerun the renderer after its first deployment and replace the service again so it can include Google's newly assigned canonical URL.

One warm instance avoids timing out GECX discovery while the sidecar stores initialize. Idle capacity is billable. To allow the sandbox to scale to zero between demos, run:

gcloud run services update trueserve-multimodal-demo \
  --project=us-con-gcp-sbx-0000500-032725 --region=us-central1 --min=0

Scaling to zero clears experiences; create new ones when resuming the demo.

The app starts only after the Postgres and Redis startup probes succeed. MULTIMODAL_BOOTSTRAP_EDGE_SCHEMA=true creates the isolated session table. Never use this sidecar manifest for the platform deployment.

Authenticated browser preview

Run the Google Cloud CLI's official proxy in the devcontainer:

gcloud run services proxy trueserve-multimodal-demo \
  --project=us-con-gcp-sbx-0000500-032725 \
  --region=us-central1 --port=18085

The proxy binds locally and adds X-Serverless-Authorization using your active Google identity. It preserves the app's Authorization header and streams SSE. Forward devcontainer port 18085 privately to your laptop in VS Code. Keep the proxy terminal open. Do not make the forwarded port public.

Create a test experience

python apps/multimodal-service/examples/create_gecx_session.py \
  --google-iam --app-origin=http://127.0.0.1:18085 \
  --output=/tmp/gecx-experience.json

The launcher prints the non-secret experience ID and saves the private browser link, session token and GECX variable mapping in a mode-0600 file. Open its app_url in your browser and put gecx_variables.experienceSessionId into the GECX test session. Create one experience per customer conversation; do not put a shared session ID into production agent defaults.

All three catalogues are loaded. The bundled /demo/ page renders only the ACG driver/vehicle forms and the legacy AutoChangeForm. Other catalogue components require a client renderer implementing their contract. The launcher excludes connect_auto_experience: this deployment has no AWS SMS configuration. Browser links are distributed by the operator or customer application.

GECX configuration

Follow the GECX operator guide. The toolset JSON contains the exact API configuration, including Google service-agent ID token authentication and the $context.variables.experienceSessionId header binding. No MCP credential is copied into the GECX agent. The browser proxy is for the operator; GECX calls the private Cloud Run endpoint directly.

The toolset requests synchronous execution with a 60-second timeout. await_auto_change waits for at most 20 seconds; pending requires a later check. A form POST returns through an outstanding call or a subsequent read, not an unsolicited invocation into an idle GECX conversation. Synchronous execution was verified with a native GECX agent. Asynchronous execution returned pending placeholders and did not complete reliably in the validation session. Validate it and voice interruption separately before changing the pilot configuration.

Validation

Check IAM-authenticated health, unauthenticated ingress rejection, authenticated MCP initialization/discovery and a complete create-render-submit-read sequence. Health alone does not prove the database, catalogue or GECX connection works.

On 2026-10-08, revision trueserve-multimodal-demo-00004-ggw passed:

  • 44 unit tests, including signed Google identity-token acceptance and rejection.
  • Two live HTTP/MCP tests covering both insurance catalogues, submission waits, isolation, retries, replay and revocation.
  • Native GECX discovery of all 12 tools and an agent catalogue-list response.
  • A native GECX synchronous agent turn with a real session-variable header: get_session_info → start_auto_change → SSE form delivery → synthetic HTTP submission → await_auto_change → the agent's correct demo receipt.
  • Anonymous Cloud Run rejection and authenticated browser-proxy access.

The temporary GECX validation application was removed after testing. No existing GECX agents were changed. Voice/PSTN/SIP and manual browser interaction remain outside this validation.

The existing live suite accepts a private MULTIMODAL_TEST_CONFIG JSON file with base_url, api_token, oauth_secret, and oauth_client_id: "gecx-multimodal-demo" and google_iam: true. It obtains a Google identity token from the active CLI account and tests both insurance catalogue variants, OAuth, cross-session denial, retries, replay, late reads and revocation:

cd apps/multimodal-service
MULTIMODAL_TEST_CONFIG=/private/path/cloud-run-test.json \
  .venv/bin/python -m pytest tests/test_live_edge.py -q

Do not put the private config file into the source directory. The test uses synthetic data and sends no text messages.

Sources

See also