feat: govern connector configurations and simulations
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class ConnectorMappingRule(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
source: str = Field(min_length=1, max_length=255)
|
||||
target: str = Field(min_length=1, max_length=255)
|
||||
required: bool = False
|
||||
default: Any = None
|
||||
|
||||
|
||||
class ConnectorMappingDefinition(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
version: str = Field(min_length=1, max_length=100)
|
||||
rules: list[ConnectorMappingRule] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
class ConnectorValidationRule(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
kind: Literal["required", "one_of", "unique"]
|
||||
field: str = Field(min_length=1, max_length=255)
|
||||
values: list[Any] = Field(default_factory=list, max_length=500)
|
||||
severity: Literal["warning", "error"] = "error"
|
||||
code: str = Field(min_length=1, max_length=120)
|
||||
message: str = Field(min_length=1, max_length=500)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_values(self) -> "ConnectorValidationRule":
|
||||
if self.kind == "one_of" and not self.values:
|
||||
raise ValueError("one_of validation requires allowed values")
|
||||
if self.kind != "one_of" and self.values:
|
||||
raise ValueError("Only one_of validation accepts values")
|
||||
return self
|
||||
|
||||
|
||||
class ConnectorDryRunMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
supported: bool = True
|
||||
simulation_supported: bool = True
|
||||
sample_rows: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
|
||||
max_items: int = Field(default=500, ge=1, le=10_000)
|
||||
redacted_fields: list[str] = Field(default_factory=list, max_length=100)
|
||||
|
||||
|
||||
class ConnectorAuditMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
event_prefix: str = Field(min_length=1, max_length=120)
|
||||
expected_events: list[str] = Field(default_factory=list, max_length=100)
|
||||
evidence_fields: list[str] = Field(default_factory=list, max_length=100)
|
||||
|
||||
|
||||
class GovernedConnectorSpecification(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
provider: str = Field(min_length=1, max_length=120)
|
||||
protocol: str = Field(min_length=1, max_length=80)
|
||||
capabilities: list[str] = Field(min_length=1, max_length=100)
|
||||
input_schema: dict[str, Any]
|
||||
output_schema: dict[str, Any]
|
||||
mapping: ConnectorMappingDefinition
|
||||
validation_rules: list[ConnectorValidationRule] = Field(
|
||||
default_factory=list,
|
||||
max_length=500,
|
||||
)
|
||||
dry_run: ConnectorDryRunMetadata
|
||||
audit: ConnectorAuditMetadata
|
||||
privacy_classification: Literal[
|
||||
"public",
|
||||
"internal",
|
||||
"confidential",
|
||||
"restricted",
|
||||
] = "internal"
|
||||
retention_class: str = Field(min_length=1, max_length=120)
|
||||
operational_limits: dict[str, Any] = Field(default_factory=dict)
|
||||
retry_policy: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ConnectorDefinitionUpsertRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
definition_key: str = Field(pattern=r"^[a-z0-9][a-z0-9._-]{0,159}$")
|
||||
name: str = Field(min_length=1, max_length=300)
|
||||
description: str | None = Field(default=None, max_length=4000)
|
||||
origin: Literal["package", "local"] = "local"
|
||||
package_ref: str | None = Field(default=None, max_length=300)
|
||||
specification: GovernedConnectorSpecification
|
||||
|
||||
@model_validator(mode="after")
|
||||
def package_provenance(self) -> "ConnectorDefinitionUpsertRequest":
|
||||
if self.origin == "package" and not self.package_ref:
|
||||
raise ValueError("Package definitions require package_ref")
|
||||
if self.origin == "local" and self.package_ref:
|
||||
raise ValueError("Local definitions cannot claim package_ref")
|
||||
return self
|
||||
|
||||
|
||||
class ConnectorDefinitionItem(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
definition_key: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
status: str
|
||||
current_revision: int
|
||||
source_package: str | None = None
|
||||
local_definition: bool
|
||||
revision_id: str
|
||||
definition_hash: str
|
||||
origin: str
|
||||
package_ref: str | None = None
|
||||
specification: GovernedConnectorSpecification
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ConnectorDefinitionListResponse(BaseModel):
|
||||
items: list[ConnectorDefinitionItem]
|
||||
|
||||
|
||||
class ConnectorConfigurationCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
definition_id: str = Field(min_length=1, max_length=36)
|
||||
name: str = Field(min_length=1, max_length=300)
|
||||
endpoint_url: str | None = Field(default=None, max_length=1500)
|
||||
credential_ref: str | None = Field(default=None, max_length=500)
|
||||
local_overrides: dict[str, Any] = Field(default_factory=dict)
|
||||
ambiguity_policy: Literal["manual_review", "quarantine", "reject"] = (
|
||||
"manual_review"
|
||||
)
|
||||
status: Literal["draft", "active", "disabled"] = "draft"
|
||||
|
||||
|
||||
class ConnectorConfigurationUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
expected_revision: int = Field(ge=1)
|
||||
name: str | None = Field(default=None, min_length=1, max_length=300)
|
||||
endpoint_url: str | None = Field(default=None, max_length=1500)
|
||||
credential_ref: str | None = Field(default=None, max_length=500)
|
||||
local_overrides: dict[str, Any] | None = None
|
||||
ambiguity_policy: Literal["manual_review", "quarantine", "reject"] | None = None
|
||||
status: Literal["draft", "active", "disabled"] | None = None
|
||||
adopt_latest_definition: bool = False
|
||||
|
||||
|
||||
class ConnectorConfigurationItem(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
definition_id: str
|
||||
definition_key: str
|
||||
definition_name: str
|
||||
name: str
|
||||
status: str
|
||||
endpoint_url: str | None = None
|
||||
credential_ref: str | None = None
|
||||
base_definition_revision: int
|
||||
latest_definition_revision: int
|
||||
update_available: bool
|
||||
local_overrides: dict[str, Any]
|
||||
protected_paths: list[str]
|
||||
effective_configuration: dict[str, Any]
|
||||
effective_hash: str
|
||||
resource_revision: int
|
||||
ambiguity_policy: str
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ConnectorConfigurationListResponse(BaseModel):
|
||||
items: list[ConnectorConfigurationItem]
|
||||
|
||||
|
||||
class ConnectorRunRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
idempotency_key: str = Field(min_length=1, max_length=255)
|
||||
input_rows: list[dict[str, Any]] | None = Field(default=None, max_length=500)
|
||||
external_revision: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class ConnectorRunItem(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
configuration_id: str
|
||||
mode: Literal["dry_run", "simulation"]
|
||||
idempotency_key: str
|
||||
status: str
|
||||
review_state: str
|
||||
definition_revision: int
|
||||
configuration_revision: int
|
||||
configuration_hash: str
|
||||
input_hash: str
|
||||
summary: dict[str, Any]
|
||||
effects: list[dict[str, Any]]
|
||||
diagnostics: list[dict[str, Any]]
|
||||
provenance: dict[str, Any]
|
||||
reviewed_by: str | None = None
|
||||
reviewed_at: datetime | None = None
|
||||
review_reason: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ConnectorRunListResponse(BaseModel):
|
||||
items: list[ConnectorRunItem]
|
||||
|
||||
|
||||
class ConnectorReviewRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
decision: Literal["approved", "rejected"]
|
||||
reason: str = Field(min_length=5, max_length=1000)
|
||||
Reference in New Issue
Block a user