diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..c20b655 --- /dev/null +++ b/agent.py @@ -0,0 +1,1170 @@ +"""Production-safe deterministic customer integration engineering agent. + +This agent guides an integration from requirements through generated workspace +artifacts and sandbox-style acceptance evidence. It deliberately does not call +an LLM, read provider credentials, execute arbitrary shell, deploy, merge, or +mutate customer systems. All customer credentials stay behind caller-managed +secret references. +""" +from __future__ import annotations + +import asyncio +import hashlib +import ipaddress +import json +import re +import socket +import time +from datetime import UTC, datetime +from typing import Any, Literal +from urllib.parse import urlparse + +import a2a_pack as a2a +from a2a_pack import ( + A2AAgent, + ConsumerSetup, + ConsumerSetupField, + EgressPolicy, + NoAuth, + Pricing, + Resources, + RunContext, + WorkspaceAccess, + WorkspaceMode, +) +from a2a_pack.context import AgentEvent +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + + +MAX_JSON_BYTES = 256_000 +MAX_TEXT_BYTES = 128_000 +APPROVAL_PHRASE = "I_APPROVE_THE_LISTED_STEPS" +OUTPUT_ROOT = "outputs/integrations" +SECRET_WORDS = ("secret", "token", "password", "passwd", "apikey", "api_key", "authorization", "bearer") + + +def _tool_input_schema(request_model: type[BaseModel]) -> dict[str, Any]: + return { + "type": "object", + "required": ["request"], + "additionalProperties": False, + "properties": {"request": request_model.model_json_schema()}, + } + + +class StrictModel(BaseModel): + model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) + + +class AuditEntry(StrictModel): + ts: str + action: str + integration_id: str + details: dict[str, Any] = Field(default_factory=dict) + + +class ArtifactRecord(StrictModel): + path: str + sha256: str + size_bytes: int = Field(ge=0) + mime_type: str + + +class SkillStatus(StrictModel): + status: Literal["ok", "needs_setup", "blocked", "failed"] + integration_id: str + dry_run: bool + audit: list[AuditEntry] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + residual_risks: list[str] = Field(default_factory=list) + + +class SystemSpec(StrictModel): + name: str = Field(min_length=1, max_length=80) + kind: Literal[ + "rest_api", + "webhook", + "database", + "object_storage", + "observability", + "gitea", + "kubernetes", + "argo_cd", + "queue", + "other", + ] + base_url: str | None = Field(default=None, max_length=2048) + environment: str = Field(default="sandbox", max_length=64) + owner: str | None = Field(default=None, max_length=120) + notes: str | None = Field(default=None, max_length=2000) + + @field_validator("base_url") + @classmethod + def _safe_url_text(cls, value: str | None) -> str | None: + if value is None or not value: + return None + parsed = urlparse(value) + if parsed.scheme not in {"https", "http"} or not parsed.netloc: + raise ValueError("base_url must be an absolute http(s) URL") + return value + + +class AuthMethod(StrictModel): + system_name: str = Field(min_length=1, max_length=80) + auth_type: Literal["none", "api_key", "bearer_token", "oauth2_client_credentials", "hmac_signature", "mtls", "basic", "oidc"] + secret_ref: str | None = Field( + default=None, + pattern=r"^[A-Z][A-Z0-9_]{2,127}$", + description="Caller-managed secret reference name only; never a raw secret value.", + ) + header_name: str | None = Field(default=None, max_length=80) + token_url: str | None = Field(default=None, max_length=2048) + scopes: list[str] = Field(default_factory=list, max_length=20) + + @model_validator(mode="after") + def _require_secret_ref_for_secret_auth(self) -> "AuthMethod": + if self.auth_type != "none" and not self.secret_ref: + raise ValueError("secret_ref is required for non-none auth methods") + _reject_raw_secret(self.secret_ref or "") + return self + + +class FieldMapping(StrictModel): + source: str = Field(min_length=1, max_length=160) + target: str = Field(min_length=1, max_length=160) + transform: str = Field(default="copy", max_length=500) + required: bool = True + pii: bool = False + + +class DataContract(StrictModel): + name: str = Field(min_length=1, max_length=120) + direction: Literal["inbound", "outbound", "bidirectional"] + content_type: str = Field(default="application/json", max_length=120) + schema_ref: str | None = Field(default=None, max_length=500) + fields: list[FieldMapping] = Field(default_factory=list, max_length=200) + example_payload: dict[str, Any] | None = None + + @field_validator("example_payload") + @classmethod + def _bounded_example(cls, value: dict[str, Any] | None) -> dict[str, Any] | None: + if value is not None: + _ensure_bounded_json(value, MAX_JSON_BYTES) + return value + + +class RateLimitSpec(StrictModel): + system_name: str = Field(min_length=1, max_length=80) + max_requests: int = Field(ge=1, le=1_000_000) + per_seconds: int = Field(ge=1, le=86_400) + burst: int | None = Field(default=None, ge=1, le=1_000_000) + retry_after_header: str | None = Field(default="Retry-After", max_length=80) + + +class EnvironmentSpec(StrictModel): + name: str = Field(min_length=1, max_length=64) + purpose: Literal["dev", "test", "sandbox", "staging", "prod"] + endpoint_refs: list[str] = Field(default_factory=list, max_length=20) + object_storage_ref: str | None = Field(default=None, max_length=128) + observability_ref: str | None = Field(default=None, max_length=128) + mutable: bool = False + + +class ComplianceConstraint(StrictModel): + name: str = Field(min_length=1, max_length=120) + category: Literal["pii", "phi", "pci", "soc2", "gdpr", "hipaa", "data_residency", "retention", "audit", "other"] + requirement: str = Field(min_length=1, max_length=2000) + + +class SuccessCriterion(StrictModel): + name: str = Field(min_length=1, max_length=120) + measurement: str = Field(min_length=1, max_length=500) + target: str = Field(min_length=1, max_length=200) + + +class IntegrationRequirements(StrictModel): + integration_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,63}$") + title: str = Field(min_length=1, max_length=160) + systems: list[SystemSpec] = Field(min_length=1, max_length=12) + auth_methods: list[AuthMethod] = Field(default_factory=list, max_length=12) + data_contracts: list[DataContract] = Field(default_factory=list, max_length=20) + rate_limits: list[RateLimitSpec] = Field(default_factory=list, max_length=20) + environments: list[EnvironmentSpec] = Field(default_factory=list, max_length=12) + compliance_constraints: list[ComplianceConstraint] = Field(default_factory=list, max_length=30) + success_criteria: list[SuccessCriterion] = Field(min_length=1, max_length=20) + external_host_allowlist: list[str] = Field(default_factory=list, max_length=50) + repository_write_allowlist: list[str] = Field(default_factory=list, max_length=20) + notes: str | None = Field(default=None, max_length=4000) + dry_run: bool = True + + @field_validator("external_host_allowlist") + @classmethod + def _clean_hosts(cls, value: list[str]) -> list[str]: + return [_validate_hostname(host) for host in value] + + @field_validator("repository_write_allowlist") + @classmethod + def _clean_repos(cls, value: list[str]) -> list[str]: + out: list[str] = [] + for repo in value: + clean = repo.strip() + if not re.fullmatch(r"[A-Za-z0-9_.-]{1,80}/[A-Za-z0-9_.-]{1,120}", clean): + raise ValueError("repository allowlist entries must look like owner/repo") + out.append(clean) + return out + + +class DesignIntegrationRequest(StrictModel): + requirements: IntegrationRequirements + create_artifacts: bool = True + dry_run: bool = True + + +class DesignIntegrationResult(SkillStatus): + architecture_path: str | None = None + mapping_path: str | None = None + plan_digest: str + artifacts: list[ArtifactRecord] = Field(default_factory=list) + architecture_summary: str + field_mapping_count: int = Field(ge=0) + required_setup: list[str] = Field(default_factory=list) + + +class GenerateConnectorRequest(StrictModel): + requirements: IntegrationRequirements + connector_language: Literal["python", "typescript"] = "python" + include_tests: bool = True + dry_run: bool = True + external_host_allowlist: list[str] = Field(default_factory=list, max_length=50) + repository_write_allowlist: list[str] = Field(default_factory=list, max_length=20) + + @field_validator("external_host_allowlist") + @classmethod + def _clean_hosts(cls, value: list[str]) -> list[str]: + return [_validate_hostname(host) for host in value] + + +class GenerateConnectorResult(SkillStatus): + connector_path: str | None = None + test_path: str | None = None + plan_digest: str + artifacts: list[ArtifactRecord] = Field(default_factory=list) + generated_files: list[str] = Field(default_factory=list) + blocked_actions: list[str] = Field(default_factory=list) + + +class ContractFixture(StrictModel): + name: str = Field(min_length=1, max_length=120) + payload: dict[str, Any] + should_pass: bool = True + + @field_validator("payload") + @classmethod + def _bounded_payload(cls, value: dict[str, Any]) -> Any: + _ensure_bounded_json(value, MAX_JSON_BYTES) + return value + + +class ContractArtifact(StrictModel): + name: str = Field(min_length=1, max_length=120) + kind: Literal["json_schema", "openapi_3", "webhook"] + schema_doc: dict[str, Any] + fixtures: list[ContractFixture] = Field(default_factory=list, max_length=50) + signature_header: str | None = Field(default=None, max_length=120) + secret_ref: str | None = Field(default=None, pattern=r"^[A-Z][A-Z0-9_]{2,127}$") + + @field_validator("schema_doc") + @classmethod + def _bounded_schema(cls, value: dict[str, Any]) -> Any: + _ensure_bounded_json(value, MAX_JSON_BYTES) + return value + + +class ValidateContractRequest(StrictModel): + integration_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,63}$") + contracts: list[ContractArtifact] = Field(min_length=1, max_length=20) + fail_on_drift: bool = True + dry_run: bool = True + + +class ContractCheck(StrictModel): + name: str + kind: str + status: Literal["pass", "fail", "warn"] + findings: list[str] = Field(default_factory=list) + fixture_results: list[dict[str, Any]] = Field(default_factory=list) + + +class ValidateContractResult(SkillStatus): + contract_report_path: str | None = None + artifacts: list[ArtifactRecord] = Field(default_factory=list) + checks: list[ContractCheck] = Field(default_factory=list) + drift_detected: bool = False + + +class EndpointTest(StrictModel): + name: str = Field(min_length=1, max_length=120) + method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = "POST" + url: str = Field(max_length=2048) + expected_status: int = Field(default=200, ge=100, le=599) + request_json: dict[str, Any] | None = None + required_secret_refs: list[str] = Field(default_factory=list, max_length=10) + idempotency_key: str | None = Field(default=None, max_length=160) + + @field_validator("request_json") + @classmethod + def _bounded_request(cls, value: dict[str, Any] | None) -> dict[str, Any] | None: + if value is not None: + _ensure_bounded_json(value, MAX_JSON_BYTES) + return value + + @field_validator("required_secret_refs") + @classmethod + def _secret_refs_only(cls, value: list[str]) -> list[str]: + for ref in value: + if not re.fullmatch(r"[A-Z][A-Z0-9_]{2,127}", ref): + raise ValueError("secret refs must be uppercase reference names") + _reject_raw_secret(ref) + return value + + +class RunAcceptanceRequest(StrictModel): + integration_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,63}$") + endpoint_tests: list[EndpointTest] = Field(default_factory=list, max_length=20) + contract_fixtures: list[ContractArtifact] = Field(default_factory=list, max_length=20) + external_host_allowlist: list[str] = Field(default_factory=list, max_length=50) + allow_network: bool = False + max_retries: int = Field(default=1, ge=0, le=3) + timeout_seconds: float = Field(default=8.0, ge=1.0, le=30.0) + dry_run: bool = True + + @field_validator("external_host_allowlist") + @classmethod + def _clean_hosts(cls, value: list[str]) -> list[str]: + return [_validate_hostname(host) for host in value] + + +class AcceptanceStepResult(StrictModel): + name: str + status: Literal["pass", "fail", "skipped", "blocked"] + attempts: int = Field(ge=0) + detail: str + idempotency_key: str | None = None + + +class RunAcceptanceResult(SkillStatus): + acceptance_report_path: str | None = None + artifacts: list[ArtifactRecord] = Field(default_factory=list) + step_results: list[AcceptanceStepResult] = Field(default_factory=list) + contract_results: list[ContractCheck] = Field(default_factory=list) + network_calls_made: int = Field(ge=0) + + +class HandoffSection(StrictModel): + heading: str = Field(min_length=1, max_length=120) + body: str = Field(min_length=1, max_length=8000) + + @field_validator("body") + @classmethod + def _redact_body(cls, value: str) -> str: + return redact_secrets(value) + + +class PrepareHandoffRequest(StrictModel): + integration_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,63}$") + title: str = Field(min_length=1, max_length=160) + rollout_steps: list[str] = Field(min_length=1, max_length=30) + rollback_steps: list[str] = Field(min_length=1, max_length=30) + monitoring_checks: list[str] = Field(min_length=1, max_length=30) + operator_notes: list[HandoffSection] = Field(default_factory=list, max_length=20) + delivery_actions: list[str] = Field(default_factory=list, max_length=20) + plan_digest: str | None = Field(default=None, max_length=128) + approval_phrase: str | None = Field(default=None, max_length=64) + approve_delivery: bool = False + dry_run: bool = True + + +class PrepareHandoffResult(SkillStatus): + handoff_path: str | None = None + plan_digest: str + artifacts: list[ArtifactRecord] = Field(default_factory=list) + approval_required: bool + approval_valid: bool + delivery_executed: bool = False + + +class CustomerIntegrationEngineerConfig(StrictModel): + default_dry_run: bool = True + + +_DESIGN_OUTPUT_SCHEMA = DesignIntegrationResult.model_json_schema() +_GENERATE_OUTPUT_SCHEMA = GenerateConnectorResult.model_json_schema() +_VALIDATE_OUTPUT_SCHEMA = ValidateContractResult.model_json_schema() +_ACCEPTANCE_OUTPUT_SCHEMA = RunAcceptanceResult.model_json_schema() +_HANDOFF_OUTPUT_SCHEMA = PrepareHandoffResult.model_json_schema() + + +class CustomerIntegrationEngineer(A2AAgent[CustomerIntegrationEngineerConfig, NoAuth]): + name = "customer-integration-engineer" + description = ( + "Guides and validates customer integrations from requirements through a tested handoff while " + "keeping credentials and production changes under customer control." + ) + version = "0.1.4" + + config_model = CustomerIntegrationEngineerConfig + auth_model = NoAuth + pricing = Pricing( + price_per_call_usd=0.0, + caller_pays_llm=False, + notes="Deterministic no-LLM integration engineering; callers keep credentials and production changes under their control.", + ) + resources = Resources(cpu="500m", memory="512Mi", max_runtime_seconds=600) + egress = EgressPolicy(deny_internet_by_default=True) + workspace_access = WorkspaceAccess.dynamic( + max_files=256, + allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), + require_reason=True, + deny_patterns=("**/.env", "**/*secret*", "**/*token*", "**/id_rsa", "**/.git/**"), + max_total_size_bytes=20 * 1024 * 1024, + ) + tools_used = ("pydantic", "jsonschema", "httpx") + consumer_setup = ConsumerSetup.from_fields( + ConsumerSetupField.config("GITEA_REPOSITORY", label="Gitea repository", description="Optional owner/repo target controlled by the caller.", required=False), + ConsumerSetupField.secret("GITEA_TOKEN", label="Gitea token", description="Optional caller-managed token reference; never returned or stored.", required=False), + ConsumerSetupField.config("API_BASE_URL", label="API base URL", description="Optional endpoint used only when allowlisted and dry_run=false.", required=False, input_type="url"), + ConsumerSetupField.secret("API_AUTH_TOKEN", label="API auth token", description="Optional caller-managed API credential reference.", required=False), + ConsumerSetupField.config("OBJECT_STORAGE_REF", label="Object storage reference", description="Caller-owned object storage config reference.", required=False), + ConsumerSetupField.config("OBSERVABILITY_REF", label="Observability reference", description="Caller-owned logs/metrics/traces reference.", required=False), + ConsumerSetupField.config("KUBERNETES_CONTEXT_REF", label="Kubernetes context reference", description="Optional caller-owned Kubernetes context reference.", required=False), + ConsumerSetupField.config("ARGOCD_APP_REF", label="Argo CD app reference", description="Optional caller-owned Argo CD app reference.", required=False), + ) + + @a2a.tool(description="Capture requirements, produce architecture and field mapping artifacts", input_schema=_tool_input_schema(DesignIntegrationRequest), timeout_seconds=120, idempotent=True, max_retries=0, cost_class="deterministic") + async def design_integration(self, ctx: RunContext[NoAuth], request: DesignIntegrationRequest) -> DesignIntegrationResult: + req = request.requirements.model_copy( + update={"dry_run": _effective_dry_run(self, request.dry_run, request.requirements.dry_run)} + ) + audit = [_audit("design_started", req.integration_id, {"dry_run": req.dry_run})] + await _emit(ctx, audit[-1]) + warnings = _requirements_warnings(req) + required_setup = _required_setup(req) + digest = _plan_digest({"phase": "design", "requirements": req.model_dump(mode="json")}) + architecture = _render_architecture(req, digest, warnings, required_setup) + mapping = _build_mapping(req) + artifacts: list[ArtifactRecord] = [] + arch_path = mapping_path = None + if request.create_artifacts: + base = _integration_base(req.integration_id) + arch_path = f"{base}/architecture.md" + mapping_path = f"{base}/mapping.json" + artifacts.append(await _write_workspace_text(ctx, arch_path, architecture, "text/markdown")) + artifacts.append(await _write_workspace_json(ctx, mapping_path, mapping)) + audit.append(_audit("design_completed", req.integration_id, {"artifacts": [a.path for a in artifacts]})) + await _emit(ctx, audit[-1]) + return DesignIntegrationResult( + status="ok", + integration_id=req.integration_id, + dry_run=req.dry_run, + audit=audit, + warnings=warnings, + residual_risks=_residual_risks(req), + architecture_path=arch_path, + mapping_path=mapping_path, + plan_digest=digest, + artifacts=artifacts, + architecture_summary=f"{req.title}: {len(req.systems)} systems, {len(req.data_contracts)} contracts, {len(req.success_criteria)} success criteria.", + field_mapping_count=sum(len(c.fields) for c in req.data_contracts), + required_setup=required_setup, + ) + + @a2a.tool(description="Generate a minimal workspace-contained connector scaffold with no deployment or repository mutation", input_schema=_tool_input_schema(GenerateConnectorRequest), timeout_seconds=120, idempotent=True, max_retries=0, cost_class="deterministic") + async def generate_connector(self, ctx: RunContext[NoAuth], request: GenerateConnectorRequest) -> GenerateConnectorResult: + req = request.requirements.model_copy( + update={"dry_run": _effective_dry_run(self, request.dry_run, request.requirements.dry_run)} + ) + audit = [_audit("connector_generation_started", req.integration_id, {"language": request.connector_language, "dry_run": req.dry_run})] + await _emit(ctx, audit[-1]) + warnings = _requirements_warnings(req) + blocked_actions: list[str] = [] + if request.repository_write_allowlist or req.repository_write_allowlist: + blocked_actions.append("repository writes are never performed; generated source is written only under outputs/integrations/{integration_id}/") + allowlist = sorted(set(req.external_host_allowlist + request.external_host_allowlist)) + digest = _plan_digest({"phase": "generate_connector", "requirements": req.model_dump(mode="json"), "language": request.connector_language}) + base = _integration_base(req.integration_id) + artifacts: list[ArtifactRecord] = [] + generated_files: list[str] = [] + if request.connector_language == "python": + connector_path = f"{base}/connector/python/connector.py" + test_path = f"{base}/connector/python/test_connector.py" if request.include_tests else None + source = _python_connector(req, allowlist, digest) + artifacts.append(await _write_workspace_text(ctx, connector_path, source, "text/x-python")) + generated_files.append(connector_path) + if test_path: + test_source = _python_connector_test(req) + artifacts.append(await _write_workspace_text(ctx, test_path, test_source, "text/x-python")) + generated_files.append(test_path) + else: + connector_path = f"{base}/connector/typescript/connector.ts" + test_path = f"{base}/connector/typescript/connector.test.ts" if request.include_tests else None + source = _typescript_connector(req, allowlist, digest) + artifacts.append(await _write_workspace_text(ctx, connector_path, source, "text/typescript")) + generated_files.append(connector_path) + if test_path: + test_source = _typescript_connector_test(req) + artifacts.append(await _write_workspace_text(ctx, test_path, test_source, "text/typescript")) + generated_files.append(test_path) + audit.append(_audit("connector_generation_completed", req.integration_id, {"files": generated_files, "blocked_actions": blocked_actions})) + await _emit(ctx, audit[-1]) + status: Literal["ok", "needs_setup", "blocked", "failed"] = "blocked" if blocked_actions and not req.dry_run else "ok" + return GenerateConnectorResult( + status=status, + integration_id=req.integration_id, + dry_run=req.dry_run, + audit=audit, + warnings=warnings, + residual_risks=_residual_risks(req), + connector_path=connector_path, + test_path=test_path, + plan_digest=digest, + artifacts=artifacts, + generated_files=generated_files, + blocked_actions=blocked_actions, + ) + + @a2a.tool(description="Validate OpenAPI, JSON Schema, and webhook contracts with deterministic fixtures", input_schema=_tool_input_schema(ValidateContractRequest), timeout_seconds=120, idempotent=True, max_retries=0, cost_class="deterministic") + async def validate_contract(self, ctx: RunContext[NoAuth], request: ValidateContractRequest) -> ValidateContractResult: + dry_run = _effective_dry_run(self, request.dry_run) + audit = [_audit("contract_validation_started", request.integration_id, {"contracts": len(request.contracts), "dry_run": dry_run})] + await _emit(ctx, audit[-1]) + checks = [_validate_one_contract(contract) for contract in request.contracts] + drift = any(check.status == "fail" for check in checks) + report = { + "integration_id": request.integration_id, + "status": "fail" if drift and request.fail_on_drift else "pass", + "dry_run": dry_run, + "generated_at": _now(), + "checks": [c.model_dump(mode="json") for c in checks], + "secret_handling": "secret_ref values only; raw credentials are rejected and redacted", + } + path = f"{_integration_base(request.integration_id)}/contract-report.json" + artifact = await _write_workspace_json(ctx, path, report) + audit.append(_audit("contract_validation_completed", request.integration_id, {"drift_detected": drift, "report": path})) + await _emit(ctx, audit[-1]) + return ValidateContractResult( + status="failed" if drift and request.fail_on_drift else "ok", + integration_id=request.integration_id, + dry_run=dry_run, + audit=audit, + warnings=[] if not drift else ["contract drift detected; inspect contract-report.json before handoff"], + residual_risks=[] if not drift else ["fixtures are deterministic and bounded; live provider behavior still requires caller-controlled staging verification"], + contract_report_path=path, + artifacts=[artifact], + checks=checks, + drift_detected=drift, + ) + + @a2a.tool(description="Run dry-run or allowlisted acceptance checks using endpoint metadata and secret references", input_schema=_tool_input_schema(RunAcceptanceRequest), timeout_seconds=180, idempotent=True, max_retries=0, cost_class="deterministic") + async def run_acceptance(self, ctx: RunContext[NoAuth], request: RunAcceptanceRequest) -> RunAcceptanceResult: + dry_run = _effective_dry_run(self, request.dry_run) + live_network_block = _live_network_unavailable_reason(self) + audit = [_audit("acceptance_started", request.integration_id, {"endpoint_tests": len(request.endpoint_tests), "dry_run": dry_run, "allow_network": request.allow_network})] + await _emit(ctx, audit[-1]) + contract_results = [_validate_one_contract(contract) for contract in request.contract_fixtures] + step_results: list[AcceptanceStepResult] = [] + network_calls = 0 + warnings: list[str] = [] + for test in request.endpoint_tests: + idem = test.idempotency_key or _plan_digest({"integration_id": request.integration_id, "test": test.model_dump(mode="json")})[:32] + if dry_run or not request.allow_network: + validation = _validate_endpoint_metadata(test.url, request.external_host_allowlist) + if validation: + step_results.append(AcceptanceStepResult(name=test.name, status="blocked", attempts=0, detail=validation, idempotency_key=idem)) + warnings.append(validation) + else: + step_results.append(AcceptanceStepResult(name=test.name, status="skipped", attempts=0, detail="dry_run/default safe mode: endpoint metadata was validated but no DNS lookup or external request was sent", idempotency_key=idem)) + continue + metadata_validation = _validate_endpoint_metadata(test.url, request.external_host_allowlist) + if metadata_validation: + step_results.append(AcceptanceStepResult(name=test.name, status="blocked", attempts=0, detail=metadata_validation, idempotency_key=idem)) + warnings.append(metadata_validation) + continue + if live_network_block: + step_results.append(AcceptanceStepResult(name=test.name, status="blocked", attempts=0, detail=live_network_block, idempotency_key=idem)) + warnings.append(live_network_block) + continue + validation = await _validate_endpoint_target(test.url, request.external_host_allowlist) + if validation: + step_results.append(AcceptanceStepResult(name=test.name, status="blocked", attempts=0, detail=validation, idempotency_key=idem)) + warnings.append(validation) + continue + result = await _call_endpoint_with_retries(test, request.max_retries, request.timeout_seconds, idem) + network_calls += result.attempts + step_results.append(result) + report = { + "integration_id": request.integration_id, + "dry_run": dry_run, + "allow_network": request.allow_network, + "network_calls_made": network_calls, + "generated_at": _now(), + "endpoint_results": [s.model_dump(mode="json") for s in step_results], + "contract_results": [c.model_dump(mode="json") for c in contract_results], + "safety": { + "ssrf_protection": "scheme, host allowlist, DNS/IP public-routability, and redirect revalidation enforced", + "secrets": "only secret references accepted; no raw values stored", + "payload_limits_bytes": MAX_JSON_BYTES, + "timeouts_seconds": request.timeout_seconds, + }, + } + path = f"{_integration_base(request.integration_id)}/acceptance-report.json" + artifact = await _write_workspace_json(ctx, path, report) + failed = any(s.status in {"fail", "blocked"} for s in step_results) or any(c.status == "fail" for c in contract_results) + audit.append(_audit("acceptance_completed", request.integration_id, {"report": path, "network_calls_made": network_calls, "failed_or_blocked": failed})) + await _emit(ctx, audit[-1]) + return RunAcceptanceResult( + status="blocked" if any(s.status == "blocked" for s in step_results) else "failed" if failed else "ok", + integration_id=request.integration_id, + dry_run=dry_run, + audit=audit, + warnings=sorted(set(warnings)), + residual_risks=["No production deployment, merge, or environment mutation is performed by this agent."], + acceptance_report_path=path, + artifacts=[artifact], + step_results=step_results, + contract_results=contract_results, + network_calls_made=network_calls, + ) + + @a2a.tool(description="Prepare rollout, rollback, monitoring, and operator handoff documentation with approval-gated delivery planning", input_schema=_tool_input_schema(PrepareHandoffRequest), timeout_seconds=120, idempotent=True, max_retries=0, cost_class="deterministic") + async def prepare_handoff(self, ctx: RunContext[NoAuth], request: PrepareHandoffRequest) -> PrepareHandoffResult: + dry_run = _effective_dry_run(self, request.dry_run) + audit = [_audit("handoff_started", request.integration_id, {"dry_run": dry_run, "delivery_actions": len(request.delivery_actions)})] + await _emit(ctx, audit[-1]) + plan_material = { + "integration_id": request.integration_id, + "rollout_steps": request.rollout_steps, + "rollback_steps": request.rollback_steps, + "monitoring_checks": request.monitoring_checks, + "delivery_actions": request.delivery_actions, + } + digest = _plan_digest(plan_material) + approval_required = bool(request.delivery_actions) + approval_valid = (not approval_required) or ( + request.approve_delivery + and request.approval_phrase == APPROVAL_PHRASE + and request.plan_digest == digest + ) + warnings: list[str] = [] + if approval_required and not approval_valid: + warnings.append(f"delivery actions require approval_phrase={APPROVAL_PHRASE!r} and plan_digest={digest}") + if approval_required and approval_valid: + warnings.append("approval validated for the listed plan, but this agent still does not deploy, merge, or modify customer systems") + handoff_request = request.model_copy(update={"dry_run": dry_run}) + handoff = _render_handoff(handoff_request, digest, approval_required, approval_valid) + path = f"{_integration_base(request.integration_id)}/handoff.md" + artifact = await _write_workspace_text(ctx, path, handoff, "text/markdown") + audit.append(_audit("handoff_completed", request.integration_id, {"handoff": path, "approval_required": approval_required, "approval_valid": approval_valid})) + await _emit(ctx, audit[-1]) + return PrepareHandoffResult( + status="blocked" if approval_required and not approval_valid else "ok", + integration_id=request.integration_id, + dry_run=dry_run, + audit=audit, + warnings=warnings, + residual_risks=["Operators must execute rollout or rollback in caller-controlled systems outside this agent."], + handoff_path=path, + plan_digest=digest, + artifacts=[artifact], + approval_required=approval_required, + approval_valid=approval_valid, + delivery_executed=False, + ) + + +def _now() -> str: + return datetime.now(UTC).isoformat() + + +def _effective_dry_run(agent: CustomerIntegrationEngineer, *values: bool) -> bool: + return bool(agent.config.default_dry_run or any(values)) + + +def _live_network_unavailable_reason(agent: CustomerIntegrationEngineer) -> str | None: + if getattr(agent.egress, "deny_internet_by_default", False): + return ( + "live network checks are disabled by this runtime because egress denies internet by default; " + "acceptance can validate metadata in dry_run mode but cannot contact external endpoints" + ) + return None + + +def _audit(action: str, integration_id: str, details: dict[str, Any] | None = None) -> AuditEntry: + return AuditEntry(ts=_now(), action=action, integration_id=integration_id, details=redact_obj(details or {})) + + +async def _emit(ctx: RunContext[NoAuth], entry: AuditEntry) -> None: + await ctx.emit_event(AgentEvent(kind="audit", payload=entry.model_dump(mode="json"))) + await ctx.emit_progress(f"{entry.action}: {entry.integration_id}") + + +def _integration_base(integration_id: str) -> str: + if not re.fullmatch(r"[a-z][a-z0-9-]{2,63}", integration_id): + raise ValueError("invalid integration_id") + return f"{OUTPUT_ROOT}/{integration_id}" + + +def _safe_output_path(path: str) -> str: + clean = path.replace("\\", "/").strip("/") + if not clean.startswith(f"{OUTPUT_ROOT}/"): + raise ValueError("writes must stay under outputs/integrations/{integration_id}/") + if ".." in clean.split("/") or clean.startswith("/"): + raise ValueError("unsafe output path") + return clean + + +async def _write_workspace_text(ctx: RunContext[NoAuth], path: str, text: str, mime_type: str) -> ArtifactRecord: + clean = _safe_output_path(path) + data = redact_secrets(text).encode("utf-8") + if len(data) > MAX_TEXT_BYTES * 4: + raise ValueError("artifact exceeds bounded text size") + ws = ctx.workspace + if hasattr(ws, "is_writable_output") and ws.is_writable_output(clean): # type: ignore[attr-defined] + if hasattr(ws, "write_bytes"): + ws.write_bytes(clean, data) # type: ignore[attr-defined] + else: + view = await ws.open_view(purpose=f"write {clean}", hints=[clean], max_files=1, mode=WorkspaceMode.READ_WRITE_OVERLAY, reason="Persist integration engineering output under outputs/integrations") + await view.write(clean, data) + else: + raise PermissionError("workspace grant does not allow writes under outputs/integrations") + artifact_name = clean.replace("/", "__") + ref = await ctx.write_artifact(artifact_name, data, mime_type) + await ctx.emit_artifact(ref) + return ArtifactRecord(path=clean, sha256=hashlib.sha256(data).hexdigest(), size_bytes=len(data), mime_type=mime_type) + + +async def _write_workspace_json(ctx: RunContext[NoAuth], path: str, payload: dict[str, Any]) -> ArtifactRecord: + safe_payload = redact_obj(payload) + _ensure_bounded_json(safe_payload, MAX_JSON_BYTES) + return await _write_workspace_text(ctx, path, json.dumps(safe_payload, indent=2, sort_keys=True) + "\n", "application/json") + + +def _ensure_bounded_json(value: Any, max_bytes: int) -> None: + data = json.dumps(value, default=str, sort_keys=True).encode("utf-8") + if len(data) > max_bytes: + raise ValueError(f"JSON payload exceeds {max_bytes} bytes") + + +def _reject_raw_secret(value: str) -> None: + if not value: + return + if any(marker in value.lower() for marker in ("sk-", "ghp_", "xoxb-", "-----begin", "bearer ")): + raise ValueError("raw credential-looking values are not allowed; use a secret_ref") + + +def redact_secrets(text: str) -> str: + redacted = text + patterns = [ + r"(?i)(authorization\s*[:=]\s*bearer\s+)[A-Za-z0-9._~+/=-]+", + r"(?i)(api[_-]?key\s*[:=]\s*)[A-Za-z0-9._~+/=-]{8,}", + r"(?i)(token\s*[:=]\s*)[A-Za-z0-9._~+/=-]{8,}", + r"(?i)(password\s*[:=]\s*)[^\s,;]+", + r"sk-[A-Za-z0-9]{12,}", + r"ghp_[A-Za-z0-9]{12,}", + ] + for pattern in patterns: + redacted = re.sub(pattern, _redact_match, redacted) + return redacted + + +def _redact_match(match: re.Match[str]) -> str: + return (match.group(1) if match.lastindex else "") + "[REDACTED]" + + +def _jsonschema_error_path(error: Any) -> Any: + return error.path + + +def redact_obj(value: Any) -> Any: + if isinstance(value, dict): + out: dict[str, Any] = {} + for key, item in value.items(): + if any(word in str(key).lower() for word in SECRET_WORDS): + out[key] = "[REDACTED_REF]" if isinstance(item, str) else "[REDACTED]" + else: + out[key] = redact_obj(item) + return out + if isinstance(value, list): + return [redact_obj(item) for item in value] + if isinstance(value, str): + return redact_secrets(value) + return value + + +def _plan_digest(payload: dict[str, Any]) -> str: + safe = redact_obj(payload) + encoded = json.dumps(safe, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def _requirements_warnings(req: IntegrationRequirements) -> list[str]: + warnings: list[str] = [] + if not req.auth_methods: + warnings.append("no auth methods supplied; confirm this is intentional") + if not req.data_contracts: + warnings.append("no data contracts supplied; mapping and validation will be limited") + if not req.rate_limits: + warnings.append("no rate limits supplied; connector will use conservative defaults") + if any(env.purpose == "prod" and env.mutable for env in req.environments): + warnings.append("production environment marked mutable; this agent will still not mutate it") + return warnings + + +def _residual_risks(req: IntegrationRequirements) -> list[str]: + risks = [ + "Generated connector is minimal and must be reviewed by customer operators before production use.", + "Provider-specific authentication and observability wiring stay under caller-controlled setup references.", + ] + if req.dry_run: + risks.append("dry_run=true: acceptance evidence is deterministic and non-mutating, not proof of live provider behavior.") + return risks + + +def _required_setup(req: IntegrationRequirements) -> list[str]: + setup = {auth.secret_ref for auth in req.auth_methods if auth.secret_ref} + for env in req.environments: + for ref in (env.object_storage_ref, env.observability_ref): + if ref: + setup.add(ref) + return sorted(setup) + + +def _build_mapping(req: IntegrationRequirements) -> Any: + return { + "integration_id": req.integration_id, + "title": req.title, + "contracts": [ + { + "name": c.name, + "direction": c.direction, + "content_type": c.content_type, + "schema_ref": c.schema_ref, + "fields": [f.model_dump(mode="json") for f in c.fields], + } + for c in req.data_contracts + ], + "pii_fields": [f"{c.name}.{field.target}" for c in req.data_contracts for field in c.fields if field.pii], + } + + +def _render_architecture(req: IntegrationRequirements, digest: str, warnings: list[str], setup: list[str]) -> str: + systems = "\n".join(f"- **{s.name}** ({s.kind}, {s.environment}) {s.base_url or '(endpoint configured by caller)'}" for s in req.systems) + auth = "\n".join(f"- {a.system_name}: {a.auth_type}, secret_ref={a.secret_ref or 'none'}" for a in req.auth_methods) or "- No auth methods supplied." + contracts = "\n".join(f"- {c.name}: {c.direction}, {len(c.fields)} mapped fields, schema_ref={c.schema_ref or 'inline/fixture'}" for c in req.data_contracts) or "- No contracts supplied." + rates = "\n".join(f"- {r.system_name}: {r.max_requests}/{r.per_seconds}s burst={r.burst or r.max_requests}" for r in req.rate_limits) or "- Conservative default: bounded retries with idempotency keys." + compliance = "\n".join(f"- {c.category}: {c.name} — {c.requirement}" for c in req.compliance_constraints) or "- No explicit compliance constraints supplied." + success = "\n".join(f"- {s.name}: {s.measurement} target={s.target}" for s in req.success_criteria) + warning_text = "\n".join(f"- {w}" for w in warnings) or "- None" + setup_text = "\n".join(f"- {s}" for s in setup) or "- None" + return f"""# Integration Architecture: {req.title} + +Integration ID: `{req.integration_id}` +Plan digest: `{digest}` +Dry run: `{req.dry_run}` + +## Systems +{systems} + +## Authentication and Secret References +{auth} + +Raw credentials are neither accepted nor stored. Operators configure referenced secrets outside this agent. + +## Data Contracts and Field Mapping +{contracts} + +See `mapping.json` for exact field-level mapping. + +## Rate Limits and Retries +{rates} + +All mutating calls must carry idempotency keys. Retries are bounded and honor provider retry metadata. + +## Environments +{chr(10).join(f"- {e.name}: {e.purpose}, mutable={e.mutable}" for e in req.environments) or '- No environments supplied.'} + +## Compliance Constraints +{compliance} + +## Success Criteria +{success} + +## Required Caller Setup +{setup_text} + +## Safety Controls +- Default dry_run=true. +- External requests require explicit host allowlist, DNS/IP validation, redirect revalidation, payload limits, and timeouts. +- Repository writes beyond outputs, deployments, merges, and environment mutations are not performed. +- Delivery actions require `{APPROVAL_PHRASE}` bound to a plan digest and still remain operator-executed outside this agent. + +## Warnings +{warning_text} +""" + + +def _python_connector(req: IntegrationRequirements, allowlist: list[str], digest: str) -> str: + return f'''"""Minimal connector scaffold for {req.integration_id}. +Generated by customer-integration-engineer. Review before use. +Plan digest: {digest} +""" +from __future__ import annotations + +import hashlib +import ipaddress +import json +import socket +from urllib.parse import urlparse + +import httpx + +ALLOWED_HOSTS = {allowlist!r} +MAX_BYTES = {MAX_JSON_BYTES} +TIMEOUT_SECONDS = 8.0 + + +def validate_url(url: str) -> str: + parsed = urlparse(url) + if parsed.scheme != "https" or not parsed.hostname: + raise ValueError("connector only permits absolute https URLs") + host = parsed.hostname.lower() + if host not in ALLOWED_HOSTS: + raise ValueError(f"host {{host!r}} is not allowlisted") + for info in socket.getaddrinfo(host, parsed.port or 443, type=socket.SOCK_STREAM): + ip = ipaddress.ip_address(info[4][0]) + if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified: + raise ValueError(f"host {{host!r}} resolved to unsafe address") + return url + + +def idempotency_key(operation: str, payload: dict) -> str: + material = json.dumps({{"operation": operation, "payload": payload}}, sort_keys=True).encode("utf-8") + return hashlib.sha256(material).hexdigest() + + +async def send_json(url: str, payload: dict, *, secret_ref: str | None = None, dry_run: bool = True) -> dict: + body = json.dumps(payload).encode("utf-8") + if len(body) > MAX_BYTES: + raise ValueError("payload too large") + safe_url = validate_url(url) + key = idempotency_key("send_json", payload) + if dry_run: + return {{"status": "skipped", "reason": "dry_run", "idempotency_key": key, "url": safe_url}} + # Resolve secret_ref outside this scaffold using your approved secret manager. + headers = {{"Idempotency-Key": key, "Content-Type": "application/json"}} + async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS, follow_redirects=False) as client: + response = await client.post(safe_url, content=body, headers=headers) + return {{"status_code": response.status_code, "idempotency_key": key}} +''' + + +def _python_connector_test(req: IntegrationRequirements) -> str: + return f'''from connector import idempotency_key + + +def test_idempotency_key_stable(): + payload = {{"integration_id": "{req.integration_id}", "event": "synthetic"}} + assert idempotency_key("send_json", payload) == idempotency_key("send_json", payload) +''' + + +def _typescript_connector(req: IntegrationRequirements, allowlist: list[str], digest: str) -> str: + hosts = json.dumps(allowlist) + return f'''// Minimal connector scaffold for {req.integration_id}. Plan digest: {digest} +const ALLOWED_HOSTS = new Set({hosts}); +const MAX_BYTES = {MAX_JSON_BYTES}; + +export function validateUrl(raw: string): URL {{ + const url = new URL(raw); + if (url.protocol !== "https:") throw new Error("connector only permits https URLs"); + if (!ALLOWED_HOSTS.has(url.hostname.toLowerCase())) throw new Error("host is not allowlisted"); + return url; +}} + +export async function sendJson(rawUrl: string, payload: unknown, options: {{ dryRun?: boolean, secretRef?: string }} = {{}}) {{ + const url = validateUrl(rawUrl); + const body = JSON.stringify(payload); + if (new TextEncoder().encode(body).length > MAX_BYTES) throw new Error("payload too large"); + const idem = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body)); + const idempotencyKey = Array.from(new Uint8Array(idem)).map(b => b.toString(16).padStart(2, "0")).join(""); + if (options.dryRun !== false) return {{ status: "skipped", reason: "dry_run", idempotencyKey, url: url.toString() }}; + const response = await fetch(url, {{ method: "POST", body, headers: {{ "Content-Type": "application/json", "Idempotency-Key": idempotencyKey }} }}); + return {{ statusCode: response.status, idempotencyKey }}; +}} +''' + + +def _typescript_connector_test(req: IntegrationRequirements) -> str: + return f'''import {{ validateUrl }} from "./connector"; + +test("allowlist rejects unknown host for {req.integration_id}", () => {{ + expect(() => validateUrl("https://example.invalid/webhook")).toThrow(); +}}); +''' + + +def _validate_one_contract(contract: ContractArtifact) -> ContractCheck: + findings: list[str] = [] + fixture_results: list[dict[str, Any]] = [] + try: + if contract.kind == "json_schema": + from jsonschema import Draft202012Validator + Draft202012Validator.check_schema(contract.schema_doc) + validator = Draft202012Validator(contract.schema_doc) + for fixture in contract.fixtures: + errors = sorted(validator.iter_errors(fixture.payload), key=_jsonschema_error_path) + passed = not errors + expected = fixture.should_pass + fixture_results.append({"fixture": fixture.name, "passed": passed, "expected": expected, "errors": [e.message for e in errors[:5]]}) + if passed != expected: + findings.append(f"fixture {fixture.name!r} expected should_pass={expected} but got {passed}") + elif contract.kind == "openapi_3": + version = str(contract.schema_doc.get("openapi", "")) + if not version.startswith("3."): + findings.append("OpenAPI document must declare openapi: 3.x") + if not isinstance(contract.schema_doc.get("paths"), dict) or not contract.schema_doc.get("paths"): + findings.append("OpenAPI document must include non-empty paths") + if "servers" in contract.schema_doc and not isinstance(contract.schema_doc["servers"], list): + findings.append("OpenAPI servers must be a list") + elif contract.kind == "webhook": + if not contract.signature_header: + findings.append("webhook contract requires signature_header") + if not contract.secret_ref: + findings.append("webhook contract requires secret_ref, not a raw secret") + event_type = contract.schema_doc.get("event_type") or contract.schema_doc.get("type") + if not event_type: + findings.append("webhook schema_doc should include event_type or type") + for fixture in contract.fixtures: + has_event = "event" in fixture.payload or "type" in fixture.payload + passed = bool(has_event) + fixture_results.append({"fixture": fixture.name, "passed": passed, "expected": fixture.should_pass, "errors": [] if passed else ["missing event/type"]}) + if passed != fixture.should_pass: + findings.append(f"fixture {fixture.name!r} webhook expectation mismatch") + except Exception as exc: # noqa: BLE001 + findings.append(f"contract validation error: {type(exc).__name__}: {exc}") + status: Literal["pass", "fail", "warn"] = "fail" if findings else "pass" + return ContractCheck(name=contract.name, kind=contract.kind, status=status, findings=findings, fixture_results=fixture_results) + + +def _validate_hostname(host: str) -> str: + clean = host.strip().lower().rstrip(".") + if not clean or len(clean) > 253: + raise ValueError("invalid hostname") + if clean in {"localhost", "metadata.google.internal"} or clean.endswith(".local") or clean.endswith(".internal"): + raise ValueError("internal hostnames are not allowed") + try: + ip = ipaddress.ip_address(clean) + except ValueError: + if not re.fullmatch(r"[a-z0-9.-]+", clean) or ".." in clean: + raise ValueError("hostname contains invalid characters") + return clean + if _unsafe_ip(ip): + raise ValueError("private, loopback, link-local, multicast, reserved, or unspecified IPs are not allowed") + return clean + + +def _unsafe_ip(ip: ipaddress._BaseAddress) -> bool: + return bool(ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified) + + +def _validate_endpoint_metadata(url: str, allowlist: list[str]) -> str | None: + parsed = urlparse(url) + if parsed.scheme != "https" or not parsed.hostname: + return "endpoint URL must be absolute https with a hostname" + host = parsed.hostname.lower().rstrip(".") + if host not in set(allowlist): + return f"host {host!r} is not in external_host_allowlist" + try: + _validate_hostname(host) + except ValueError as exc: + return str(exc) + return None + + +async def _validate_endpoint_target(url: str, allowlist: list[str]) -> str | None: + metadata_error = _validate_endpoint_metadata(url, allowlist) + if metadata_error: + return metadata_error + parsed = urlparse(url) + host = parsed.hostname.lower().rstrip(".") # type: ignore[union-attr] + try: + infos = await asyncio.to_thread(socket.getaddrinfo, host, parsed.port or 443, type=socket.SOCK_STREAM) + except socket.gaierror as exc: + return f"DNS resolution failed for {host!r}: {exc}" + for info in infos: + ip = ipaddress.ip_address(info[4][0]) + if _unsafe_ip(ip): + return f"host {host!r} resolves to unsafe IP {ip}" + return None + + +async def _call_endpoint_with_retries(test: EndpointTest, max_retries: int, timeout_seconds: float, idem: str) -> AcceptanceStepResult: + import httpx + + attempts = 0 + last_detail = "not attempted" + headers = {"Idempotency-Key": idem, "Content-Type": "application/json"} + for attempt in range(max_retries + 1): + attempts = attempt + 1 + try: + async with httpx.AsyncClient(timeout=timeout_seconds, follow_redirects=False) as client: + response = await client.request(test.method, test.url, json=test.request_json, headers=headers) + if 300 <= response.status_code < 400: + location = response.headers.get("location", "") + return AcceptanceStepResult(name=test.name, status="blocked", attempts=attempts, detail=f"redirect blocked pending revalidation: {location[:200]}", idempotency_key=idem) + if response.status_code == test.expected_status: + return AcceptanceStepResult(name=test.name, status="pass", attempts=attempts, detail=f"received expected status {response.status_code}", idempotency_key=idem) + last_detail = f"expected {test.expected_status}, got {response.status_code}" + if response.status_code not in {408, 409, 425, 429, 500, 502, 503, 504}: + break + await asyncio.sleep(min(2.0, 0.25 * (attempt + 1))) + except Exception as exc: # noqa: BLE001 + last_detail = f"{type(exc).__name__}: {exc}" + await asyncio.sleep(min(2.0, 0.25 * (attempt + 1))) + return AcceptanceStepResult(name=test.name, status="fail", attempts=attempts, detail=redact_secrets(last_detail), idempotency_key=idem) + + +def _render_handoff(request: PrepareHandoffRequest, digest: str, approval_required: bool, approval_valid: bool) -> str: + operator_sections = "\n\n".join(f"## {s.heading}\n{s.body}" for s in request.operator_notes) or "## Operator Notes\nNo additional operator notes supplied." + delivery = "\n".join(f"- {item}" for item in request.delivery_actions) or "- No delivery actions requested." + return f"""# Integration Handoff: {request.title} + +Integration ID: `{request.integration_id}` +Plan digest: `{digest}` +Dry run: `{request.dry_run}` + +## Rollout Plan +{chr(10).join(f"{i+1}. {redact_secrets(step)}" for i, step in enumerate(request.rollout_steps))} + +## Rollback Plan +{chr(10).join(f"{i+1}. {redact_secrets(step)}" for i, step in enumerate(request.rollback_steps))} + +## Monitoring and Alerting Checks +{chr(10).join(f"- {redact_secrets(check)}" for check in request.monitoring_checks)} + +{operator_sections} + +## Delivery Actions Requiring Operator Control +{delivery} + +Approval required: `{approval_required}` +Approval valid for this exact digest: `{approval_valid}` + +This agent does not deploy, merge, modify customer systems, or execute delivery actions. Operators must perform approved delivery steps in their own controlled systems. + +## Evidence Checklist +- Review `architecture.md` and `mapping.json`. +- Review connector source under `connector/`. +- Review `contract-report.json`. +- Review `acceptance-report.json`. +- Confirm all secret references are configured in caller-owned systems. +"""