diff --git a/arbiter/fabricator/index.py b/arbiter/fabricator/index.py index 4dacb56..63cfca7 100644 --- a/arbiter/fabricator/index.py +++ b/arbiter/fabricator/index.py @@ -2666,10 +2666,13 @@ def store_agent_config_registry_bound( agent_id=agent_use_id, agent_name=agent_use_id, registration_errors=registration_errors, org_id=org_id, + # Always write registryApprovalStatus on COMPLETED jobs so the + # intake status-check never falls back to a dangerous default. + # Priority: pending_approval > draft > approved. registry_approval_status=( "pending_approval" if run_state is not None and run_state.pending_approval - else None + else "approved" ), ) diff --git a/service/agent_intake_single/requirements.txt b/service/agent_intake_single/requirements.txt index 0c7de70..a389733 100644 --- a/service/agent_intake_single/requirements.txt +++ b/service/agent_intake_single/requirements.txt @@ -1,6 +1,6 @@ bedrock-agentcore>=1.18.1 strands-agents[otel]>=1.50.1 fastapi>=0.140.0 -boto3>=1.43.56 +boto3==1.43.116 python-dotenv>=1.2.2 requests>=2.34.2 diff --git a/service/agent_intake_single/tests/test_botocore_registry_service.py b/service/agent_intake_single/tests/test_botocore_registry_service.py new file mode 100644 index 0000000..a377bab --- /dev/null +++ b/service/agent_intake_single/tests/test_botocore_registry_service.py @@ -0,0 +1,22 @@ +"""Container-level smoke test: botocore ships the agent-registry-control service model. + +This test MUST be run with the pinned dependencies from requirements.txt +(e.g. inside the container image or a venv created from that file). It will +fail in any environment whose botocore predates the agent-registry-control +service model — that is intentional: a failing build proves the image would +raise UnknownServiceError at runtime. +""" + +import botocore +from botocore.session import get_session + + +def test_agent_registry_control_is_available(): + """botocore must include the agent-registry-control service model.""" + session = get_session() + available = session.get_available_services() + assert "agent-registry-control" in available, ( + f"botocore {botocore.__version__} does not include " + f"'agent-registry-control' — pin boto3/botocore to a version " + f"that ships this service model (>= 1.43.116 known good)" + ) diff --git a/service/agent_intake_single/tests/test_postfab_approval_status.py b/service/agent_intake_single/tests/test_postfab_approval_status.py index 0d826a3..f392a5e 100644 --- a/service/agent_intake_single/tests/test_postfab_approval_status.py +++ b/service/agent_intake_single/tests/test_postfab_approval_status.py @@ -145,11 +145,13 @@ def test_pending_approval_agent_state_reads_awaiting( def test_approved_agent_state_reads_built_and_approved( self, jobs_table, marker_store, ): - items = [_job("AgentA", "COMPLETED")] # no registryApprovalStatus = auto-approved + items = [_job("AgentA", "COMPLETED")] # no registryApprovalStatus = unknown result = _run_status(jobs_table, items, marker_store) states = {a["name"]: a["state"] for a in result["agents"]} - assert states["AgentA"] == "built and approved" + # Without an explicit registryApprovalStatus, the fallback is + # "could not be confirmed" — never "approved" by default. + assert states["AgentA"] == "built; approval status could not be confirmed" def test_mixed_approved_and_pending_partial_like( self, jobs_table, marker_store, diff --git a/service/agent_intake_single/tests/test_postfab_live_registry.py b/service/agent_intake_single/tests/test_postfab_live_registry.py index 4cd0fec..1248dbf 100644 --- a/service/agent_intake_single/tests/test_postfab_live_registry.py +++ b/service/agent_intake_single/tests/test_postfab_live_registry.py @@ -373,9 +373,9 @@ def test_registry_unset_falls_back_to_snapshot( result = _run_status(jobs_table, items, marker_store) - # No REGISTRY_ID -> assume auto-approved (legacy behavior) + # No REGISTRY_ID -> no live lookup, no snapshot -> "could not be confirmed" states = {a["name"]: a["state"] for a in result["agents"]} - assert states["AgentA"] == "built and approved" + assert states["AgentA"] == "built; approval status could not be confirmed" # =========================================================================== @@ -459,9 +459,9 @@ def test_only_first_five_agents_resolved( states = {a["name"]: a["state"] for a in result["agents"]} for n in names[:5]: assert states[n] == "built and approved" - # Last 2 get snapshot fallback (no registryApprovalStatus = auto-approved) + # Last 2 get snapshot fallback (no registryApprovalStatus = "could not be confirmed") for n in names[5:]: - assert states[n] == "built and approved" + assert states[n] == "built; approval status could not be confirmed" # =========================================================================== diff --git a/service/agent_intake_single/tools/postfab.py b/service/agent_intake_single/tools/postfab.py index 62cd44f..3849393 100644 --- a/service/agent_intake_single/tools/postfab.py +++ b/service/agent_intake_single/tools/postfab.py @@ -148,9 +148,13 @@ def _resolve_registry_status(agent_name: str, org_id: str) -> str | None: return summary.get("status") return None except Exception as e: - logger.warning( - "registry status lookup failed agent=%s: %s", - agent_name, type(e).__name__, + # Log at ERROR (not warning) so CloudWatch metric filters can alert + # on registry-client unavailability — the fallback path must never + # silently default to 'approved'. + logger.error( + "registry_status_lookup_failed agent=%s error_type=%s: %s " + "(approval status will show 'could not be confirmed')", + agent_name, type(e).__name__, e, ) return None @@ -205,7 +209,10 @@ def _plain_state(item: dict, live_status: str | None = None) -> str: approval = item.get("registryApprovalStatus") if approval == "pending_approval": return _APPROVAL_PLAIN["pending_approval"] - return _APPROVAL_PLAIN.get(approval, "built and approved") + if approval == "approved": + return _APPROVAL_PLAIN["approved"] + # Unknown or absent approval status — never default to "approved". + return "built; approval status could not be confirmed" return _STATUS_PLAIN.get(status, "in an unknown state")