Skip to content

[Bug] gem4gov app update-compliance strips the Model Armor binding — it PATCHes customerPolicy with a body that omits modelArmorConfig while the updateMask still names customerPolicy #184

Description

@JohnHales

Bug Description

Two components write the assistant's customerPolicy, and the second silently discards what the first configured.

deploy.sh binds the template correctly. When an application has enable_model_armor = true and a template exists, it merges modelArmorConfig into the assistant patch and widens the mask:

# deploy.sh:2565-2568
if [[ "$ENABLE_MA" == "true" && -n "$MODEL_ARMOR_TEMPLATE_NAME" && "$MODEL_ARMOR_TEMPLATE_NAME" != "null" ]]; then
     ASSISTANT_BODY=$(echo "$BASE_JSON" | jq --arg template "$MODEL_ARMOR_TEMPLATE_NAME" \
       '. + {customerPolicy: {modelArmorConfig: {userPromptTemplate: $template, responseTemplate: $template, failureMode: "FAIL_OPEN"}}}')
     MASK="${MASK},customerPolicy"
fi

gem4gov app update-compliance then overwrites it. For FedRAMP High, configure_gemini_enterprise_for_fedramp_high() (gem4gov.py:1526) PATCHes the same assistant:

# gem4gov.py:1577 — the mask still names customerPolicy
url = f"...{assistant_name}?updateMask=customerPolicy,agentConfigs,generationConfig,disableLocationContext,webGroundingType,defaultWebGroundingToggleOff"

# gem4gov.py:1583-1585 — but the body has no modelArmorConfig
"customerPolicy":{
  "bannedPhrases":[]
},

A field named in an updateMask is replaced wholesale, not merged. So this PATCH sets customerPolicy to a value containing only an empty bannedPhrases list — the API drops the empty list, leaving customerPolicy: {} — and the modelArmorConfig deploy.sh just wrote is gone.

The two are not far apart in practice: deploy.sh:2802 is the Helper Function that runs gem4gov app update-compliance, and the command is the documented way to re-assert compliance posture on an existing engine. Running it is the recommended remediation for a drifted deployment — so the action an operator takes to restore compliance is the action that removes this control.

Scope: FedRAMP High only. The IL4 (gem4gov.py:1642) and IL5 (gem4gov.py:1754) paths use updateMask=generationConfig.defaultLanguage,webGroundingType,defaultWebGroundingToggleOff,enableEndUserAgentCreation,disableLocationContext, which does not name customerPolicy, so they leave an existing binding alone. (The original write-up said all three regimes carried the same body — that was wrong, and is corrected here.)

Environment and Deployment Context

  • Stellar Engine Version/Commit: main at commit f64ce6cd (re-verified 2026-08-10)
  • Deployment Type:
    • US Region Restricted (e.g., Access Policy constraint)
    • FedRAMP Medium
    • FedRAMP High
    • FedRAMP Moderate
    • DoD IL4
    • DoD IL5
    • Stand-alone / Custom
  • FAST Stage (if applicable): N/A — this is a blueprint, not a FAST stage
    • Stage 0 (Bootstrap)
    • Stage 1 (Resource Management)
    • Stage 2 (Network Creation)
    • Stage 3 (Security and Audit)
  • Affected Component: blueprints/fedramp-high/gemini-enterprise/deploy.sh, blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py
  • Terraform Version: 1.12.2 (pinned by deploy.sh via tfenv; the stage declares required_version >= 1.7.4)
  • GCP Provider Version: hashicorp/google >= 6.21.0 (stage-0 declared constraint)

Steps to Reproduce

  1. Deploy gemini-stage-0 answering yes to Model Armor for an application.
  2. Read the assistant and confirm customerPolicy.modelArmorConfig is present: GET https://us-discoveryengine.googleapis.com/v1alpha/projects/<project>/locations/us/collections/default_collection/engines/<engine>/assistants/default_assistant
  3. gem4gov app update-compliance --project-id <project> --engine-id <engine> --compliance-regime FEDRAMP_HIGH
  4. Read the assistant again.

Expected Behavior

update-compliance preserves — or re-asserts — customerPolicy.modelArmorConfig. Re-running the command that enforces compliance posture should never remove a compliance control.

Actual Behavior

customerPolicy comes back empty. The Model Armor template still exists in the project, it is simply no longer referenced by the assistant, so no prompt or response is screened. The command prints Compliance configuration complete! on its way out.

Relevant Logs and Errors

Observed live 2026-07-29, reading the assistant back at step 4:

{ "googleSearchGroundingEnabled": null,
  "webGroundingType": "WEB_GROUNDING_TYPE_ENTERPRISE_WEB_SEARCH",
  "disableLocationContext": true,
  "customerPolicy": {} }

The empty bannedPhrases list the CLI sends is dropped by the API, which is why customerPolicy reads {} rather than {"bannedPhrases": []}. The template is still present at projects/<project>/locations/us/templates/<prefix>-model-armor-template.

No error is emitted at any point — the PATCH is issued as curl -s -o /dev/null with no status or body check, so a silently-destructive write looks identical to a successful one.

Impact

Model Armor is one of the controls update-compliance explicitly lists as required for FedRAMP High ("- Model Armor" in its own printed output). The command prints Compliance configuration complete! immediately after removing it. An operator who enabled Model Armor, saw the template in the console, and then re-ran the compliance command has every reason to believe prompt screening is on; it is not, and nothing surfaces the change unless the assistant is read back by hand. A control that is silently removed by the tool that claims to enforce it is worse than one that was never enabled — the deployment now attests to a screening posture it does not have.

Additional Context

Source locations:

  • blueprints/fedramp-high/gemini-enterprise/deploy.sh:2565-2578 — binds the template, then PATCHes with curl -s -o /dev/null (no status check; the same pattern appears in the usage-audit logging defect reported separately)
  • blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py:1577 (mask), :1583-1585 (body) — strips it
  • blueprints/fedramp-high/gemini-enterprise/deploy.sh:2802 — the Helper Function that invokes update-compliance

Blocks a FedRAMP High control (prompt/response screening) silently, so it is worth treating as a compliance defect rather than a UX one.

Suggested fix

Have configure_gemini_enterprise_for_fedramp_high() read the current assistant first and preserve any modelArmorConfig it finds, or accept the template name and re-assert the binding:

"customerPolicy": {
  "bannedPhrases": [],
  "modelArmorConfig": {
    "userPromptTemplate": "projects/<project>/locations/us/templates/<prefix>-model-armor-template",
    "responseTemplate":   "projects/<project>/locations/us/templates/<prefix>-model-armor-template",
    "failureMode": "FAIL_OPEN"
  }
}

Failing that, drop customerPolicy from the updateMask — the only thing the body contributes is an empty bannedPhrases list the API discards anyway, so removing it from the mask costs nothing and stops the clobber. Either way, modelArmorConfig should be part of whatever the tool verifies before printing Compliance configuration complete!.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions