feat: harden community production foundation through story 1.5
This commit is contained in:
@@ -7,6 +7,7 @@ import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -20,6 +21,19 @@ 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
|
||||
@@ -94,8 +108,14 @@ def build_operation_payload(name: str, upstream_base_url: str) -> dict[str, Any]
|
||||
}
|
||||
|
||||
|
||||
def agent_mcp_url(base_url: str, workspace_slug: str, agent_slug: str) -> str:
|
||||
return f"{base_url.rstrip('/')}/mcp/v1/{workspace_slug}/{agent_slug}"
|
||||
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]:
|
||||
@@ -150,17 +170,30 @@ class Client:
|
||||
try:
|
||||
response = self.opener.open(request, timeout=self.timeout_seconds)
|
||||
status = response.status
|
||||
raw = response.read().decode("utf-8")
|
||||
body = json.loads(raw) if raw else None
|
||||
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().decode("utf-8")
|
||||
body = json.loads(raw) if raw else None
|
||||
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 SmokeError(f"{method} {url} returned {status}: {body}")
|
||||
raise safe_error("http", "unexpected_status", status=status)
|
||||
|
||||
return JsonResponse(status=status, headers=headers_obj, body=body)
|
||||
|
||||
@@ -183,14 +216,16 @@ def resolve_workspace(
|
||||
fallback_workspace_id: str,
|
||||
fallback_workspace_slug: str,
|
||||
) -> tuple[str, str]:
|
||||
session = client.request_json("GET", "/api/auth/session").body
|
||||
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 item.get("workspace", {}).get("id") == current_workspace_id
|
||||
if isinstance(item.get("workspace"), dict) and item["workspace"].get("id") == current_workspace_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -198,12 +233,14 @@ def resolve_workspace(
|
||||
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 SmokeError("authenticated session does not include a workspace id")
|
||||
raise safe_error("workspace", "missing_id")
|
||||
if not workspace_slug:
|
||||
raise SmokeError("authenticated session does not include a workspace slug")
|
||||
raise safe_error("workspace", "missing_slug")
|
||||
return workspace_id, workspace_slug
|
||||
|
||||
|
||||
@@ -212,24 +249,54 @@ def create_operation(
|
||||
workspace_id: str,
|
||||
operation_name: str,
|
||||
internal_upstream: str,
|
||||
) -> str:
|
||||
) -> tuple[str, int]:
|
||||
created = client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, "/operations"),
|
||||
build_operation_payload(operation_name, internal_upstream),
|
||||
).body
|
||||
return created["operation_id"]
|
||||
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 publish_operation(client: Client, workspace_id: str, operation_id: str) -> None:
|
||||
client.request_json(
|
||||
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 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": 1},
|
||||
)
|
||||
{"version": operation_version},
|
||||
).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) -> str:
|
||||
def create_agent(client: Client, workspace_id: str, agent_slug: str) -> tuple[str, int]:
|
||||
created = client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, "/agents"),
|
||||
@@ -241,23 +308,28 @@ def create_agent(client: Client, workspace_id: str, agent_slug: str) -> str:
|
||||
"tool_selection_policy": {},
|
||||
},
|
||||
).body
|
||||
return created["agent_id"]
|
||||
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 bind_and_publish_agent(
|
||||
client: Client,
|
||||
workspace_id: str,
|
||||
agent_id: str,
|
||||
agent_version: int,
|
||||
operation_id: str,
|
||||
operation_version: int,
|
||||
tool_name: str,
|
||||
) -> None:
|
||||
) -> int:
|
||||
client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, f"/agents/{agent_id}/bindings"),
|
||||
[
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"operation_version": 1,
|
||||
"operation_version": operation_version,
|
||||
"tool_name": tool_name,
|
||||
"tool_title": "Check internal service health",
|
||||
"tool_description_override": None,
|
||||
@@ -265,11 +337,18 @@ def bind_and_publish_agent(
|
||||
}
|
||||
],
|
||||
)
|
||||
client.request_json(
|
||||
published = client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, f"/agents/{agent_id}/publish"),
|
||||
{"version": 1},
|
||||
)
|
||||
{"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]:
|
||||
@@ -278,7 +357,18 @@ def create_agent_key(client: Client, workspace_id: str, agent_id: str) -> tuple[
|
||||
admin_path(workspace_id, f"/agents/{agent_id}/platform-api-keys"),
|
||||
{"name": f"smoke-key-{int(time.time())}", "scopes": ["read", "write"]},
|
||||
).body
|
||||
return created["secret"], created["api_key"]["api_key"]["id"]
|
||||
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(
|
||||
@@ -347,7 +437,7 @@ def initialize_mcp_session(client: Client, mcp_url: str, api_key: str) -> str:
|
||||
)
|
||||
session_id = initialized.headers.get("MCP-Session-Id")
|
||||
if not session_id:
|
||||
raise SmokeError("MCP initialize response did not include MCP-Session-Id")
|
||||
raise safe_error("mcp_initialize", "missing_session_id")
|
||||
|
||||
client.request_json(
|
||||
"POST",
|
||||
@@ -368,6 +458,20 @@ def initialize_mcp_session(client: Client, mcp_url: str, api_key: str) -> str:
|
||||
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,
|
||||
@@ -388,13 +492,50 @@ def call_mcp(
|
||||
).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"],
|
||||
"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 SmokeError("CRANK_STAGING_ADMIN_EMAIL is required")
|
||||
raise safe_error("configuration", "missing_admin_email")
|
||||
if not admin_password:
|
||||
raise SmokeError("CRANK_STAGING_ADMIN_PASSWORD is required")
|
||||
raise safe_error("configuration", "missing_admin_password")
|
||||
|
||||
timestamp = int(time.time())
|
||||
operation_name = f"internal_health_smoke_{timestamp}"
|
||||
@@ -403,8 +544,11 @@ def run(args: argparse.Namespace) -> None:
|
||||
operation_id = None
|
||||
agent_id = None
|
||||
key_id = None
|
||||
api_key = None
|
||||
mcp_url = None
|
||||
session_id = None
|
||||
|
||||
print(f"authenticated product smoke: {args.base_url.rstrip('/')}")
|
||||
print("authenticated product smoke: started")
|
||||
login(client, admin_email, admin_password)
|
||||
print("login: ok")
|
||||
workspace_id, workspace_slug = resolve_workspace(
|
||||
@@ -414,49 +558,80 @@ def run(args: argparse.Namespace) -> None:
|
||||
)
|
||||
print(f"workspace: {workspace_id} / {workspace_slug}")
|
||||
|
||||
operation_id = create_operation(
|
||||
client,
|
||||
workspace_id,
|
||||
operation_name,
|
||||
args.internal_upstream,
|
||||
)
|
||||
print(f"operation created: {operation_id}")
|
||||
publish_operation(client, workspace_id, operation_id)
|
||||
print("operation published: v1")
|
||||
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 = create_agent(client, workspace_id, agent_slug)
|
||||
bind_and_publish_agent(client, workspace_id, agent_id, operation_id, operation_name)
|
||||
print(f"agent published: {agent_id}")
|
||||
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.base_url, workspace_slug, agent_slug)
|
||||
session_id = initialize_mcp_session(client, mcp_url, api_key)
|
||||
print("mcp initialized: ok")
|
||||
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": {}},
|
||||
)
|
||||
tool_names = [tool["name"] for tool in tools.get("result", {}).get("tools", [])]
|
||||
if operation_name not in tool_names:
|
||||
raise SmokeError(f"published tool {operation_name} not found in tools/list: {tool_names}")
|
||||
print("tools/list: 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"}),
|
||||
)
|
||||
if "error" in result:
|
||||
raise SmokeError(f"tools/call returned error: {result['error']}")
|
||||
print("tools/call: ok")
|
||||
cleanup_smoke_assets(client, workspace_id, operation_id, agent_id, key_id)
|
||||
print("authenticated product smoke completed")
|
||||
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")
|
||||
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:
|
||||
@@ -467,6 +642,16 @@ def parse_args() -> argparse.Namespace:
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user