feat(resources): implement cloud infrastructure resource types (AWS, GCP, Azure stubs) #10592

Merged
HAL9000 merged 5 commits from feat/v360/cloud-resource-types into master 2026-06-15 18:57:35 +00:00
7 changed files with 754 additions and 3 deletions
+1
View File
@@ -73,6 +73,7 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
* HAL 9000 has contributed cloud infrastructure resource type stubs (PR #10592 / issue #8607): implemented the `CloudResource` base Pydantic model with provider-specific subclasses (`AWSResource`, `GCPResource`, `AzureResource`) including field validation, auto-provider assignment, and lower-casing of provider IDs. The feature is part of Epic #8568 (Resource Types & Container Tool Execution). Included BDD Behave tests covering all instantiation and validation scenarios.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568).
* HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback <plan-id> [<checkpoint-id>]` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality.
+53
View File
@@ -0,0 +1,53 @@
Feature: Cloud resource type stubs
Cloud resource type stubs provide base models for AWS, GCP, and Azure
infrastructure resources. They validate provider names, regions, and
provider-specific fields without executing any cloud SDK operations.
Background:
Given the cloud resource stub module is available
Scenario: CloudResource accepts a provider name and validates it
Given a new CloudResource is being created
When a valid provider "aws" is set
Then the resource should have provider "aws"
Scenario: CloudResource rejects an unknown provider
Given a new CloudResource is being created
When an invalid provider "oracle" is set
Then it should reject an invalid provider
Scenario: CloudResource accepts and stores resource_type field
Given a new CloudResource with type "ec2_instance"
When the resource_type is accepted as expected
Then the resource_type should be "ec2_instance"
Scenario: AWSResource auto-sets provider to "aws"
Given a new AWSResource is being created
When provider is auto-set with resource_id "aws-res-1"
Then the resource should have provider "aws"
Scenario: GCPResource auto-sets provider to "gcp"
Given a new GCPResource is being created
When provider is auto-set with project_id "my-project"
Then the resource should have provider "gcp"
Scenario: AzureResource auto-sets provider to "azure"
Given a new AzureResource is being created
When provider is auto-set with subscription_id "sub-123"
Then the resource should have provider "azure"
Scenario: AWSResource validates tags
Given a new AWSResource with tags
When a tag has an empty key
Then it should reject tags with empty key
Scenario: GCPResource validates project_id
Given a new GCPResource is being created
When project_id is set to "My-Project"
Then the project_id should be lowercased
Scenario: AzureResource validates subscription_id
Given a new AzureResource is being created
When subscription_id is set to "SUB-123"
Then the subscription_id should be lowercased
+267
View File
@@ -0,0 +1,267 @@
"""Behave step definitions for cloud resource type stubs."""
from __future__ import annotations
from pydantic import ValidationError
from behave import given, then, when
from cleveragents.resource.cloud_types import (
AWSResource,
AzureResource,
CloudResource,
GCPResource,
)
@given("the cloud resource stub module is available")
def step_module_available(context):
"""Cloud resource stub module is importable."""
assert CloudResource is not None
assert AWSResource is not None
assert GCPResource is not None
assert AzureResource is not None
# ----------------------------- CloudResource -----------------------------
@given("a new CloudResource is being created")
def step_new_cloud_resource(context):
"""Store an incomplete CloudResource for construction."""
context.pending_resource = {
"resource_id": "cloud-test-001",
"region": "us-east-1",
}
@when('a valid provider "aws" is set')
def step_set_aws_provider(context):
"""Set valid provider on pending resource and try instantiation."""
data = dict(context.pending_resource)
data["provider"] = "aws"
try:
context.exc = None
Outdated
Review

BLOCKER — TEST QUALITY: except ValueError cannot catch pydantic.ValidationError

In Pydantic v2, field_validator raises pydantic.ValidationError (not ValueError) when validation fails. ValidationError is not a subclass of ValueError, so this except ValueError block will never execute. When CloudResource(provider="oracle") is called, the ValidationError propagates uncaught to Behave, which records the scenario as ERROR — causing the unit_tests CI failure.

Fix:

# Add import at the top:
from pydantic import ValidationError

# Replace all 8 occurrences of:
except ValueError as exc:
    context.exc = exc

# With:
except ValidationError as exc:
    context.exc = exc

This replacement must be made at lines 44, 56, 101, 125, 155, 169, 201, 215. Also update step_rejects_invalid_provider and step_rejects_empty_tag_key to assert isinstance(context.exc, ValidationError) instead of isinstance(context.exc, ValueError).

Project standard: 1090+ existing uses of except ValidationError in features/steps/ — e.g., automation_profile_steps.py, action_model_steps.py.

**BLOCKER — TEST QUALITY: `except ValueError` cannot catch `pydantic.ValidationError`** In Pydantic v2, `field_validator` raises `pydantic.ValidationError` (not `ValueError`) when validation fails. `ValidationError` is not a subclass of `ValueError`, so this `except ValueError` block will never execute. When `CloudResource(provider="oracle")` is called, the `ValidationError` propagates uncaught to Behave, which records the scenario as ERROR — causing the `unit_tests` CI failure. **Fix:** ```python # Add import at the top: from pydantic import ValidationError # Replace all 8 occurrences of: except ValueError as exc: context.exc = exc # With: except ValidationError as exc: context.exc = exc ``` This replacement must be made at lines 44, 56, 101, 125, 155, 169, 201, 215. Also update `step_rejects_invalid_provider` and `step_rejects_empty_tag_key` to assert `isinstance(context.exc, ValidationError)` instead of `isinstance(context.exc, ValueError)`. Project standard: 1090+ existing uses of `except ValidationError` in `features/steps/` — e.g., `automation_profile_steps.py`, `action_model_steps.py`.
context.result = CloudResource(**data)
except ValidationError as exc:
context.exc = exc
@when('an invalid provider "oracle" is set')
Outdated
Review

TEST QUALITY BLOCKER: except ValueError cannot catch pydantic.ValidationError. Pydantic v2 wraps field_validator raises of ValueError inside ValidationError. Fix: replace all 8 except ValueError with except ValidationError (and add the import).

TEST QUALITY BLOCKER: except ValueError cannot catch pydantic.ValidationError. Pydantic v2 wraps field_validator raises of ValueError inside ValidationError. Fix: replace all 8 except ValueError with except ValidationError (and add the import).
def step_set_invalid_provider(context):
"""Set invalid provider and expect a validation error."""
data = dict(context.pending_resource)
data["provider"] = "oracle"
try:
context.exc = None
context.result = CloudResource(**data)
except ValidationError as exc:
context.exc = exc
@then('the resource should have provider "aws"')
def step_resource_provider_is_aws(context):
"""Assert the constructed resource has provider 'aws'."""
assert context.exc is None
assert context.result.provider == "aws"
@then("it should reject an invalid provider")
def step_rejects_invalid_provider(context):
"""Assert a ValidationError was raised for an invalid provider."""
assert context.exc is not None
assert isinstance(context.exc, ValidationError)
@given('a new CloudResource with type "{resource_type}"')
def step_new_cloud_resource_with_type(context, resource_type: str):
"""Store a CloudResource with specified resource_type for construction."""
context.pending_resource = {
"resource_id": "cloud-test-001",
"type": resource_type,
}
@when("the resource_type is accepted as expected")
def step_cloud_resource_type_accepted(context):
"""Create the CloudResource with pending data including provider."""
data = dict(context.pending_resource)
data["provider"] = "aws"
try:
context.exc = None
context.result = CloudResource(**data)
except ValidationError as exc:
context.exc = exc
@then('the resource_type should be "{expected}"')
def step_resource_type_is(context, expected: str):
"""Assert the constructed resource has the expected resource_type."""
assert context.exc is None
assert context.result.type == expected
@given("a new AWSResource is being created")
def step_new_aws_resource(context):
"""Store an incomplete AWSResource for construction."""
context.pending_resource = {
"resource_id": "aws-res-test-001",
"region": "us-west-2",
}
@when('provider is auto-set with resource_id "aws-res-1"')
def step_aws_auto_provider(context):
"""Create AWSResource with auto-set provider."""
data = dict(context.pending_resource)
data["resource_id"] = "aws-res-1"
try:
context.exc = None
context.result = AWSResource(**data)
except ValidationError as exc:
context.exc = exc
@given("a new AWSResource with tags")
def step_new_aws_resource_with_tags(context):
"""Store an AWSResource with tags for construction."""
context.tags_resource = {
"resource_id": "aws-res-tags-001",
"region": "us-east-1",
}
@when("a tag has an empty key")
def step_aws_empty_tag_key(context):
"""Set tag with empty key and expect ValueError."""
data = {
"resource_id": "aws-res-tags-001",
"region": "us-east-1",
"tags": {"": "value"},
}
try:
context.exc = None
context.result = AWSResource(**data)
except ValidationError as exc:
context.exc = exc
@then("it should reject tags with empty key")
def step_rejects_empty_tag_key(context):
"""Assert a ValidationError was raised for a tag with an empty key."""
assert context.exc is not None
assert isinstance(context.exc, ValidationError)
@when('its region is set to "us-west-2"')
def step_aws_region(context):
"""Set AWS resource region."""
context.pending_resource["region"] = "us-west-2"
# ----------------------------- GCPResource -------------------------------
@given("a new GCPResource is being created")
def step_new_gcp_resource(context):
"""Store an incomplete GCPResource for construction."""
context.pending_resource = {
"project_id": "my-project",
"region": "us-central-1",
}
@when('provider is auto-set with project_id "my-project"')
def step_gcp_auto_provider(context):
"""Create GCPResource with auto-set provider."""
data = dict(context.pending_resource)
data["project_id"] = "my-project"
try:
context.exc = None
context.result = GCPResource(**data)
except ValidationError as exc:
context.exc = exc
@when('project_id is set to "My-Project"')
def step_gcp_project_id_lowercase(context):
"""Set project_id with mixed case and expect lowercased result."""
data = {
"project_id": "My-Project",
"region": "us-central-1",
}
try:
context.exc = None
context.result = GCPResource(**data)
except ValidationError as exc:
context.exc = exc
@then("the project_id should be lowercased")
def step_gcp_project_id_is_lowercase(context):
"""Assert project_id was lowercased."""
assert context.exc is None
assert context.result.project_id == "my-project"
# ----------------------------- AzureResource -----------------------------
@given("a new AzureResource is being created")
def step_new_azure_resource(context):
"""Store an incomplete AzureResource for construction."""
context.pending_resource = {
"subscription_id": "sub-123",
"tenant_id": "tenant-456",
}
@when('provider is auto-set with subscription_id "sub-123"')
def step_azure_auto_provider(context):
"""Create AzureResource with auto-set provider."""
data = dict(context.pending_resource)
data["subscription_id"] = "sub-123"
data["tenant_id"] = "tenant-456"
try:
context.exc = None
context.result = AzureResource(**data)
except ValidationError as exc:
context.exc = exc
@when('subscription_id is set to "SUB-123"')
def step_azure_subscription_id_lowercase(context):
"""Set subscription_id with mixed case and expect lowercased."""
data = {
"subscription_id": "SUB-123",
"tenant_id": "TENTANT-456",
}
try:
context.exc = None
context.result = AzureResource(**data)
except ValidationError as exc:
context.exc = exc
@then('the resource should have provider "azure"')
def step_azure_provider_is_azure(context):
"""Assert AzureResource has provider 'azure'."""
assert context.exc is None
assert context.result.provider == "azure"
@then("the subscription_id should be lowercased")
def step_azure_subscription_is_lowercase(context):
"""Assert subscription_id was lowercased."""
assert context.exc is None
assert context.result.subscription_id == "sub-123"
# ------------------------ GCPResource provider assertion --------------------
@then('the resource should have provider "gcp"')
def step_gcp_provider_is_gcp(context):
"""Assert GCPResource has provider 'gcp'."""
assert context.exc is None
assert context.result.provider == "gcp"
+1 -1
View File
@@ -199,7 +199,7 @@ def run(
except CleverAgentsError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=2) from exc
except Exception as exc: # pragma: no cover
except Exception as exc:
typer.echo(f"Unexpected error: {exc}", err=True)
raise typer.Exit(code=3) from exc
+1 -1
View File
@@ -164,7 +164,7 @@ def run(
except CleverAgentsError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=2) from exc
except Exception as exc: # pragma: no cover
except Exception as exc:
typer.echo(f"Unexpected error: {exc}", err=True)
raise typer.Exit(code=3) from exc
+18 -1
View File
@@ -1,5 +1,17 @@
"""Resource type schema loading, validation, and inheritance resolution."""
"""Resource type schema loading, validation, and inheritance resolution.
Cloud resource type stubs (``CloudResource``, ``AWSResource``,
``GCPResource``, ``AzureResource``) are also exported from this
package for convenient access.
"""
from cleveragents.resource.cloud_types import (
CLOUD_PROVIDERS,
AWSResource,
AzureResource,
CloudResource,
GCPResource,
)
from cleveragents.resource.inheritance import (
MAX_CHAIN_DEPTH,
ResourceTypeCircularInheritanceError,
@@ -20,8 +32,13 @@ from cleveragents.resource.virtual import (
)
__all__ = [
"CLOUD_PROVIDERS",
"MAX_CHAIN_DEPTH",
"APIEndpointResource",
"AWSResource",
"AzureResource",
"CloudResource",
"GCPResource",
"MetricResource",
"ResourceTypeCircularInheritanceError",
"ResourceTypeInheritanceDepthError",
+413
View File
@@ -0,0 +1,413 @@
"""Cloud infrastructure resource type stubs for CleverAgents.
Provides the base :class:`CloudResource` and provider-specific stubs
:class:`AWSResource`, :class:`GCPResource`, and :class:`AzureResource`.
These stubs define the domain shapes for cloud infrastructure resources
used by the ``CloudResourceHandler``. They do **not** execute any
cloud SDK operations -- actual provisioning raises
:exc:`NotImplementedError`.
## Provider-specific stubs
| Class | Provider | Key properties |
|-----------------|--------------|--------------------------------------------|
| ``CloudResource`` | generic | provider, type, region, resrc id, state |
| ``AWSResource`` | AWS | arn, tags, properties |
| ``GCPResource`` | GCP | project_id, properties |
| ``AzureResource`` | Azure | sub_id, tenant_id, props |
Based on:
- Issue #343: Cloud Infrastructure Resources
- Handler: cleveragents.resource.handlers.cloud
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
# ---------------------------------------------------------------------------
# Module-level constants (must precede classes that reference them in validators)
# ---------------------------------------------------------------------------
#: Set of recognised cloud providers.
CLOUD_PROVIDERS: frozenset[str] = frozenset({"aws", "gcp", "azure"})
class CloudResource(BaseModel):
"""Base stub for all cloud infrastructure resources.
Provides common properties shared across all cloud providers
and serves as the parent class for concrete provider stubs.
**Constraints**:
- ``provider`` must match a known cloud provider (aws, gcp, azure)
- ``region`` must be a non-empty string
**Convenience methods**:
- ``is_aws()`` -- Check if the provider is AWS
- ``is_gcp()`` -- Check if the provider is GCP
- ``is_azure()`` -- Check if the provider is Azure
"""
model_config = ConfigDict(frozen=True, str_strip_whitespace=True)
resource_id: str | None = Field(
Outdated
Review

BLOCKER — CORRECTNESS: resource_type field missing from CloudResource

Issue #8607 Acceptance Criterion requires:

CloudResource base class is defined with: provider, region, resource_type, resource_id

The current class defines resource_id, provider, region, account_id, and state — but resource_type is absent.

Fix: Add after region:

resource_type: str | None = Field(
    default=None,
    description="Type of cloud resource (e.g., s3_bucket, ec2_instance, gcs_bucket).",
)

Also add a Behave scenario verifying the field is accepted and populated on a constructed resource.

**BLOCKER — CORRECTNESS: `resource_type` field missing from `CloudResource`** Issue #8607 Acceptance Criterion requires: > `CloudResource` base class is defined with: `provider`, `region`, `resource_type`, `resource_id` The current class defines `resource_id`, `provider`, `region`, `account_id`, and `state` — but `resource_type` is absent. **Fix:** Add after `region`: ```python resource_type: str | None = Field( default=None, description="Type of cloud resource (e.g., s3_bucket, ec2_instance, gcs_bucket).", ) ``` Also add a Behave scenario verifying the field is accepted and populated on a constructed resource.
default=None,
description="Unique identifier for the cloud resource (e.g., ARN, ID).",
)
provider: str = Field(
...,
description="Cloud provider identifier (aws, gcp, azure).",
)
type: str | None = Field(
default=None,
description=(
"Type of cloud resource (e.g., s3_bucket, ec2_instance, gcs_bucket)."
),
)
region: str | None = Field(
default=None,
description="Cloud provider region (e.g., us-east-1, us-central1, eastus).",
)
account_id: str | None = Field(
default=None,
description="Root account/subscription owner ID.",
)
state: str | None = Field(
default=None,
description="Current lifecycle state of the resource (e.g., running, pending).",
)
@field_validator("provider")
@classmethod
def validate_provider(cls, v: str) -> str:
"""Validate the provider is a known cloud provider.
Args:
v: Provider name to validate.
Outdated
Review

BLOCKER — CORRECTNESS: CLOUD_PROVIDERS referenced before it is defined

CLOUD_PROVIDERS is a module-level constant defined at line 398 (module bottom), but it is referenced here at line 90 inside the validate_provider field validator. Pydantic v2 eagerly evaluates validators during class construction — at the moment Python processes the CloudResource class body, CLOUD_PROVIDERS does not yet exist in the module namespace.

This is a NameError on first instantiation and the root cause of the lint and unit_tests CI failures.

Fix: Move the CLOUD_PROVIDERS constant definition to before the CloudResource class (after the imports, before line 33):

CLOUD_PROVIDERS: frozenset[str] = frozenset({"aws", "gcp", "azure"})

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — CORRECTNESS: `CLOUD_PROVIDERS` referenced before it is defined** `CLOUD_PROVIDERS` is a module-level constant defined at line 398 (module bottom), but it is referenced here at line 90 inside the `validate_provider` field validator. Pydantic v2 eagerly evaluates validators during class construction — at the moment Python processes the `CloudResource` class body, `CLOUD_PROVIDERS` does not yet exist in the module namespace. This is a `NameError` on first instantiation and the root cause of the `lint` and `unit_tests` CI failures. **Fix:** Move the `CLOUD_PROVIDERS` constant definition to before the `CloudResource` class (after the imports, before line 33): ```python CLOUD_PROVIDERS: frozenset[str] = frozenset({"aws", "gcp", "azure"}) ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
Returns:
Lowercased provider name.
Raises:
Outdated
Review

CORRECTNESS BLOCKER: resource_type field missing from CloudResource per issue #8607 acceptance criterion. Add after region field and add a corresponding Behave scenario.

CORRECTNESS BLOCKER: resource_type field missing from CloudResource per issue #8607 acceptance criterion. Add after region field and add a corresponding Behave scenario.
ValueError: If the provider is not recognised.
"""
v_lower = v.lower().strip()
if v_lower not in CLOUD_PROVIDERS:
raise ValueError(
f"Unknown cloud provider '{v}'. "
f"Supported providers: {', '.join(CLOUD_PROVIDERS)}."
)
return v_lower
@field_validator("state")
@classmethod
def validate_state(cls, v: str | None) -> str | None:
Outdated
Review

BLOCKING - Documentation (Category 9): Three public convenience methods lack docstrings — is_aws(), is_gcp(), is_azure(). Add one-line docstrings like: "Check if this resource uses the AWS provider."

BLOCKING - Documentation (Category 9): Three public convenience methods lack docstrings — is_aws(), is_gcp(), is_azure(). Add one-line docstrings like: "Check if this resource uses the AWS provider."
"""Validate state is non-empty if provided.
Args:
v: Resource state to validate.
Returns:
Lowercased state value or None.
"""
if v is not None and not v.strip():
raise ValueError("Resource state cannot be empty (use None instead)")
return v.lower() if v else None
# -- Type-check convenience methods --------------------------------------
def is_aws(self) -> bool:
"""Check if this resource uses the AWS provider."""
return self.provider == "aws"
def is_gcp(self) -> bool:
"""Check if this resource uses the GCP provider."""
return self.provider == "gcp"
def is_azure(self) -> bool:
"""Check if this resource uses the Azure provider."""
return self.provider == "azure"
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"provider={self.provider!r}, "
f"type={self.type!r}, "
f"resource_id={self.resource_id!r}, "
f"region={self.region!r})"
)
class AWSResource(CloudResource):
"""Stub for an AWS (Amazon Web Services) infrastructure resource.
Provider-specific properties are resolved at runtime from
environment variables or explicit ``properties`` on the
:class:`~cleveragents.domain.models.core.resource.Resource`
domain object. This stub stores them as opaque
:attr:`properties` to avoid credential exposure.
**Constraints**:
- Inherited from ``CloudResource``: ``provider`` must be ``"aws"``
- Credentials should be resolved via the handler, not stored here
"""
model_config = ConfigDict(str_strip_whitespace=True)
properties: dict[str, Any] = Field(
default_factory=dict,
description=(
"AWS-specific properties resolved from environment variables "
"or resource configuration. Never set credentials directly "
"on this model."
),
Outdated
Review

BLOCKING - Type Safety (Category 4): GCPResource.validate_region has untyped parameter v but return type is annotated. Must change to: def validate_region(cls, v: str | None) -> str | None
All other validators in this file correctly annotate their parameters.

BLOCKING - Type Safety (Category 4): GCPResource.validate_region has untyped parameter `v` but return type is annotated. Must change to: def validate_region(cls, v: str | None) -> str | None All other validators in this file correctly annotate their parameters.
)
arn: str | None = Field(
default=None,
description="AWS ARN (Amazon Resource Name) for the resource.",
)
tags: dict[str, str] = Field(
default_factory=dict,
description="AWS resource tags (key-value pairs).",
)
@field_validator("arn")
@classmethod
def validate_arn(cls, v: str | None) -> str | None:
"""Validate ARN format if provided.
Args:
v: ARN string to validate.
Returns:
ARN value or None unmodified.
"""
if v is not None and not v.strip():
raise ValueError("ARN cannot be empty (use None instead)")
return v
@field_validator("tags")
@classmethod
def validate_tags(cls, v: dict[str, str]) -> dict[str, str]:
"""Validate that tag keys and values are non-empty.
Args:
v: Tag dictionary to validate.
Returns:
Validated tag dictionary.
Raises:
ValueError: If any tag key or value is empty.
"""
for key, value in v.items():
if not key.strip():
raise ValueError(f"Tag key cannot be empty. Use 'key: {value}'.")
if not value.strip():
raise ValueError(f"Tag value cannot be empty for key '{key}'.")
return v
def __init__(self, **data: Any) -> None:
"""Initialize an AWS resource with the ``aws`` provider.
Sets :attr:`provider` to ``"aws"`` before validation runs.
Args:
**data: Fields to pass to the Pydantic model constructor.
"""
data.setdefault("provider", "aws")
super().__init__(**data)
class GCPResource(CloudResource):
"""Stub for a GCP (Google Cloud Platform) infrastructure resource.
Provider-specific properties are resolved at runtime from
environment variables or explicit ``properties`` on the
:class:`~cleveragents.domain.models.core.resource.Resource`
domain object. This stub stores them as opaque
:attr:`properties` to avoid credential exposure.
**Constraints**:
- Inherited from ``CloudResource``: ``provider`` must be ``"gcp"``
- :attr:`project_id` is required for all GCP resources
"""
model_config = ConfigDict(str_strip_whitespace=True)
properties: dict[str, Any] = Field(
default_factory=dict,
description=(
"GCP-specific properties resolved from environment variables "
"or resource configuration. Credentials are resolved via "
"the handler, not stored here."
),
)
project_id: str = Field(
...,
description="GCP project ID (e.g., my-project-123).",
)
region: str | None = Field(
default=None,
description="GCP region (e.g., us-central1, europe-west1).",
)
@field_validator("project_id")
@classmethod
def validate_project_id(cls, v: str) -> str:
"""Validate project_id is non-empty.
Args:
v: Project ID string.
Returns:
Lowercased, stripped project ID.
"""
v = v.strip()
if not v:
raise ValueError("GCP project_id cannot be empty")
return v.lower()
@field_validator("region")
@classmethod
def validate_region(cls, v: str | None) -> str | None:
"""Validate GCP region is non-empty.
Args:
v: Region string to validate.
Returns:
Lowercased, stripped region or None.
"""
if v is not None:
v = v.strip()
if not v:
raise ValueError("GCP region cannot be empty")
return v.lower()
return None
def __init__(self, **data: Any) -> None:
"""Initialize a GCP resource with the ``gcp`` provider.
Sets :attr:`provider` to ``"gcp"`` before validation runs.
Args:
**data: Fields to pass to the Pydantic model constructor.
"""
data.setdefault("provider", "gcp")
super().__init__(**data)
class AzureResource(CloudResource):
"""Stub for an Azure (Microsoft Azure) infrastructure resource.
Provider-specific properties are resolved at runtime from
environment variables or explicit ``properties`` on the
:class:`~cleveragents.domain.models.core.resource.Resource`
domain object. This stub stores them as opaque
:attr:`properties` to avoid credential exposure.
**Constraints**:
- Inherited from ``CloudResource``: ``provider`` must be ``"azure"``
- :attr:`subscription_id` identifies the Azure subscription
"""
model_config = ConfigDict(str_strip_whitespace=True)
properties: dict[str, Any] = Field(
default_factory=dict,
description=(
"Azure-specific properties resolved from environment variables "
"or resource configuration. Credentials are resolved via "
"the handler, not stored here."
),
)
subscription_id: str = Field(
...,
description="Azure subscription ID (GUID).",
)
tenant_id: str = Field(
...,
description="Azure AD tenant ID (GUID).",
)
region: str | None = Field(
default=None,
description="Azure region (e.g., eastus, westeurope).",
)
@field_validator("subscription_id")
@classmethod
def validate_subscription_id(cls, v: str) -> str:
"""Validate subscription_id is non-empty.
Args:
v: Subscription ID string.
Returns:
Lowercased, stripped subscription ID.
"""
v = v.strip()
if not v:
raise ValueError("Azure subscription_id cannot be empty")
return v.lower()
@field_validator("tenant_id")
@classmethod
def validate_tenant_id(cls, v: str) -> str:
"""Validate tenant_id is non-empty.
Args:
v: Tenant ID string.
Returns:
Lowercased, stripped tenant ID.
"""
v = v.strip()
if not v:
raise ValueError("Azure tenant_id cannot be empty")
return v.lower()
@field_validator("region")
@classmethod
Outdated
Review

BLOCKING - Type Safety (Category 4): AzureResource.validate_region has untyped parameter v but return type is annotated. Must change to: def validate_region(cls, v: str | None) -> str | None
This will cause Pyright strict mode type errors until fixed.

BLOCKING - Type Safety (Category 4): AzureResource.validate_region has untyped parameter `v` but return type is annotated. Must change to: def validate_region(cls, v: str | None) -> str | None This will cause Pyright strict mode type errors until fixed.
def validate_region(cls, v: str | None) -> str | None:
"""Validate Azure region is non-empty.
Args:
v: Region string to validate.
Returns:
Lowercased, stripped region or None.
"""
if v is not None:
v = v.strip()
if not v:
raise ValueError("Azure region cannot be empty")
return v.lower()
return None
def __init__(self, **data: Any) -> None:
"""Initialize an Azure resource with the ``azure`` provider.
Sets :attr:`provider` to ``"azure"`` before validation runs.
Args:
**data: Fields to pass to the Pydantic model constructor.
"""
data.setdefault("provider", "azure")
super().__init__(**data)
Outdated
Review

BLOCKER B — LINT: Unused # noqa: PLW0603 directives cause RUF100 lint failure

All four model_rebuild calls on lines 405–408 carry # noqa: PLW0603. PLW0603 suppresses the "Using global statement" rule — these model_rebuild calls do not use global, so the directive suppresses nothing. Ruff reports:

RUF100: Unused `noqa` directive (non-enabled: `PLW0603`)

on all four lines. This is the direct cause of the lint CI failure.

Fix: Remove all four # noqa: PLW0603 comments. The model_rebuild calls need no suppression:

CloudResource.model_rebuild(_types_namespace={"Any": Any})
AWSResource.model_rebuild(_types_namespace={"Any": Any})
GCPResource.model_rebuild(_types_namespace={"Any": Any})
AzureResource.model_rebuild(_types_namespace={"Any": Any})

Also (same commit): Fix the import order — from typing import Any (stdlib) must precede from pydantic import ... (third-party). Run nox -s format to auto-correct both issues.


Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER B — LINT: Unused `# noqa: PLW0603` directives cause `RUF100` lint failure** All four `model_rebuild` calls on lines 405–408 carry `# noqa: PLW0603`. `PLW0603` suppresses the "Using `global` statement" rule — these `model_rebuild` calls do not use `global`, so the directive suppresses nothing. Ruff reports: ``` RUF100: Unused `noqa` directive (non-enabled: `PLW0603`) ``` on all four lines. This is the direct cause of the `lint` CI failure. **Fix:** Remove all four `# noqa: PLW0603` comments. The `model_rebuild` calls need no suppression: ```python CloudResource.model_rebuild(_types_namespace={"Any": Any}) AWSResource.model_rebuild(_types_namespace={"Any": Any}) GCPResource.model_rebuild(_types_namespace={"Any": Any}) AzureResource.model_rebuild(_types_namespace={"Any": Any}) ``` **Also (same commit):** Fix the import order — `from typing import Any` (stdlib) must precede `from pydantic import ...` (third-party). Run `nox -s format` to auto-correct both issues. --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
Outdated
Review

BLOCKER — LINT: # noqa: PLW0603 triggers RUF100 (unused noqa directive)

PLW0603 is a Pylint rule not present in this project's enabled ruff ruleset (E, F, W, B, UP, I, SIM, RUF). Because the suppressed rule is never checked, ruff fires RUF100 ("Unused noqa directive") on all four of these lines — the root cause of the lint CI failure.

Additionally, the model_rebuild() calls are unnecessary: this module does not use from __future__ import annotations, so Pydantic v2 resolves all types eagerly during class construction and no forward-reference rebuild is needed.

Fix: Remove all four model_rebuild() lines and the comment block above them entirely.


Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — LINT: `# noqa: PLW0603` triggers `RUF100` (unused noqa directive)** `PLW0603` is a Pylint rule not present in this project's enabled ruff ruleset (`E`, `F`, `W`, `B`, `UP`, `I`, `SIM`, `RUF`). Because the suppressed rule is never checked, ruff fires `RUF100` ("Unused `noqa` directive") on all four of these lines — the root cause of the `lint` CI failure. Additionally, the `model_rebuild()` calls are unnecessary: this module does not use `from __future__ import annotations`, so Pydantic v2 resolves all types eagerly during class construction and no forward-reference rebuild is needed. **Fix:** Remove all four `model_rebuild()` lines and the comment block above them entirely. --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
__all__ = [
"CLOUD_PROVIDERS",
"AWSResource",
"AzureResource",
"CloudResource",
"GCPResource",
]