Google ADK 2.0 refund workflow (app/agent/)¶
This directory contains the Google Agent Development Kit 2.0 (ADK 2.0) (https://adk.dev/2.0/) workflow runtime, Cloud Spanner Graph ISO Graph Query Language (GQL) client, Knowledge Catalog semantic contract resolver, and BigQuery Action Lineage callback writer.
Directory layout¶
The app/agent/ package contains the following modules:
app/agent/
├── README.md # ADK 2.0 workflow and node specification (this file)
├── __init__.py # Package initializer
├── workflow.py # ADK 2.0 Workflow graph and BigQuery lineage callbacks
├── spanner_client.py # Cloud Spanner Graph ISO GQL queries and mutations
├── bq_lineage.py # BigQuery ActionLineageGraph buffered writer and GQL runner
└── catalog_client.py # Knowledge Catalog glossary and aspect lookup client
ADK 2.0 hybrid workflow design (workflow.py)¶
workflow.py compiles a google.adk.Workflow (AdkWorkflow) that uses
google.adk.Agent (AdkAgent) nodes for natural language intent extraction and
response synthesis, and wraps catalog lookups, Spanner GQL evaluations, and
transaction commits in google.adk.workflow.FunctionNode step nodes.
Before starting the workflow, the FastAPI handler (POST /api/chat/refund or
POST /api/chat/refund/stream) binds ctx.state["user_id"],
ctx.state["trace_id"], an optional ctx.state["_step_observer"] callback, and
an empty ctx.state["lineage_buffer"] outside the LLM prompt, and
Workflow.run(ctx) executes refund_system_of_action.adk_workflow via
google.adk.runners.InMemoryRunner. The workflow executes five sequential
nodes:
step_order |
Canonical node_name |
Node type | Input ctx.state keys |
Output ctx.state keys |
Execution and fallback behavior |
|---|---|---|---|---|---|
1 |
IntentAgent |
google.adk.Agent (before_agent_callback, gemini-3-flash-preview) + hybrid order resolver |
user_prompt, user_id, trace_id, confirmed, pending_order_id |
order_id (int \| None), resolution_mode (str), unmatched_product (str \| None), candidate_orders (list[dict]), inferred_order (dict \| None), refund_reason (str) |
Runs google.adk.Agent (intent_agent.adk_agent) via before_agent_callback against the GOOGLE_CLOUD_LOCATION = "global" Vertex AI endpoint (_generate_gemini_text_fast with ThinkingLevel.MINIMAL, max_output_tokens = 250, timeout = 4000 ms, and attempts = 1). When confirmed=True or an explicit 5-digit ORD-<id> is present in the prompt (such as 1-click prompt chips), Step 1 resolves order_id directly and skips the Step 1 LLM call to conserve Qwiklabs RPM quota for Step 5. On natural-language prompts without an explicit ORD-<id>, it invokes _try_gemini_intent_extraction + _apply_gemini_intent_decision first with full multi-turn conversation context (candidate_orders, list_orders_pool, focused_order, pending_order, awaiting_order) to classify structured intents (REFUND_REQUEST, ORDER_INFO, ORDER_LIST, POLICY_INFO, CONFIRM_REFUND, DECLINE_CONFIRMATION, CANCEL_REFUND), resolve order_id, list_limit, and status_filter, and detect unpurchased storefront items (unmatched_product matched against STOREFRONT_CATALOG_PRODUCTS, evaluated before falling back to focused_order). Falls back to deterministic regex/keyword inference (_infer_order_from_candidates, _detect_unmatched_product, _filter_orders_by_prompt_status, SINGLE_ORDER_SELECTOR_CUES, NEGATED_REFUND_REGEX) when Vertex AI is disabled or raises google.genai.errors.APIError. |
2 |
resolve_catalog_contract |
google.adk.workflow.FunctionNode |
trace_id |
catalog_contract (dict) |
Calls fetch_knowledge_catalog_aspect() to resolve the governed contract for rolling-100d-refund-count, order-age-days, and customer-loyalty-tier from Knowledge Catalog (or the 5-minute in-memory cache / local JSON fallback). |
3 |
evaluate_spanner_policy |
google.adk.workflow.FunctionNode |
user_id, order_id, resolution_mode, catalog_contract |
eval_id (str), policy_eval (dict) |
If order_id is None and resolution_mode is "list_orders", "policy_info", or "cancelled", sets evaluation_result = "INFO" and failure_code = None. If order_id is None on an unconfirmed refund request ("clarify"), sets failure_code = "MISSING_ORDER_ID" and evaluation_result = "FAIL". Otherwise calls query_spanner_operational_context_graph() (which matches cond.applicable_tier = u.customer_tier in GQL) and classifies eligibility against the six deterministic failure codes below (or evaluation_result = "INFO" when resolution_mode == "order_info"). |
4 |
route_and_commit_action |
google.adk.workflow.FunctionNode |
user_id, order_id, trace_id, eval_id, resolution_mode, unmatched_product, require_confirmation, confirmed, catalog_contract, policy_eval |
action_outcome (dict) |
Routes read-only informational modes to ORDER_LIST (LIST_CUSTOMER_ORDERS), ORDER_INFO (SHOW_ORDER_DETAILS), or POLICY_INFO (SHOW_POLICY_AND_ORDERS). When require_confirmation=True, enforces a universal two-turn customer confirmation gate: Turn 1 pauses at decision_status = "PENDING_CONFIRMATION" (action_type = "AWAIT_CUSTOMER_CONFIRMATION") whenever a valid order is resolved (order_found == True, whether PASS or FAIL) or when order_id is None ("clarify", enriched with unmatched_product when the customer asked for an item they never purchased, or "cancelled"). On Turn 2 (confirmed=True), executes commit_spanner_order_refund() inside a Cloud Spanner read-write transaction if evaluation_result == "PASS", or returns decision_status = "DENIED" ("BLOCK_SPANNER_MUTATION") if evaluation_result == "FAIL". |
5 |
ResponseAgent |
google.adk.Agent (before_agent_callback, gemini-3-flash-preview) + template fallback & card sync |
user_prompt, policy_eval, action_outcome, unmatched_product, candidate_orders, trace_id |
final_response (str), candidate_orders (list[dict]) |
Runs google.adk.Agent (response_agent.adk_agent) via before_agent_callback. Invokes _try_gemini_response_generation (_generate_gemini_text_fast with ThinkingLevel.MINIMAL, max_output_tokens = 300, timeout = 4000 ms, and attempts = 1) across all decision_status outcomes (PENDING_CONFIRMATION, APPROVED, DENIED, ORDER_INFO, ORDER_LIST, POLICY_INFO) to synthesize grounded replies (including unpurchased product explanations when unmatched_product is set) with a deterministic bilingual template fallback (_format_bilingual_fallback_response), and runs _sync_candidate_orders_with_response() on ORDER_LIST and POLICY_INFO turns so ctx.state["candidate_orders"] and _LAST_SHOWN_ORDERS match the ORD-<id> order IDs cited in final_response. |
Fast Vertex AI Gemini execution and Qwiklabs quota resilience (_get_genai_client and _generate_gemini_text_fast)¶
workflow.py configures the Vertex AI Gemini client for low latency and bounded
failure recovery in shared Qwiklabs (gcpfree) projects:
- Single-attempt 4-second HTTP client (
_get_genai_client): Passestypes.HttpOptions(timeout=int(os.getenv("VERTEX_LLM_TIMEOUT_MS", "4000")), retry_options=types.HttpRetryOptions(attempts=1))togenai.Client. By default,google-genaisetstimeout=Noneand retries429/503/504responses up to 5 times with 60-second exponential backoff, which can stall a Cloud Run request until the 300-second container timeout under Qwiklabs Dynamic Shared Quota contention. Setting a 4-second timeout withattempts=1bounds every LLM call and fails over immediately to the deterministic fallback if Vertex AI throttles a request. - Minimal thinking budget (
_generate_gemini_text_fast): UsesADK_MODEL_NAME = "gemini-3-flash-preview"withThinkingConfig(thinking_level=ThinkingLevel.MINIMAL)(falling back toThinkingLevel.LOWif a custom model override rejectsMINIMAL) and capsmax_output_tokensat250..300, completing each Gemini call in 1.1 to 1.5 seconds. - Single-LLM-call quota conservation (
_run_intent_node): WhenconfirmedisTrueorORDER_ID_REGEXextracts an explicit 5-digit order ID from the prompt (such as 1-click prompt chips"Please refund my order ORD-90124."), Step 1 resolves the order ID directly without calling Gemini, cutting per-turn RPM consumption in half so Step 5 (ResponseAgent) consistently executes a live Gemini call. Natural-language prompts without an explicit order ID execute live Gemini calls in both Step 1 and Step 5.
Deterministic failure classification rules (evaluate_spanner_policy)¶
evaluate_spanner_policy (and commit_spanner_order_refund under the
read-write transaction lock) evaluates conditions in strict priority order and
sets evaluation_result = "PASS" only when failure_code is None:
| Priority | failure_code |
Trigger condition | Rejection reason template (route_and_commit_action) |
|---|---|---|---|
1 |
MISSING_ORDER_ID |
ctx.state["order_id"] is None (and resolution_mode not in {"list_orders", "policy_info", "cancelled"}) |
"Which order would you like to refund? Select one of your active orders below: ..." (or "Please provide a valid 5-digit order number..." when no active orders exist) |
2 |
ORDER_NOT_FOUND_OR_UNAUTHORIZED |
not eval_result["order_found"] |
"Order {order_id} was not found for user {user_id}." |
3 |
ALREADY_RETURNED |
target_order_status == contract["returned_status"] ('Returned') |
"Order {order_id} has already been returned (reset demo data to re-test)." |
4 |
ORDER_NOT_DELIVERED |
target_order_status != contract["eligible_order_status"] ('Complete') or target_order_age_days is None |
"Order {order_id} has status '{target_order_status}' and has not been delivered yet." |
5 |
RETURN_WINDOW_EXPIRED |
target_order_age_days > eval_result["return_window_days"] (30) |
"Order was delivered {target_order_age_days} days ago, exceeding the {return_window_days}-day return window." |
6 |
MAX_REFUNDS_EXCEEDED |
prior_refund_count_100d >= eval_result["max_refunds_100d"] (10 for STANDARD, 20 for VIP) |
"Customer ({customer_tier} tier) already has {prior_refund_count_100d} returned items in the last 100 days (limit: {max_refunds_100d})." |
State dictionary schemas (policy_eval and action_outcome)¶
Step 3 (evaluate_spanner_policy) writes ctx.state["policy_eval"] with the
following keys:
eval_id(str):f"EVAL-{uuid4().hex[:8]}".node_exec_id(str): The execution ID of Step 3 (evaluate_spanner_policy), whichbq_lineage.pywrites tosoa_action_lineage.policy_evaluations.node_exec_id.catalog_term_id(str):"order-age-days"whenfailure_codeis"RETURN_WINDOW_EXPIRED"or"ORDER_NOT_DELIVERED","customer-loyalty-tier"whenevaluation_result == "PASS"andcustomer_tier == catalog_contract["vip_tier"]("VIP") withprior_refund_count_100d >= int(os.getenv("MAX_REFUNDS_100D_LIMIT", "10")), and"rolling-100d-refund-count"otherwise.policy_id(str),policy_name(str),condition_id(str,"COND-REFUND-STD-100D-30D"or"COND-REFUND-VIP-100D-30D"),customer_tier("STANDARD" | "VIP"),order_found(bool),target_order_status(str | None),product_names(str), andtarget_order_price(float).target_order_age_days(int | None) andreturn_window_days(int), wherebq_lineage.pymapstarget_order_age_daystoobserved_order_age_days.prior_refund_count_100d(int) andmax_refunds_100d(int,10forSTANDARDor20forVIP), wherebq_lineage.pymapsprior_refund_count_100dtoobserved_refunds_100d.evaluation_result("PASS" | "FAIL" | "INFO") andfailure_code(str | None).
Step 4 (route_and_commit_action) writes ctx.state["action_outcome"] with the
keys action_id (f"ACT-{uuid4().hex[:8]}"), eval_id
(policy_eval["eval_id"]), action_type ("COMMIT_SPANNER_REFUND",
"BLOCK_SPANNER_MUTATION", "AWAIT_CUSTOMER_CONFIRMATION",
"LIST_CUSTOMER_ORDERS", "SHOW_ORDER_DETAILS", or "SHOW_POLICY_AND_ORDERS"),
decision_status ("APPROVED", "DENIED", "PENDING_CONFIRMATION",
"ORDER_LIST", "ORDER_INFO", or "POLICY_INFO"), and rejection_reason
(str | None).
ADK 2.0 workflow wiring (workflow.py)¶
workflow.py imports the canonical ADK 2.0 graph primitives (AdkAgent,
AdkContext, AdkWorkflow, FunctionNode, and InMemoryRunner), compiles the
five nodes and the stream_node_lineage_to_bigquery(ctx: Context, node_name:
str, latency_ms: float) callback into refund_system_of_action.adk_workflow,
emits real-time per-node "start" and "end" telemetry events via
_build_step_event and _notify_step_observer whenever step_observer is
bound on ctx.state["_step_observer"], and executes each session turn through
InMemoryRunner.run_async(...) (using a contextvars.ContextVar _ACTIVE_CTX
so live Spanner/BigQuery clients and ctx.state["lineage_buffer"] persist
across ADK node events while recording emitted google.adk.events.Event records
in ctx.adk_events):
from google.adk import Agent as AdkAgent
from google.adk import Context as AdkContext
from google.adk import Workflow as AdkWorkflow
from google.adk.runners import InMemoryRunner
from google.adk.workflow import FunctionNode
refund_system_of_action = Workflow(
name="RefundSystemOfActionWorkflow",
edges=[
("START", intent_agent),
(intent_agent, resolve_catalog_contract),
(resolve_catalog_contract, evaluate_spanner_policy),
(evaluate_spanner_policy, route_and_commit_action),
(route_and_commit_action, response_agent),
],
after_node_callbacks=[stream_node_lineage_to_bigquery],
)
Cloud Spanner Graph client and transactional mutation contract (spanner_client.py)¶
spanner_client.py encapsulates all read and write operations against Cloud
Spanner (OperationalContextGraph):
-
query_spanner_operational_context_graph(user_id: int, target_order_id: int, catalog_contract: dict) -> dict:- Executes the two-stage ISO GQL
OperationalContextGraphtraversal inside a Cloud Spanner read-only snapshot (database.snapshot()), binding the six query parameters@user_id(INT64),@target_order_id(INT64),@returned_status(STRING,'Returned'),@rolling_window_days(INT64,100),@standard_tier(STRING,'STANDARD'), and@vip_tier(STRING,'VIP') resolved fromcatalog_contract:
-- Purpose: Parameterized two-stage ISO GQL policy evaluation traversal -- executed by Step 3 (evaluate_spanner_policy) at runtime. -- Target Engine: Cloud Spanner (thelook-soa-db -> OperationalContextGraph). -- How It Works: -- Stage 1 (MATCH): Binds @user_id, @target_order_id, @standard_tier, and -- @vip_tier to traverse R2G (users -> orders -> inventory_items -> -- products -> distribution_centers) and U2G (products -> policies -> -- policy_conditions / policy_restrictions) for the customer's tier. -- Stage 2 (NEXT + OPTIONAL MATCH): Binds @returned_status ('Returned') and -- @rolling_window_days (100) to count prior returned items across other -- orders within the rolling window. -- Information Gathered: Customer tier, target order status/age/price, product -- and fulfillment center names, active policy/condition IDs, max_refunds_100d, -- return_window_days, restriction_action, and prior_refund_count_100d. GRAPH OperationalContextGraph MATCH (u:users {id: @user_id}) -[:places]->(target_o:orders {order_id: @target_order_id}) -[target_oi:contains_item]->(ii:inventory_items)-[:is_product]->(p:products) -[:supplied_by]->(dc:distribution_centers), (p)-[:governed_by]->(pol:policies {status: 'ACTIVE'}) -[:requires_condition]->(cond:policy_conditions), (pol)-[:enforces_restriction]->(res:policy_restrictions) WHERE u.customer_tier IN (@standard_tier, @vip_tier) AND cond.applicable_tier = u.customer_tier RETURN u.id AS user_id, u.email AS email, u.customer_tier AS customer_tier, target_o.order_id AS target_order_id, target_o.status AS target_order_status, TIMESTAMP_DIFF(CURRENT_TIMESTAMP(), target_o.delivered_at, DAY) AS target_order_age_days, ROUND(SUM(target_oi.sale_price), 2) AS target_order_price, STRING_AGG(DISTINCT p.name, ', ') AS product_names, STRING_AGG(DISTINCT dc.name, ', ') AS distribution_center_names, pol.policy_id AS policy_id, pol.name AS policy_name, cond.condition_id AS condition_id, cond.max_refunds_100d AS max_refunds_100d, cond.return_window_days AS return_window_days, res.action_code AS restriction_action GROUP BY user_id, email, customer_tier, target_order_id, target_order_status, target_order_age_days, policy_id, policy_name, condition_id, max_refunds_100d, return_window_days, restriction_action NEXT -- Stage 2: Count prior returned items in the rolling 100-day window OPTIONAL MATCH (u2:users)-[:places]->(hist_o:orders)-[hist_oi:contains_item]->(:inventory_items) WHERE u2.id = user_id AND hist_o.order_id != target_order_id AND hist_oi.status = @returned_status AND COALESCE(hist_oi.returned_at, hist_o.returned_at, hist_o.created_at) >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL @rolling_window_days DAY) RETURN user_id, email, customer_tier, target_order_id, target_order_status, product_names, distribution_center_names, target_order_price, target_order_age_days, COUNT(DISTINCT hist_oi.id) AS prior_refund_count_100d, policy_id, policy_name, condition_id, max_refunds_100d, return_window_days, restriction_action GROUP BY user_id, email, customer_tier, target_order_id, target_order_status, product_names, distribution_center_names, target_order_price, target_order_age_days, policy_id, policy_name, condition_id, max_refunds_100d, return_window_days, restriction_action- All seeded products in
thelook-soa-dblink toPOL-REFUND-STD-V1, and filteringcond.applicable_tier = u.customer_tierguarantees a strict1:1condition match (COND-REFUND-STD-100D-30Dwithmax_refunds_100d = 10forSTANDARDusers, andCOND-REFUND-VIP-100D-30Dwithmax_refunds_100d = 20forVIPusers). - When the traversal returns zero rows (or when
evaluate_spanner_policyshort-circuits onMISSING_ORDER_ID), the function setsorder_found = Falseand populates baseline fallback values (policy_id = "POL-REFUND-STD-V1",policy_name = "Tiered Standard and VIP Refund Policy",condition_id = "COND-REFUND-STD-100D-30D",customer_tier = "STANDARD",prior_refund_count_100d = 0,max_refunds_100d = int(os.getenv("MAX_REFUNDS_100D_LIMIT", "10")),return_window_days = 30,target_order_age_days = None,target_order_status = None) so allNOT NULLcolumns insoa_action_lineage.policy_evaluationsremain populated. commit_spanner_order_refund(user_id: int, order_id: int, trace_id: str, catalog_contract: dict, policy_eval: dict) -> dict:- Executes inside a Cloud Spanner read-write transaction
(
database.run_in_transaction()) to prevent time-of-check to time-of-use (TOCTOU) race conditions. - Re-verifies under the transaction lock (binding
@user_id,@order_id,@eligible_order_status,@returned_status,@rolling_window_days,@standard_tier, and@vip_tier) thatorders.user_id == @user_id,users.customer_tier IN (@standard_tier, @vip_tier),orders.status == catalog_contract["eligible_order_status"],target_order_age_days <= policy_eval["return_window_days"], andprior_refund_count_100d < policy_eval["max_refunds_100d"]. If a concurrent transaction modified the order or exceeded a threshold between Step 3 and Step 4, the transaction aborts the mutation, re-runs the Priority 2 to 6 failure classification to updatepolicy_evalin place (evaluation_result = "FAIL",failure_code, andcatalog_term_idrecomputed for the failure code), and returns{"action_id": f"ACT-{uuid4().hex[:8]}", "eval_id": policy_eval["eval_id"], "action_type": "BLOCK_SPANNER_MUTATION", "decision_status": "DENIED", "rejection_reason": formatted_reason}. - When preconditions hold, updates
orders(SET status = @returned_status, returned_at = CURRENT_TIMESTAMP() WHERE order_id = @order_id AND user_id = @user_id) andorder_items(SET status = @returned_status, returned_at = CURRENT_TIMESTAMP(), refund_trace_id = @trace_id WHERE order_id = @order_id), and returns{"action_id": f"ACT-{uuid4().hex[:8]}", "eval_id": policy_eval["eval_id"], "action_type": "COMMIT_SPANNER_REFUND", "decision_status": "APPROVED", "rejection_reason": None}.
- Executes the two-stage ISO GQL
BigQuery Action Lineage callback contract (bq_lineage.py)¶
stream_node_lineage_to_bigquery populates the four property graph tables of
soa_action_lineage.ActionLineageGraph (defined in
data/README.md). During Steps 1 through 4, the callback
buffers each node's telemetry in ctx.state["lineage_buffer"] to keep per-node
BigQuery network calls out of the Step 3-to-Step 4 transaction window. When
Step 5 (ResponseAgent) finishes, the callback flushes all four tables in a
single batched client.insert_rows_json() sequence with 3-attempt exponential
backoff. If a transient BigQuery streaming error occurs after Step 4 commits in
Cloud Spanner, order_items.refund_trace_id is the durable Spanner-side
reconciliation identifier linking the order mutation to trace_id:
agent_sessions(1 row per turn): Recordstrace_id,customer_id = ctx.state["user_id"],target_order_id = ctx.state["order_id"](NULLwhen omitted),user_prompt, andstarted_atwhen Step 1 (IntentAgent) completes.workflow_nodes(5 rows per turn): Recordsnode_exec_id,trace_id,node_name,step_order,latency_ms, andexecuted_atafter each of the five nodes (step_order1..5:IntentAgent,resolve_catalog_contract,evaluate_spanner_policy,route_and_commit_action,ResponseAgent) finishes.policy_evaluations(1 row per turn): Recordseval_id,node_exec_id(always bound to thenode_exec_idof Step 3,evaluate_spanner_policy),trace_id,catalog_term_id,policy_id,policy_name,observed_refunds_100d(fromprior_refund_count_100d),max_refunds_100d,observed_order_age_days(fromtarget_order_age_days),return_window_days,evaluation_result, andevaluated_atfromctx.state["policy_eval"], including any concurrent conflict detected during Step 4.agent_actions(1 row per turn): Recordsaction_id,eval_id,trace_id,action_type,decision_status,rejection_reason, andcreated_atfromctx.state["action_outcome"].
Knowledge Catalog client with local JSON fallback (catalog_client.py)¶
fetch_knowledge_catalog_aspect() queries the Knowledge Catalog API
(projects/{project}/locations/global/glossaries/soa-business-glossary, aspect
type refund-metric-contract) provisioned by Terraform via
get_glossary() and list_glossary_terms() to resolve the live governed term
IDs (rolling-100d-refund-count, order-age-days, and
customer-loyalty-tier), merges the metric threshold parameters from
business_glossary.json, and caches the resolved contract dictionary in memory
with a 5-minute time-to-live (TTL):
{
"catalog_term_id": "rolling-100d-refund-count",
"secondary_term_id": "order-age-days",
"tier_term_id": "customer-loyalty-tier",
"returned_status": "Returned",
"eligible_order_status": "Complete",
"rolling_window_days": 100,
"standard_tier": "STANDARD",
"vip_tier": "VIP"
}
The workflow passes returned_status, rolling_window_days, standard_tier,
and vip_tier into the Step 3 read-only GQL traversal, checks
eligible_order_status and tier membership in Step 3 Python logic and Step 4's
read-write transaction lock, and records the governing term ID in
soa_action_lineage.policy_evaluations ("order-age-days" when
RETURN_WINDOW_EXPIRED or ORDER_NOT_DELIVERED triggers,
"customer-loyalty-tier" when a VIP customer is approved with
prior_refund_count_100d >= 10, and "rolling-100d-refund-count" otherwise).
If the Knowledge Catalog API is unreachable or propagating Identity and Access
Management (IAM) bindings, catalog_client.py loads
data/policies/business_glossary.json so workflow execution continues without
blocking on API availability.
Conversational agent security and identity binding¶
The ADK 2.0 workflow enforces a three-layer defense against prompt injection, role spoofing, and unauthorized order access:
- Tier 1: Session identity binding and order ownership:
- In this lab,
POST /api/chat/refundacceptsuser_id(1001,2048, or3072) as a structured JSON field from the UI 1 Persona Switcher outside the natural language chat message so students can switch between Customer A, Customer B, and Customer C. FastAPI bindsctx.state["user_id"]before the workflow starts, preventing prompt text inmessagefrom overridinguser_idor spoofing VIP status (becausecustomer_tieris read directly fromusers.customer_tierin Cloud Spanner for@user_id). - In
evaluate_spanner_policyandcommit_spanner_order_refund, Cloud Spanner binds@user_idand@target_order_idas typedINT64parameters along(u:users {id: @user_id})-[:places]->(target_o:orders {order_id: @target_order_id}). If Customer B requests a refund for Customer A's order (order_id: 90124) or Customer C's order (order_id: 90910), the traversal returns zero rows and fails closed withORDER_NOT_FOUND_OR_UNAUTHORIZED.
- In this lab,
- Tier 2: Pre-execution negative role boundaries and anti-spoofing:
IntentAgentandResponseAgentsystem instructions forbid the LLM from modifyinguser_id,customer_tier, or policy thresholds, calling database mutation tools, honoring user claims of VIP or administrator override privileges in chat text, or exposing internalctx.statevariable names.
- Tier 3: Deterministic policy and mutation gate (
spanner_client.py):- Neither
IntentAgentnorResponseAgenthas tool access to mutate Cloud Spanner. Only the deterministic Python steproute_and_commit_actioncan callcommit_spanner_order_refund(), and only whenevaluate_spanner_policyreturnsevaluation_result == "PASS".
- Neither