from __future__ import annotations from typing import Any from engine.devops_agent.spec import WorkflowSpec WRITE_PERMISSIONS = {"issues", "pull_requests", "contents"} def _is_write_permission(value: Any) -> bool: return str(value).strip().lower() == "write" def validate_workflow_spec(spec: WorkflowSpec) -> list[str]: errors: list[str] = [] if spec.provider not in {"gitea"}: errors.append(f"unsupported provider: {spec.provider}") triggers = spec.frontmatter.get("on") if not isinstance(triggers, dict) or not triggers: errors.append("workflow spec must declare at least one trigger in 'on'") permissions = spec.frontmatter.get("permissions") or {} safe_outputs = spec.frontmatter.get("safe_outputs") or {} if not isinstance(permissions, dict): errors.append("'permissions' must be a mapping") if not isinstance(safe_outputs, dict): errors.append("'safe_outputs' must be a mapping") if isinstance(permissions, dict): has_write_permission = any( permission_name in WRITE_PERMISSIONS and _is_write_permission(permission_value) for permission_name, permission_value in permissions.items() ) if has_write_permission and not safe_outputs: errors.append("write permissions require declared safe_outputs") policy = spec.frontmatter.get("policy") or {} if policy and not isinstance(policy, dict): errors.append("'policy' must be a mapping") elif isinstance(policy, dict) and "path_scope" in policy: path_scope = policy["path_scope"] if not isinstance(path_scope, list) or any( not isinstance(item, str) or not item.strip() for item in path_scope ): errors.append("policy.path_scope must be a list of non-empty path prefixes") return errors