Send organization_id when the MAS gate lists the application's API keys.

The attended mint run on 2026-11-03 got HTTP 400 from the key listing before
any key was minted. A Console session needs organization_id there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4541@bnt-lap001
Assistant-Session: 2099b3aa-d7d9-49a9-ab4e-fa8b7d0e39e8
This commit is contained in:
codex 2026-11-03 05:36:45 +01:00
parent fbc4830aa0
commit de13931c75
3 changed files with 24 additions and 9 deletions

View file

@ -671,11 +671,16 @@ def write_pair(pair):
return summary
def _app_keys(api):
value = api(
"GET",
"/iam/v1alpha1/api-keys?application_id=" + APPLICATION_ID + "&page_size=100",
)
def _app_keys(api, organization):
"""List the application's keys. A Console session needs organization_id or gets HTTP 400."""
from urllib.parse import urlencode
query = urlencode({
"organization_id": organization,
"application_id": APPLICATION_ID,
"page_size": 100,
})
value = api("GET", "/iam/v1alpha1/api-keys?" + query)
keys = value.get("api_keys") if isinstance(value, dict) else None
if not isinstance(keys, list) or value.get("total_count") != len(keys):
raise GateError("key_inventory_rejected")
@ -699,9 +704,10 @@ def mint_workload_pair(api, prove, write, sleep=time.sleep):
"""
receipt = {"application_id": APPLICATION_ID, "values_recorded": False}
app = api("GET", "/iam/v1alpha1/applications/" + APPLICATION_ID)
if app.get("name") != POLICY_NAME:
organization = app.get("organization_id")
if app.get("name") != POLICY_NAME or not isinstance(organization, str) or not organization:
raise GateError("application_binding_refused")
before = _app_keys(api)
before = _app_keys(api, organization)
receipt["keys_before"] = _key_view(before)
if len(before) != 1 or before[0].get("default_project_id") != PROJECT_ID:
raise GateError("supplier_key_state_requires_review", receipt["keys_before"])
@ -745,7 +751,7 @@ def mint_workload_pair(api, prove, write, sleep=time.sleep):
detail = dict(exc.detail or {}) if isinstance(exc, GateError) and isinstance(exc.detail, dict) else {}
detail["minted_key_deleted"] = deleted
raise GateError(code, detail) from None
receipt["keys_after"] = _key_view(_app_keys(api))
receipt["keys_after"] = _key_view(_app_keys(api, organization))
receipt["status"] = "passed"
return receipt

View file

@ -269,8 +269,9 @@ class _FakeConsole:
def __call__(self, method, path, data=None):
self.calls.append((method, path))
if method == "GET" and path.startswith("/iam/v1alpha1/applications/"):
return {"name": self.name}
return {"name": self.name, "organization_id": "org-1"}
if method == "GET" and path.startswith("/iam/v1alpha1/api-keys?"):
assert "organization_id=org-1" in path and "application_id=" + gate.APPLICATION_ID in path
return {"api_keys": list(self.keys), "total_count": len(self.keys)}
if method == "POST":
assert data["application_id"] == gate.APPLICATION_ID

View file

@ -104,6 +104,14 @@ The supplier key stays founder custody and can be revoked separately.
Tests cover the mint, retry, deletion and refusal paths. The attended
run is still pending.
2026-11-03 05:36 CET: the first attended `--mint-workload` run exited with
`provider_http_error`, a GET returning HTTP 400, before any key was minted.
The denials receipt was not rewritten and the pair stayed empty. Cause: the
API-key listing omitted `organization_id`, which a Console session requires.
`mas_evidence_iam_grant.py` already sends it. Fix: the gate takes
`organization_id` from the application read and sends it on both key
listings. The next attended run is the same command.
## Admit delivery and place the node file
```task