#!/usr/bin/env python3 import argparse import http.cookiejar import json import os import time import urllib.error import urllib.request from dataclasses import dataclass from pathlib import Path from typing import Any DEFAULT_WORKSPACE_ID = "ws_default" DEFAULT_WORKSPACE_SLUG = "default" DEFAULT_INTERNAL_UPSTREAM = "http://admin-api:3001" MCP_PROTOCOL_VERSION = "2025-11-25" class SmokeError(RuntimeError): pass def safe_error(stage: str, code: str, status: int | None = None) -> SmokeError: message = f"stage={stage[:64]} code={code[:96]}" if status is not None: message += f" status={status}" return SmokeError(message[:256]) def require_object(value: Any, stage: str) -> dict[str, Any]: if not isinstance(value, dict): raise safe_error(stage, "invalid_response") return value @dataclass class JsonResponse: status: int headers: Any body: Any def build_operation_payload(name: str, upstream_base_url: str) -> dict[str, Any]: return { "name": name, "display_name": "Internal Health Smoke", "category": "smoke", "protocol": "rest", "security_level": "standard", "target": { "kind": "rest", "base_url": upstream_base_url.rstrip("/"), "method": "GET", "path_template": "/health", "static_headers": {}, }, "input_schema": { "type": "object", "required": True, "fields": { "probe": { "type": "string", "required": True, "description": "Smoke probe value.", }, }, }, "output_schema": { "type": "object", "required": True, "fields": { "status": { "type": "string", "required": True, "description": "Health status returned by admin-api.", }, }, }, "input_mapping": { "rules": [ { "source": "$.mcp.probe", "target": "$.request.query.probe", "required": True, }, ], }, "output_mapping": { "rules": [ { "source": "$.response.body.status", "target": "$.output.status", "required": True, }, ], }, "execution_config": { "timeout_ms": 5000, "headers": {}, }, "tool_description": { "title": "Check internal service health", "description": "Checks the internal Crank admin-api health endpoint. Use this only for deployment smoke verification.", "tags": ["smoke", "health"], "examples": [{"input": {"probe": "ok"}}], }, } def agent_mcp_url( base_url: str, workspace_slug: str, agent_slug: str, path_prefix: str = "/mcp", ) -> str: normalized_prefix = "/" + path_prefix.strip("/") if path_prefix.strip("/") else "" return f"{base_url.rstrip('/')}{normalized_prefix}/v1/{workspace_slug}/{agent_slug}" def tools_call_payload(tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: return { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": tool_name, "arguments": arguments, }, } class Client: def __init__(self, base_url: str, timeout_seconds: int) -> None: self.base_url = base_url.rstrip("/") self.timeout_seconds = timeout_seconds cookie_jar = http.cookiejar.CookieJar() self.opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(cookie_jar) ) def request_json( self, method: str, path_or_url: str, payload: dict[str, Any] | list[Any] | None = None, headers: dict[str, str] | None = None, expected: tuple[int, ...] = (200,), ) -> JsonResponse: url = ( path_or_url if path_or_url.startswith("http://") or path_or_url.startswith("https://") else f"{self.base_url}{path_or_url}" ) data = None request_headers = {"Accept": "application/json"} if headers: request_headers.update(headers) if payload is not None: data = json.dumps(payload).encode("utf-8") request_headers["Content-Type"] = "application/json" request = urllib.request.Request( url, data=data, headers=request_headers, method=method, ) try: response = self.opener.open(request, timeout=self.timeout_seconds) status = response.status raw_bytes = response.read(1_048_577) if len(raw_bytes) > 1_048_576: raise safe_error("http", "response_too_large", status=status) try: raw = raw_bytes.decode("utf-8") body = json.loads(raw) if raw else None except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError) as error: raise safe_error("http", "invalid_json", status=status) from error headers_obj = response.headers except urllib.error.HTTPError as error: status = error.code raw = error.read(1_048_577) if len(raw) > 1_048_576: raise safe_error("http", "response_too_large", status=status) try: body = json.loads(raw.decode("utf-8")) if raw else None except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError) as parse_error: raise safe_error("http", "invalid_json", status=status) from parse_error headers_obj = error.headers except (urllib.error.URLError, TimeoutError, OSError) as error: raise safe_error("http", "request_failed") from error if status not in expected: raise safe_error("http", "unexpected_status", status=status) return JsonResponse(status=status, headers=headers_obj, body=body) def admin_path(workspace_id: str, suffix: str) -> str: return f"/api/admin/workspaces/{workspace_id}{suffix}" def login(client: Client, email: str, password: str) -> None: client.request_json( "POST", "/api/auth/login", {"email": email, "password": password}, expected=(200,), ) def resolve_workspace( client: Client, fallback_workspace_id: str, fallback_workspace_slug: str, ) -> tuple[str, str]: session = require_object(client.request_json("GET", "/api/auth/session").body, "workspace") memberships = session.get("memberships") or [] if not isinstance(memberships, list) or any(not isinstance(item, dict) for item in memberships): raise safe_error("workspace", "invalid_response") current_workspace_id = session.get("current_workspace_id") or fallback_workspace_id membership = next( ( item for item in memberships if isinstance(item.get("workspace"), dict) and item["workspace"].get("id") == current_workspace_id ), None, ) if not membership and memberships: membership = memberships[0] workspace = membership.get("workspace", {}) if membership else {} if not isinstance(workspace, dict): raise safe_error("workspace", "invalid_response") workspace_id = workspace.get("id") or current_workspace_id workspace_slug = workspace.get("slug") or fallback_workspace_slug if not workspace_id: raise safe_error("workspace", "missing_id") if not workspace_slug: raise safe_error("workspace", "missing_slug") return workspace_id, workspace_slug def create_operation( client: Client, workspace_id: str, operation_name: str, internal_upstream: str, ) -> tuple[str, int]: created = client.request_json( "POST", admin_path(workspace_id, "/operations"), build_operation_payload(operation_name, internal_upstream), ).body try: return str(created["operation_id"]), int(created["version"]) except (KeyError, TypeError, ValueError) as error: raise safe_error("operation_create", "invalid_response") from error def run_operation_test( client: Client, workspace_id: str, operation_id: str, operation_version: int, ) -> None: result = client.request_json( "POST", admin_path(workspace_id, f"/operations/{operation_id}/test-runs"), {"version": operation_version, "input": {"probe": "ok"}}, ).body if not isinstance(result, dict) or result.get("ok") is not True: raise safe_error("operation_test", "outcome_not_ok") def operation_etag(client: Client, workspace_id: str, operation_id: str) -> str: response = client.request_json( "GET", admin_path(workspace_id, f"/operations/{operation_id}"), ) etag = response.headers.get("ETag") if response.headers is not None else None if not isinstance(etag, str) or len(etag) > 128 or not etag.startswith('"') or not etag.endswith('"'): raise safe_error("operation_precondition", "invalid_response") return etag def publish_operation( client: Client, workspace_id: str, operation_id: str, operation_version: int, ) -> int: published = client.request_json( "POST", admin_path(workspace_id, f"/operations/{operation_id}/publish"), {"version": operation_version}, headers={"If-Match": operation_etag(client, workspace_id, operation_id)}, ).body try: published_version = int(published["published_version"]) except (KeyError, TypeError, ValueError) as error: raise safe_error("operation_publish", "invalid_response") from error if published_version != operation_version: raise safe_error("operation_publish", "version_mismatch") return published_version def create_agent(client: Client, workspace_id: str, agent_slug: str) -> tuple[str, int]: created = client.request_json( "POST", admin_path(workspace_id, "/agents"), { "slug": agent_slug, "display_name": "Internal Health Smoke", "description": "Deployment smoke agent.", "instructions": {}, "tool_selection_policy": {}, }, ).body try: return str(created["agent_id"]), int(created["version"]) except (KeyError, TypeError, ValueError) as error: raise safe_error("agent_create", "invalid_response") from error def edit_and_archive_operation( client: Client, workspace_id: str, operation_id: str, operation_name: str, internal_upstream: str, ) -> int: payload = build_operation_payload(operation_name, internal_upstream) payload.pop("name", None) payload.pop("protocol", None) payload["display_name"] = "Internal Health Smoke Edited Draft" updated = client.request_json( "PATCH", admin_path(workspace_id, f"/operations/{operation_id}"), payload, headers={"If-Match": operation_etag(client, workspace_id, operation_id)}, ).body try: draft_version = int(updated["version"]) except (KeyError, TypeError, ValueError) as error: raise safe_error("operation_edit", "invalid_response") from error client.request_json( "POST", admin_path(workspace_id, f"/operations/{operation_id}/archive"), headers={"If-Match": operation_etag(client, workspace_id, operation_id)}, ) return draft_version def bind_and_publish_agent( client: Client, workspace_id: str, agent_id: str, agent_version: int, operation_id: str, operation_version: int, tool_name: str, ) -> int: client.request_json( "POST", admin_path(workspace_id, f"/agents/{agent_id}/bindings"), [ { "operation_id": operation_id, "operation_version": operation_version, "tool_name": tool_name, "tool_title": "Check internal service health", "tool_description_override": None, "enabled": True, } ], ) published = client.request_json( "POST", admin_path(workspace_id, f"/agents/{agent_id}/publish"), {"version": agent_version}, ).body try: published_version = int(published["published_version"]) except (KeyError, TypeError, ValueError) as error: raise safe_error("agent_publish", "invalid_response") from error if published_version != agent_version: raise safe_error("agent_publish", "version_mismatch") return published_version def create_agent_key(client: Client, workspace_id: str, agent_id: str) -> tuple[str, str]: created = client.request_json( "POST", admin_path(workspace_id, f"/agents/{agent_id}/platform-api-keys"), {"name": f"smoke-key-{int(time.time())}", "scopes": ["read", "write"]}, ).body try: secret = created["secret"] key_id = created["api_key"]["api_key"]["id"] except (KeyError, TypeError) as error: raise safe_error("key_create", "invalid_response") from error if not nonempty_string(secret) or not nonempty_string(key_id): raise safe_error("key_create", "invalid_response") return secret, key_id def nonempty_string(value: Any) -> bool: return isinstance(value, str) and bool(value.strip()) def cleanup_smoke_assets( client: Client, workspace_id: str, operation_id: str | None, agent_id: str | None, key_id: str | None, ) -> None: if os.environ.get("CRANK_PRODUCT_SMOKE_KEEP_ASSETS") == "1": print("cleanup skipped: CRANK_PRODUCT_SMOKE_KEEP_ASSETS=1") return if agent_id and key_id: try: client.request_json( "DELETE", admin_path(workspace_id, f"/agents/{agent_id}/platform-api-keys/{key_id}"), expected=(200, 204, 404), ) except SmokeError as error: print(f"cleanup warning: key delete failed: {error}") if agent_id: try: client.request_json( "DELETE", admin_path(workspace_id, f"/agents/{agent_id}"), expected=(200, 404), ) except SmokeError as error: print(f"cleanup warning: agent delete failed: {error}") if operation_id: try: client.request_json( "DELETE", admin_path(workspace_id, f"/operations/{operation_id}"), headers={"If-Match": operation_etag(client, workspace_id, operation_id)}, expected=(200, 404), ) except SmokeError: try: client.request_json( "POST", admin_path(workspace_id, f"/operations/{operation_id}/archive"), headers={"If-Match": operation_etag(client, workspace_id, operation_id)}, expected=(200, 404), ) except SmokeError as error: print(f"cleanup warning: operation cleanup failed: {error}") def initialize_mcp_session(client: Client, mcp_url: str, api_key: str) -> str: initialized = client.request_json( "POST", mcp_url, { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": MCP_PROTOCOL_VERSION}, }, headers={ "Accept": "application/json, text/event-stream", "Authorization": f"Bearer {api_key}", }, ) session_id = initialized.headers.get("MCP-Session-Id") if not session_id: raise safe_error("mcp_initialize", "missing_session_id") client.request_json( "POST", mcp_url, { "jsonrpc": "2.0", "method": "notifications/initialized", "params": {}, }, headers={ "Accept": "application/json, text/event-stream", "Authorization": f"Bearer {api_key}", "MCP-Session-Id": session_id, "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, }, expected=(202,), ) return session_id def terminate_mcp_session(client: Client, mcp_url: str, api_key: str, session_id: str) -> None: client.request_json( "DELETE", mcp_url, headers={ "Accept": "application/json", "Authorization": f"Bearer {api_key}", "MCP-Session-Id": session_id, "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, }, expected=(204, 404), ) def call_mcp( client: Client, mcp_url: str, api_key: str, session_id: str, payload: dict[str, Any], ) -> Any: return client.request_json( "POST", mcp_url, payload, headers={ "Accept": "application/json, text/event-stream", "Authorization": f"Bearer {api_key}", "MCP-Session-Id": session_id, "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, }, ).body def validate_tools_list(value: Any, expected_name: str) -> None: payload = require_object(value, "mcp_tools_list") result = payload.get("result") tools = result.get("tools") if isinstance(result, dict) else None if not isinstance(tools, list) or any(not isinstance(tool, dict) or not nonempty_string(tool.get("name")) for tool in tools): raise safe_error("mcp_tools_list", "invalid_response") if expected_name not in [tool["name"] for tool in tools]: raise safe_error("mcp_tools_list", "published_tool_missing") def validate_tool_call_result(value: Any) -> None: payload = require_object(value, "mcp_tools_call") result = payload.get("result") if "error" in payload or not isinstance(result, dict) or result.get("isError") is not False: raise safe_error("mcp_tools_call", "outcome_not_ok") structured = result.get("structuredContent") if not isinstance(structured, dict) or structured.get("status") != "ok": raise safe_error("mcp_tools_call", "unexpected_output") def build_safe_summary( operation_id: str, operation_version: int, agent_id: str, agent_revision: int, ) -> dict[str, Any]: return { "agent_id": agent_id[:128], "agent_revision": agent_revision, "command_id": "authenticated-product-smoke", "operation_id": operation_id[:128], "operation_version": operation_version, "stages": [ "admin_test", "operation_publish", "agent_publish", "mcp_list", "mcp_call", "operation_edit_archive", "pinned_mcp_list_call", ], "verdict": "pass", } def run(args: argparse.Namespace) -> None: admin_email = os.environ.get("CRANK_STAGING_ADMIN_EMAIL") admin_password = os.environ.get("CRANK_STAGING_ADMIN_PASSWORD") if not admin_email: raise safe_error("configuration", "missing_admin_email") if not admin_password: raise safe_error("configuration", "missing_admin_password") timestamp = int(time.time()) operation_name = f"internal_health_smoke_{timestamp}" agent_slug = f"health-smoke-{timestamp}" client = Client(args.base_url, args.timeout_seconds) operation_id = None agent_id = None key_id = None api_key = None mcp_url = None session_id = None print("authenticated product smoke: started") login(client, admin_email, admin_password) print("login: ok") workspace_id, workspace_slug = resolve_workspace( client, args.workspace_id, args.workspace_slug, ) print(f"workspace: {workspace_id} / {workspace_slug}") try: operation_id, operation_version = create_operation( client, workspace_id, operation_name, args.internal_upstream, ) print(f"operation created: {operation_id} version={operation_version}") run_operation_test(client, workspace_id, operation_id, operation_version) print("operation test: ok") published_operation_version = publish_operation( client, workspace_id, operation_id, operation_version ) print(f"operation published: version={published_operation_version}") agent_id, agent_version = create_agent(client, workspace_id, agent_slug) published_agent_version = bind_and_publish_agent( client, workspace_id, agent_id, agent_version, operation_id, published_operation_version, operation_name, ) print(f"agent published: {agent_id} revision={published_agent_version}") api_key, key_id = create_agent_key(client, workspace_id, agent_id) mcp_url = agent_mcp_url( args.mcp_base_url or args.base_url, workspace_slug, agent_slug, args.mcp_path_prefix, ) session_id = initialize_mcp_session(client, mcp_url, api_key) print("mcp initialized: ok") tools = call_mcp( client, mcp_url, api_key, session_id, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, ) validate_tools_list(tools, operation_name) print("tools/list: ok") result = call_mcp( client, mcp_url, api_key, session_id, tools_call_payload(operation_name, {"probe": "ok"}), ) validate_tool_call_result(result) print("tools/call: ok") edited_draft_version = edit_and_archive_operation( client, workspace_id, operation_id, operation_name, args.internal_upstream, ) print(f"operation edited and archived: draft_version={edited_draft_version}") tools = call_mcp( client, mcp_url, api_key, session_id, {"jsonrpc": "2.0", "id": 4, "method": "tools/list", "params": {}}, ) validate_tools_list(tools, operation_name) pinned_result = call_mcp( client, mcp_url, api_key, session_id, tools_call_payload(operation_name, {"probe": "ok"}), ) validate_tool_call_result(pinned_result) print("pinned tools/list and tools/call after Operation archive: ok") summary = build_safe_summary( operation_id, published_operation_version, agent_id, published_agent_version ) if args.summary_output: try: Path(args.summary_output).write_text( json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) except OSError as error: raise safe_error("summary", "write_failed") from error print("authenticated product smoke completed") finally: if mcp_url and api_key and session_id: try: terminate_mcp_session(client, mcp_url, api_key, session_id) except SmokeError as error: print(f"cleanup warning: session termination failed: {error}") cleanup_smoke_assets(client, workspace_id, operation_id, agent_id, key_id) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run deterministic authenticated product smoke against deployed Crank." ) parser.add_argument("base_url", help="Public Crank base URL, for example https://crank.example.com") parser.add_argument("--workspace-id", default=DEFAULT_WORKSPACE_ID) parser.add_argument("--workspace-slug", default=DEFAULT_WORKSPACE_SLUG) parser.add_argument("--internal-upstream", default=DEFAULT_INTERNAL_UPSTREAM) parser.add_argument( "--mcp-base-url", help="Optional direct MCP base URL for a local split-port stack.", ) parser.add_argument( "--mcp-path-prefix", default="/mcp", help="MCP proxy prefix; use an empty value for a direct MCP listener.", ) parser.add_argument("--summary-output") parser.add_argument( "--timeout-seconds", type=int, default=int(os.environ.get("SMOKE_TIMEOUT_SECONDS", "20")), ) return parser.parse_args() def main() -> int: try: run(parse_args()) except SmokeError as error: print(f"smoke failed: {error}", flush=True) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())