Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deploy/cloudrun/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ roles=(
"roles/cloudsql.client"
"roles/serviceusage.serviceUsageConsumer"
)
# NOTE: The Cloud Commerce Partner Procurement API (cloudcommerceprocurement.googleapis.com)
# does not use standard IAM roles. To enable multi-agent product filtering (fetching
# entitlement product info from the API), register this SA in the GCP Marketplace
# Producer Portal → Technical Integration → Partner Procurement API.

for role in "${roles[@]}"; do
log_info " Granting $role..."
Expand Down
10 changes: 6 additions & 4 deletions deploy/gitops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,14 @@ Many `setup.sh` env vars must match the corresponding `values-override.yaml` fie

#### 2. Register Service Accounts in the Google Cloud Marketplace Producer Portal

Each instance's service accounts must be registered in the [Producer Portal](https://console.cloud.google.com/producer-portal) under the **Technical Integration** section of your product:
**All** instance service accounts must be registered in the [Producer Portal](https://console.cloud.google.com/producer-portal) under the **Technical Integration** section:

- **Partner Procurement API integration** — add the runtime SA (e.g., `sa-lightspeed-agent-staging@<project>.iam.gserviceaccount.com`). This authorizes the SA to call the Procurement API for entitlement management.
- **Cloud Pub/Sub integration** — add the Pub/Sub invoker SA (e.g., `pubsub-invoker-staging@<project>.iam.gserviceaccount.com`). This authorizes the SA to create push subscriptions on the Marketplace entitlements topic.
- **Partner Procurement API integration** — add the runtime SA for **every** instance (e.g., both `sa-lightspeed-agent@<project>.iam.gserviceaccount.com` and `sa-lightspeed-agent-staging@<project>.iam.gserviceaccount.com`). This authorizes the SA to call the Procurement API for entitlement management and multi-agent product filtering.
- **Cloud Pub/Sub integration** — add the Pub/Sub invoker SA for **every** instance (e.g., both `pubsub-invoker@<project>.iam.gserviceaccount.com` and `pubsub-invoker-staging@<project>.iam.gserviceaccount.com`). This authorizes the SA to create push subscriptions on the Marketplace entitlements topic.

Without this step, the deployment will fail at the `configure-pubsub` Cloud Build step with `"User not authorized to perform this action"`.
> **Important:** The Technical Integration configuration is **provider-level** (per GCP project), not per-product. Updating the SA list on one product automatically updates all products under the same provider. This means you register all SAs in a single place, and all instances gain access to the Procurement API regardless of which product listing you edit. This is also why access to the Procurement API cannot be granted via standard IAM roles — it is managed exclusively through the Producer Portal.

Without this step, the marketplace handler will get `403 Forbidden` when calling the Procurement API to resolve entitlement products, causing multi-agent product filtering to silently drop events. The deployment will also fail at the `configure-pubsub` Cloud Build step with `"User not authorized to perform this action"`.

#### 3. Create Per-Instance GCP Deploy Service Accounts

Expand Down
19 changes: 15 additions & 4 deletions docs/marketplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,20 @@ Marketplace sends procurement events via Pub/Sub:
### Multi-Agent Product Filtering

In multi-agent deployments where multiple agents share the same Google Cloud
project and Pub/Sub topic, events are filtered by the `product` field in the
entitlement data. Each agent only processes events matching its
`SERVICE_CONTROL_SERVICE_NAME`. Account-only events (no product field) pass
through without filtering and are handled normally (e.g. account approval).
project and Pub/Sub topic, events are filtered by product. Each agent only
processes events matching its `SERVICE_CONTROL_SERVICE_NAME`.

Since most Pub/Sub event types omit the `product` field, the handler fetches
it from the Procurement API using the entitlement ID. This requires the
Cloud Run runtime SA to be registered in the **Producer Portal → Technical
Integration → Partner Procurement API**. The Technical Integration is
provider-level (per GCP project, not per product), so all instance SAs must
be registered together — see [GitOps README: Step 2](../deploy/gitops/README.md#2-register-service-accounts-in-the-google-cloud-marketplace-producer-portal).

If the SA is not registered, the Procurement API returns `403 Forbidden`
and the handler cannot determine the product, causing events to be silently
dropped. Account-only events (no entitlement data) pass through without
filtering and are handled normally (e.g., account approval).

### Handling Entitlements

Expand Down Expand Up @@ -386,6 +396,7 @@ gcloud logging read \
| Missing events | Subscription not configured | Verify Pub/Sub subscription |
| Event processing failed | Handler error | Check logs for exceptions |
| Entitlement not found | Sync delay | Wait and retry |
| 403 on Procurement API / events silently dropped | Runtime SA not registered in Producer Portal | Register the Cloud Run SA in Producer Portal → Technical Integration → Partner Procurement API (this is provider-level, not per-product — register all instance SAs together) |

### Usage Reporting Issues

Expand Down
10 changes: 6 additions & 4 deletions src/lightspeed_agent/marketplace/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,9 @@ async def _handle_pubsub_event(body: dict[str, Any]) -> JSONResponse:
product = await _fetch_entitlement_product(entitlement_id, settings)

if product == _NOT_AUTHORIZED:
logger.info(
"Entitlement %s belongs to a different product (SA not authorized)",
logger.warning(
"Entitlement %s skipped: SA returned 403 from Procurement API "
"(different product, or SA not registered in Producer Portal)",
entitlement_data.get("id", "unknown") if entitlement_data else "unknown",
)
return JSONResponse(content={"status": "ok", "message": "not for this product"})
Expand Down Expand Up @@ -379,8 +380,9 @@ async def _fetch_entitlement_product(entitlement_id: str, settings: Settings) ->
)
return product
elif response.status_code == 403:
logger.info(
"Not authorized to access entitlement %s — belongs to a different product",
logger.warning(
"403 fetching entitlement %s — either belongs to a different product "
"or this SA is not registered in the Producer Portal Technical Integration",
entitlement_id,
)
return _NOT_AUTHORIZED
Expand Down
Loading