From a3553377b88664bcdb2005e560a204f8c460128e Mon Sep 17 00:00:00 2001 From: "yanan.zhangyn" Date: Tue, 28 Jul 2026 21:12:06 +0800 Subject: [PATCH] feat(harness): add sidecar extension --- agentkit/extensions/__init__.py | 17 + agentkit/extensions/harness_sidecar/README.md | 14 + .../extensions/harness_sidecar/__init__.py | 93 ++++ agentkit/extensions/harness_sidecar/cli.py | 371 +++++++++++++++ .../harness_sidecar/component_catalog.py | 232 +++++++++ agentkit/extensions/harness_sidecar/deploy.py | 121 +++++ .../harness_sidecar/failover_proxy.py | 162 +++++++ .../extensions/harness_sidecar/profiles.py | 125 +++++ .../harness_sidecar/runtime_components.py | 111 +++++ .../extensions/harness_sidecar/selection.py | 293 ++++++++++++ .../extensions/harness_sidecar/sidecar.py | 445 ++++++++++++++++++ .../harness_sidecar/sidecar_config.py | 335 +++++++++++++ agentkit/toolkit/cli/cli.py | 2 + agentkit/toolkit/harness/config_builder.py | 3 + agentkit/toolkit/harness/deploy.py | 7 +- agentkit/version.py | 2 +- pyproject.toml | 3 +- .../harness_sidecar/cli/test_cli.py | 159 +++++++ .../harness_sidecar/test_component_catalog.py | 186 ++++++++ .../extensions/harness_sidecar/test_deploy.py | 96 ++++ .../harness_sidecar/test_failover_proxy.py | 72 +++ .../harness_sidecar/test_sidecar.py | 380 +++++++++++++++ .../harness_sidecar/test_sidecar_config.py | 289 ++++++++++++ tests/toolkit/test_harness_extension_hooks.py | 53 +++ 24 files changed, 3568 insertions(+), 3 deletions(-) create mode 100644 agentkit/extensions/__init__.py create mode 100644 agentkit/extensions/harness_sidecar/README.md create mode 100644 agentkit/extensions/harness_sidecar/__init__.py create mode 100644 agentkit/extensions/harness_sidecar/cli.py create mode 100644 agentkit/extensions/harness_sidecar/component_catalog.py create mode 100644 agentkit/extensions/harness_sidecar/deploy.py create mode 100644 agentkit/extensions/harness_sidecar/failover_proxy.py create mode 100644 agentkit/extensions/harness_sidecar/profiles.py create mode 100644 agentkit/extensions/harness_sidecar/runtime_components.py create mode 100644 agentkit/extensions/harness_sidecar/selection.py create mode 100644 agentkit/extensions/harness_sidecar/sidecar.py create mode 100644 agentkit/extensions/harness_sidecar/sidecar_config.py create mode 100644 tests/extensions/harness_sidecar/cli/test_cli.py create mode 100644 tests/extensions/harness_sidecar/test_component_catalog.py create mode 100644 tests/extensions/harness_sidecar/test_deploy.py create mode 100644 tests/extensions/harness_sidecar/test_failover_proxy.py create mode 100644 tests/extensions/harness_sidecar/test_sidecar.py create mode 100644 tests/extensions/harness_sidecar/test_sidecar_config.py create mode 100644 tests/toolkit/test_harness_extension_hooks.py diff --git a/agentkit/extensions/__init__.py b/agentkit/extensions/__init__.py new file mode 100644 index 00000000..258bafa3 --- /dev/null +++ b/agentkit/extensions/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Optional AgentKit extensions.""" + +__all__: list[str] = [] diff --git a/agentkit/extensions/harness_sidecar/README.md b/agentkit/extensions/harness_sidecar/README.md new file mode 100644 index 00000000..6be07ef7 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/README.md @@ -0,0 +1,14 @@ +# AgentKit Harness Sidecar Extension + +Public integration for configuring and launching the private Harness Sidecar +Runtime. Projects that use the capability can declare the SDK extra: + +```bash +pip install "agentkit-sdk-python[harness-sidecar]" +``` + +The extension source is always bundled in the single SDK wheel, matching the +existing veADK extension convention. The extra does not change the wheel contents +and currently adds no public dependency; it is a stable install selector for +Sidecar users. The private Runtime artifact is supplied only by AgentKit-managed +cloud runtimes. diff --git a/agentkit/extensions/harness_sidecar/__init__.py b/agentkit/extensions/harness_sidecar/__init__.py new file mode 100644 index 00000000..690599ce --- /dev/null +++ b/agentkit/extensions/harness_sidecar/__init__.py @@ -0,0 +1,93 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Optional AgentKit Harness Sidecar integration.""" + +from .component_catalog import ( + CATALOG_SCHEMA_VERSION, + CATALOG_VERSION, + PRODUCT_COMPONENT_ORDER, + ComponentAvailability, + HarnessComponentDefinition, + HarnessProfileDefinition, + HarnessSidecarCatalog, + get_harness_sidecar_catalog, +) +from .deploy import build_runtime_network, deploy_harness, to_runtime_env +from .runtime_components import ( + RUNTIME_COMPONENT_ORDER, + resolve_runtime_components, + runtime_flavor_for_components, +) +from .selection import ( + PLAN_SCHEMA_VERSION, + AutoAddedComponent, + HarnessActivationTargets, + HarnessSelectionIntent, + ResolvedHarnessPlan, + resolve_harness_sidecar_selection, +) +from .sidecar import ( + HarnessSidecarError, + HarnessSidecarRuntimeUnavailable, + SidecarBinding, + doctor_harness_sidecar, + export_sidecar_env, + run_with_harness_sidecar, + start_harness_sidecar, +) +from .sidecar_config import ( + HarnessSidecarConfig, + MCPGatewayConfig, + ModelProxyConfig, + SidecarBindingSpec, + resolve_sidecar_config, + sidecar_config_to_env, +) + +__all__ = [ + "AutoAddedComponent", + "CATALOG_SCHEMA_VERSION", + "CATALOG_VERSION", + "ComponentAvailability", + "HarnessActivationTargets", + "HarnessComponentDefinition", + "HarnessProfileDefinition", + "HarnessSelectionIntent", + "HarnessSidecarCatalog", + "HarnessSidecarConfig", + "HarnessSidecarError", + "HarnessSidecarRuntimeUnavailable", + "MCPGatewayConfig", + "ModelProxyConfig", + "PLAN_SCHEMA_VERSION", + "PRODUCT_COMPONENT_ORDER", + "RUNTIME_COMPONENT_ORDER", + "ResolvedHarnessPlan", + "SidecarBinding", + "SidecarBindingSpec", + "build_runtime_network", + "deploy_harness", + "doctor_harness_sidecar", + "export_sidecar_env", + "get_harness_sidecar_catalog", + "resolve_harness_sidecar_selection", + "resolve_runtime_components", + "resolve_sidecar_config", + "run_with_harness_sidecar", + "runtime_flavor_for_components", + "sidecar_config_to_env", + "start_harness_sidecar", + "to_runtime_env", +] diff --git a/agentkit/extensions/harness_sidecar/cli.py b/agentkit/extensions/harness_sidecar/cli.py new file mode 100644 index 00000000..6e9f9c95 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/cli.py @@ -0,0 +1,371 @@ +"""Public ``agentkit harness sidecar`` commands.""" + +from __future__ import annotations + +import json +import shlex +import sys +from pathlib import Path +from typing import Annotated + +import typer + +from agentkit.toolkit.harness import HarnessDeployAborted + +from .component_catalog import ( + CATALOG_VERSION, + get_harness_sidecar_catalog, +) +from .deploy import deploy_harness +from .selection import resolve_harness_sidecar_selection +from .sidecar import ( + HarnessSidecarError, + doctor_harness_sidecar, + run_with_harness_sidecar, + start_harness_sidecar, +) +from .sidecar_config import ( + HarnessSidecarConfig, + sidecar_config_to_env, +) + + +harness_app = typer.Typer(help="AgentKit Harness capabilities.") +sidecar_app = typer.Typer(help="Configure and run zero-intrusion Harness Sidecar.") +harness_app.add_typer(sidecar_app, name="sidecar") + + +@sidecar_app.command("catalog") +def catalog( + profile: Annotated[str, typer.Option("--profile")] = "ops", +) -> None: + """Print the Runtime-independent Product Component Catalog.""" + + try: + value = get_harness_sidecar_catalog(profile) + except ValueError as error: + raise typer.BadParameter(str(error)) from error + typer.echo(value.model_dump_json(indent=2)) + + +@sidecar_app.command("resolve") +def resolve_selection( + profile: Annotated[str, typer.Option("--profile")] = "ops", + enabled: Annotated[bool, typer.Option("--enabled/--disabled")] = True, + component: Annotated[ + list[str] | None, + typer.Option( + "--component", help="Product component override as ID=true|false." + ), + ] = None, + catalog_version: Annotated[ + str, typer.Option("--catalog-version") + ] = CATALOG_VERSION, + runtime_version: Annotated[str | None, typer.Option("--runtime-version")] = None, +) -> None: + """Resolve Product Component overrides into a deterministic plan.""" + + overrides: dict[str, bool] = {} + for raw in component or []: + component_id, separator, selected = raw.partition("=") + normalized = selected.strip().lower() + if not separator or normalized not in {"true", "false"}: + raise typer.BadParameter( + f"invalid component override '{raw}'; expected ID=true|false" + ) + overrides[component_id.strip()] = normalized == "true" + plan = resolve_harness_sidecar_selection( + enabled=enabled, + profile=profile, + component_overrides=overrides, + catalog_version=catalog_version, + runtime_version=runtime_version, + ) + typer.echo(plan.model_dump_json(indent=2, exclude_none=True)) + if not plan.valid: + raise typer.Exit(1) + + +def _config( + *, + profile: str | None, + config_path: Path | None, + model_proxy: bool | None, + mcp_gateway: bool | None, + model_upstream_env: str | None, + compression_provider: str | None, + mcp_upstreams_env: str | None, + presets: list[str] | None, + readonly_segments: list[str] | None, +) -> HarnessSidecarConfig: + raw: dict = {} + if config_path is not None: + value = json.loads(config_path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise typer.BadParameter("sidecar config must be a JSON object") + raw.update(value) + if profile is not None: + raw["profile"] = profile + else: + raw.setdefault("profile", "ops") + + model_overrides = {} + if model_proxy is not None: + model_overrides["enabled"] = model_proxy + if model_upstream_env is not None: + model_overrides["upstream_base_url_env"] = model_upstream_env + if compression_provider is not None: + model_overrides["compression_provider"] = compression_provider + if model_overrides: + raw.setdefault("model_proxy", {}).update(model_overrides) + + mcp_overrides = {} + if mcp_gateway is not None: + mcp_overrides["enabled"] = mcp_gateway + if mcp_upstreams_env is not None: + mcp_overrides["upstreams_env"] = mcp_upstreams_env + if presets is not None: + mcp_overrides["presets"] = presets + if readonly_segments is not None: + mcp_overrides["readonly_segments"] = readonly_segments + if mcp_overrides: + raw.setdefault("mcp_gateway", {}).update(mcp_overrides) + return HarnessSidecarConfig.model_validate(raw) + + +def _common_config( + profile: str | None, + config: Path | None, + model_proxy: bool | None, + mcp_gateway: bool | None, + model_upstream_env: str | None, + compression_provider: str | None, + mcp_upstreams_env: str | None, + preset: list[str] | None, + readonly_segment: list[str] | None, +) -> HarnessSidecarConfig: + return _config( + profile=profile, + config_path=config, + model_proxy=model_proxy, + mcp_gateway=mcp_gateway, + model_upstream_env=model_upstream_env, + compression_provider=compression_provider, + mcp_upstreams_env=mcp_upstreams_env, + presets=preset, + readonly_segments=readonly_segment, + ) + + +@sidecar_app.command("doctor") +def doctor( + profile: Annotated[str, typer.Option("--profile")] = "ops", +) -> None: + try: + report = doctor_harness_sidecar({"profile": profile}) + except HarnessSidecarError as error: + typer.echo(str(error), err=True) + raise typer.Exit(1) from error + typer.echo(json.dumps(report, ensure_ascii=False, indent=2)) + + +@sidecar_app.command("start") +def start( + profile: Annotated[str | None, typer.Option("--profile")] = None, + config: Annotated[Path | None, typer.Option("--config")] = None, + model_proxy: Annotated[ + bool | None, typer.Option("--model-proxy/--no-model-proxy") + ] = None, + mcp_gateway: Annotated[ + bool | None, typer.Option("--mcp-gateway/--no-mcp-gateway") + ] = None, + model_upstream_env: Annotated[ + str | None, typer.Option("--model-upstream-env") + ] = None, + compression_provider: Annotated[ + str | None, typer.Option("--compression-provider") + ] = None, + mcp_upstreams_env: Annotated[ + str | None, typer.Option("--mcp-upstreams-env") + ] = None, + preset: Annotated[list[str] | None, typer.Option("--preset")] = None, + readonly_segment: Annotated[ + list[str] | None, typer.Option("--readonly-segment") + ] = None, +) -> None: + resolved = _common_config( + profile, + config, + model_proxy, + mcp_gateway, + model_upstream_env, + compression_provider, + mcp_upstreams_env, + preset, + readonly_segment, + ) + try: + with start_harness_sidecar(resolved) as binding: + typer.echo(binding.spec.model_dump_json(indent=2)) + if binding.process is not None: + binding.process.wait() + except (HarnessSidecarError, KeyboardInterrupt) as error: + if isinstance(error, HarnessSidecarError): + typer.echo(str(error), err=True) + raise typer.Exit(1) from error + + +@sidecar_app.command( + "run", + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) +def run( + ctx: typer.Context, + profile: Annotated[str | None, typer.Option("--profile")] = None, + config: Annotated[Path | None, typer.Option("--config")] = None, + model_proxy: Annotated[ + bool | None, typer.Option("--model-proxy/--no-model-proxy") + ] = None, + mcp_gateway: Annotated[ + bool | None, typer.Option("--mcp-gateway/--no-mcp-gateway") + ] = None, + model_upstream_env: Annotated[ + str | None, typer.Option("--model-upstream-env") + ] = None, + compression_provider: Annotated[ + str | None, typer.Option("--compression-provider") + ] = None, + mcp_upstreams_env: Annotated[ + str | None, typer.Option("--mcp-upstreams-env") + ] = None, + preset: Annotated[list[str] | None, typer.Option("--preset")] = None, + readonly_segment: Annotated[ + list[str] | None, typer.Option("--readonly-segment") + ] = None, +) -> None: + command = list(ctx.args) + if command and command[0] == "--": + command.pop(0) + if not command: + raise typer.BadParameter("provide a command after '--'") + resolved = _common_config( + profile, + config, + model_proxy, + mcp_gateway, + model_upstream_env, + compression_provider, + mcp_upstreams_env, + preset, + readonly_segment, + ) + try: + exit_code = run_with_harness_sidecar(resolved, command) + except HarnessSidecarError as error: + typer.echo(str(error), err=True) + raise typer.Exit(1) from error + raise typer.Exit(exit_code) + + +@sidecar_app.command("export-env") +def export_env( + profile: Annotated[str | None, typer.Option("--profile")] = None, + config: Annotated[Path | None, typer.Option("--config")] = None, + shell: Annotated[bool, typer.Option("--shell")] = False, +) -> None: + raw = {} + if config is not None: + raw = json.loads(config.read_text(encoding="utf-8")) + if profile is not None: + raw["profile"] = profile + else: + raw.setdefault("profile", "ops") + values = sidecar_config_to_env(raw) + if shell: + for key, value in values.items(): + typer.echo(f"export {key}={shlex.quote(value)}") + return + typer.echo(json.dumps(values, ensure_ascii=False, indent=2)) + + +def _prompt_harness_update(info: dict) -> bool: + version = info.get("version") + version_label = f"v{version}" if version is not None else "unknown" + return typer.confirm( + f"Harness '{info['name']}' already exists (current version " + f"{version_label}). Update it to a new version?", + default=False, + ) + + +@sidecar_app.command("deploy") +def deploy( + name: Annotated[str, typer.Argument(help="Harness spec name.")], + region: Annotated[str | None, typer.Option("--region")] = None, + access_key: Annotated[str | None, typer.Option("--volcengine-access-key")] = None, + secret_key: Annotated[str | None, typer.Option("--volcengine-secret-key")] = None, + discovery_url: Annotated[str | None, typer.Option("--discovery-url")] = None, + allowed_id: Annotated[str | None, typer.Option("--allowed-id")] = None, + runtime_network_mode: Annotated[ + str | None, typer.Option("--runtime-network-mode") + ] = None, + runtime_vpc_id: Annotated[str | None, typer.Option("--runtime-vpc-id")] = None, + runtime_subnet_ids: Annotated[ + list[str] | None, + typer.Option("--runtime-subnet-id", "--runtime-subnet-ids"), + ] = None, + runtime_enable_shared_internet_access: Annotated[ + bool | None, + typer.Option( + "--runtime-enable-shared-internet-access/--no-runtime-enable-shared-internet-access" + ), + ] = None, + yes: Annotated[bool, typer.Option("--yes", "-y")] = False, +) -> None: + """Deploy a Sidecar-enabled Harness with optional Runtime VPC settings.""" + + from agentkit.toolkit.cli.console_reporter import ConsoleReporter + from agentkit.toolkit.context import ExecutionContext + + reporter = ConsoleReporter() + ExecutionContext.set_reporter(reporter) + on_conflict = None + if yes: + on_conflict = lambda info: True # noqa: E731 + elif sys.stdin.isatty(): + on_conflict = _prompt_harness_update + + try: + result = deploy_harness( + name=name, + region=region, + access_key=access_key, + secret_key=secret_key, + discovery_url=discovery_url, + allowed_id=allowed_id, + runtime_network_mode=runtime_network_mode, + runtime_vpc_id=runtime_vpc_id, + runtime_subnet_ids=runtime_subnet_ids, + runtime_enable_shared_internet_access=( + runtime_enable_shared_internet_access + ), + reporter=reporter, + on_conflict=on_conflict, + ) + except HarnessDeployAborted: + typer.echo("Harness deploy cancelled.") + return + except (ValueError, FileNotFoundError, NotADirectoryError) as error: + typer.echo(f"Harness deploy failed: {error}", err=True) + raise typer.Exit(1) from error + + if not result.success: + typer.echo(f"Harness deploy failed: {result.error}", err=True) + raise typer.Exit(1) + + deploy_result = result.deploy_result + metadata = (deploy_result and deploy_result.metadata) or {} + typer.echo(f"Harness Runtime deployed: {metadata.get('runtime_id', name)}") + + +__all__ = ["harness_app", "sidecar_app"] diff --git a/agentkit/extensions/harness_sidecar/component_catalog.py b/agentkit/extensions/harness_sidecar/component_catalog.py new file mode 100644 index 00000000..be7b3164 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/component_catalog.py @@ -0,0 +1,232 @@ +"""Stable Product Component Catalog for Harness Sidecar control planes.""" + +from __future__ import annotations + +from collections.abc import Mapping +from copy import deepcopy +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + +from .profiles import PROFILE_DEFAULTS, PROFILE_METADATA, profile_default_components + + +CATALOG_SCHEMA_VERSION = "agentkit.harness-sidecar.catalog/v1" +CATALOG_VERSION = "2026.07.1" +PRODUCT_COMPONENT_ORDER = ( + "context_engine", + "compressor", + "verifier", + "long_run_control", + "mcp_resilience", + "sql_readonly", + "browser", + "evaluation", + "shadow", +) + + +class CatalogModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class ComponentAvailability(CatalogModel): + available: bool + status: str + reason: str | None = None + min_runtime_version: str | None = None + regions: list[str] = Field(default_factory=list) + + +class HarnessComponentDefinition(CatalogModel): + id: str + display_name: str + description: str + category: str + dependencies: list[str] = Field(default_factory=list) + risk_level: str = "standard" + status: str + settings_schema_ref: str | None = None + selected_by_profile: bool = False + availability: ComponentAvailability + + +class HarnessProfileDefinition(CatalogModel): + id: str + display_name: str + description: str + default_components: list[str] + profile_component_count: int + + +class HarnessSidecarCatalog(CatalogModel): + schema_version: str = CATALOG_SCHEMA_VERSION + catalog_version: str = CATALOG_VERSION + profile_count: int + profiles: list[HarnessProfileDefinition] + selected_profile: HarnessProfileDefinition + components: list[HarnessComponentDefinition] + total_component_count: int + selectable_component_count: int + + +_COMPONENT_REGISTRY: dict[str, dict[str, Any]] = { + "context_engine": { + "display_name": "上下文治理", + "description": "治理上下文组装、任务锚定和上下文预算。", + "category": "model", + }, + "compressor": { + "display_name": "上下文与结果压缩", + "description": "压缩长上下文和大型工具结果,降低 Token 成本。", + "category": "model", + }, + "verifier": { + "display_name": "回答校验与修复", + "description": "校验证据和回答,在失败时执行修复或告警。", + "category": "model", + }, + "long_run_control": { + "display_name": "长任务控制", + "description": "管理长任务进度、续跑和结束条件。", + "category": "model", + }, + "mcp_resilience": { + "display_name": "MCP 稳定性治理", + "description": "治理连接、超时、空结果、大返回、预算和结构化错误。", + "category": "tool", + }, + "sql_readonly": { + "display_name": "SQL 只读保护", + "description": "拦截写 SQL,并对所选 MCP 路由提供只读保护。", + "category": "security", + "dependencies": ["mcp_resilience"], + "risk_level": "high_when_disabled", + }, + "browser": { + "display_name": "浏览器任务增强", + "description": "增强浏览器动作、结果、grounding 和 trace。", + "category": "browser", + "availability": { + "available": False, + "status": "preview", + "reason": "当前 Runtime 尚未提供 Browser Controller", + }, + }, + "evaluation": { + "display_name": "在线评测", + "description": "提供评测采样、指标和回归数据。", + "category": "quality", + "dependencies": ["browser"], + "availability": { + "available": False, + "status": "preview", + "reason": "当前 Runtime 尚未提供 Evaluation Controller", + }, + }, + "shadow": { + "display_name": "Shadow 对照运行", + "description": "执行不影响主链路的影子运行和结果对照。", + "category": "quality", + "dependencies": ["evaluation"], + "availability": { + "available": False, + "status": "preview", + "reason": "当前 Runtime 尚未提供 Shadow Controller", + }, + }, +} + + +def get_harness_sidecar_catalog( + profile: str = "ops", + *, + availability_overrides: Mapping[str, ComponentAvailability | Mapping[str, Any]] + | None = None, +) -> HarnessSidecarCatalog: + """Return a Runtime-independent Product Component Catalog snapshot.""" + + selected_ids = set(profile_default_components(profile)) + overrides = dict(availability_overrides or {}) + unknown = sorted(set(overrides) - set(PRODUCT_COMPONENT_ORDER)) + if unknown: + raise ValueError( + "Unknown Harness Product Component availability override: " + + ", ".join(unknown) + ) + + profiles = [_profile_definition(profile_id) for profile_id in PROFILE_DEFAULTS] + selected_profile = next(item for item in profiles if item.id == profile) + components = [ + _component_definition( + component_id, + selected=component_id in selected_ids, + availability_override=overrides.get(component_id), + ) + for component_id in PRODUCT_COMPONENT_ORDER + ] + return HarnessSidecarCatalog( + profile_count=len(profiles), + profiles=profiles, + selected_profile=selected_profile, + components=components, + total_component_count=len(components), + selectable_component_count=sum( + component.availability.available for component in components + ), + ) + + +def _profile_definition(profile: str) -> HarnessProfileDefinition: + components = list(profile_default_components(profile)) + metadata = PROFILE_METADATA[profile] + return HarnessProfileDefinition( + id=profile, + display_name=metadata["display_name"], + description=metadata["description"], + default_components=components, + profile_component_count=len(components), + ) + + +def _component_definition( + component_id: str, + *, + selected: bool, + availability_override: ComponentAvailability | Mapping[str, Any] | None, +) -> HarnessComponentDefinition: + definition = deepcopy(_COMPONENT_REGISTRY[component_id]) + availability_value = definition.pop( + "availability", {"available": True, "status": "ga"} + ) + if availability_override is not None: + override = ( + availability_override.model_dump(exclude_unset=True) + if isinstance(availability_override, ComponentAvailability) + else dict(availability_override) + ) + availability_value.update(override) + if override.get("available") is True and "reason" not in override: + availability_value["reason"] = None + availability = ComponentAvailability.model_validate(availability_value) + return HarnessComponentDefinition( + id=component_id, + dependencies=list(definition.pop("dependencies", [])), + risk_level=definition.pop("risk_level", "standard"), + status=availability.status, + selected_by_profile=selected, + availability=availability, + **definition, + ) + + +__all__ = [ + "CATALOG_SCHEMA_VERSION", + "CATALOG_VERSION", + "PRODUCT_COMPONENT_ORDER", + "ComponentAvailability", + "HarnessComponentDefinition", + "HarnessProfileDefinition", + "HarnessSidecarCatalog", + "get_harness_sidecar_catalog", +] diff --git a/agentkit/extensions/harness_sidecar/deploy.py b/agentkit/extensions/harness_sidecar/deploy.py new file mode 100644 index 00000000..e5370163 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/deploy.py @@ -0,0 +1,121 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Harness deploy integration, including opt-in Runtime VPC configuration.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from pathlib import Path +from typing import Any + +from agentkit.toolkit.harness.deploy import deploy_harness as _deploy_harness +from agentkit.toolkit.harness.env_mapping import to_runtime_env as _base_to_runtime_env +from agentkit.toolkit.models import LifecycleResult +from agentkit.toolkit.reporter import Reporter + +from .sidecar_config import sidecar_config_to_env + + +def to_runtime_env(spec: dict[str, Any]) -> dict[str, str]: + """Map a Harness spec without leaking Sidecar config into generic env keys.""" + + sanitized = dict(spec) + sidecar_section = sanitized.pop("sidecar", None) + sidecar_profile = sanitized.get("profile") + harness_section = sanitized.get("harness") + if isinstance(harness_section, Mapping): + if sidecar_section is None: + sidecar_section = harness_section.get("sidecar") + sidecar_profile = sidecar_profile or harness_section.get("profile") + remaining_harness = dict(harness_section) + remaining_harness.pop("sidecar", None) + if remaining_harness: + sanitized["harness"] = remaining_harness + else: + sanitized.pop("harness", None) + + env = _base_to_runtime_env(sanitized) + if isinstance(sidecar_section, (Mapping, bool)): + env.update( + sidecar_config_to_env( + sidecar_section, + profile=str(sidecar_profile or "default"), + ) + ) + return env + + +def build_runtime_network( + *, + mode: str | None = None, + vpc_id: str | None = None, + subnet_ids: list[str] | None = None, + enable_shared_internet_access: bool | None = None, +) -> dict[str, Any]: + """Build the cloud runner's Runtime network block when explicitly requested.""" + + network: dict[str, Any] = {} + if mode is not None: + network["mode"] = mode + if vpc_id is not None: + network["vpc_id"] = vpc_id + if subnet_ids: + network["subnet_ids"] = subnet_ids + if enable_shared_internet_access is not None: + network["enable_shared_internet_access"] = enable_shared_internet_access + return network + + +def deploy_harness( + name: str, + path: str | Path = ".", + *, + region: str | None = None, + access_key: str | None = None, + secret_key: str | None = None, + discovery_url: str | None = None, + allowed_id: str | None = None, + runtime_network_mode: str | None = None, + runtime_vpc_id: str | None = None, + runtime_subnet_ids: list[str] | None = None, + runtime_enable_shared_internet_access: bool | None = None, + reporter: Reporter | None = None, + on_conflict: Callable[[dict[str, Any]], bool] | None = None, +) -> LifecycleResult: + """Deploy a Sidecar-enabled Harness with optional Runtime networking.""" + + runtime_network = build_runtime_network( + mode=runtime_network_mode, + vpc_id=runtime_vpc_id, + subnet_ids=runtime_subnet_ids, + enable_shared_internet_access=runtime_enable_shared_internet_access, + ) + cloud_overrides = {"runtime_network": runtime_network} if runtime_network else None + return _deploy_harness( + name=name, + path=path, + region=region, + access_key=access_key, + secret_key=secret_key, + discovery_url=discovery_url, + allowed_id=allowed_id, + runtime_env_builder=to_runtime_env, + cloud_config_overrides=cloud_overrides, + reporter=reporter, + on_conflict=on_conflict, + ) + + +__all__ = ["build_runtime_network", "deploy_harness", "to_runtime_env"] diff --git a/agentkit/extensions/harness_sidecar/failover_proxy.py b/agentkit/extensions/harness_sidecar/failover_proxy.py new file mode 100644 index 00000000..8a0e29e4 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/failover_proxy.py @@ -0,0 +1,162 @@ +"""Stable localhost HTTP relay for Sidecar-to-direct-path failover.""" + +from __future__ import annotations + +import json +import threading +import urllib.error +import urllib.parse +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +_HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +} + + +class StableHttpRelay: + """Expose one stable URL and atomically switch its upstream target.""" + + def __init__(self, active_url: str, fallback_url: str) -> None: + self._active = _validated_base_url(active_url) + self._fallback = _validated_base_url(fallback_url) + self._source_path = urllib.parse.urlsplit(self._active).path.rstrip("/") + self._target = self._active + self._target_lock = threading.Lock() + self._server = ThreadingHTTPServer(("127.0.0.1", 0), self._handler()) + self._server.daemon_threads = True + self._thread = threading.Thread( + target=self._server.serve_forever, + name="agentkit-harness-direct-failover", + daemon=True, + ) + self._thread.start() + + @property + def url(self) -> str: + host, port = self._server.server_address[:2] + return f"http://{host}:{port}{self._source_path}" + + def activate_fallback(self) -> None: + with self._target_lock: + self._target = self._fallback + + def close(self) -> None: + self._server.shutdown() + self._server.server_close() + if threading.current_thread() is not self._thread: + self._thread.join(timeout=5) + + def _target_url(self, request_path: str) -> str: + incoming = urllib.parse.urlsplit(request_path) + request_base = self._source_path + if incoming.path == request_base: + suffix = "" + elif request_base and incoming.path.startswith(f"{request_base}/"): + suffix = incoming.path[len(request_base) :] + else: + suffix = incoming.path + with self._target_lock: + target = urllib.parse.urlsplit(self._target) + target_path = target.path.rstrip("/") + suffix + query = "&".join(value for value in (target.query, incoming.query) if value) + return urllib.parse.urlunsplit( + (target.scheme, target.netloc, target_path or "/", query, "") + ) + + def _handler(self) -> type[BaseHTTPRequestHandler]: + relay = self + + class RelayHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *_args: object) -> None: + return + + def do_GET(self) -> None: # noqa: N802 + self._forward() + + def do_POST(self) -> None: # noqa: N802 + self._forward() + + def do_PUT(self) -> None: # noqa: N802 + self._forward() + + def do_PATCH(self) -> None: # noqa: N802 + self._forward() + + def do_DELETE(self) -> None: # noqa: N802 + self._forward() + + def do_OPTIONS(self) -> None: # noqa: N802 + self._forward() + + def do_HEAD(self) -> None: # noqa: N802 + self._forward() + + def _forward(self) -> None: + content_length = int(self.headers.get("Content-Length", "0") or 0) + body = self.rfile.read(content_length) if content_length else None + headers = { + name: value + for name, value in self.headers.items() + if name.lower() not in _HOP_BY_HOP_HEADERS + and name.lower() not in {"host", "content-length"} + } + request = urllib.request.Request( + relay._target_url(self.path), + data=body, + headers=headers, + method=self.command, + ) + try: + response = urllib.request.urlopen(request, timeout=60) + except urllib.error.HTTPError as error: + response = error + except (OSError, urllib.error.URLError): + payload = json.dumps( + {"status": "error", "error": "direct_upstream_unavailable"} + ).encode("utf-8") + self.send_response(502) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Connection", "close") + self.end_headers() + if self.command != "HEAD": + self.wfile.write(payload) + self.close_connection = True + return + + with response: + self.send_response(response.getcode()) + for name, value in response.headers.items(): + if name.lower() not in _HOP_BY_HOP_HEADERS: + self.send_header(name, value) + self.send_header("Connection", "close") + self.end_headers() + if self.command != "HEAD": + while chunk := response.read(64 * 1024): + self.wfile.write(chunk) + self.wfile.flush() + self.close_connection = True + + return RelayHandler + + +def _validated_base_url(value: str) -> str: + parsed = urllib.parse.urlsplit(str(value).strip()) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise ValueError("failover upstream must be an HTTP(S) URL") + return urllib.parse.urlunsplit( + (parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), parsed.query, "") + ) + + +__all__ = ["StableHttpRelay"] diff --git a/agentkit/extensions/harness_sidecar/profiles.py b/agentkit/extensions/harness_sidecar/profiles.py new file mode 100644 index 00000000..f98a3387 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/profiles.py @@ -0,0 +1,125 @@ +"""Customer-facing Harness Sidecar product profiles.""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Any, Mapping + + +PROFILE_DEFAULTS: dict[str, tuple[str, ...]] = { + "default": ( + "context_engine", + "compressor", + "verifier", + "long_run_control", + ), + "ops": ( + "context_engine", + "compressor", + "verifier", + "long_run_control", + "mcp_resilience", + "sql_readonly", + ), +} + +PROFILE_METADATA: dict[str, dict[str, str]] = { + "default": { + "display_name": "通用增强", + "description": "适用于通用对话、知识问答和常规 Agent 的默认增强能力。", + }, + "ops": { + "display_name": "运维增强", + "description": "面向运维诊断、数据库、日志和监控 MCP 的安全增强能力。", + }, +} + + +def profile_default_components(profile: str) -> tuple[str, ...]: + try: + return PROFILE_DEFAULTS[profile] + except KeyError as error: + raise ValueError( + f"Unknown Harness Sidecar profile '{profile}'. " + f"Known profiles: {', '.join(sorted(PROFILE_DEFAULTS))}" + ) from error + + +def expand_sidecar_profile( + profile: str, overrides: Mapping[str, Any] | None = None +) -> dict[str, Any]: + """Expand product selections into the legacy technical config contract.""" + + raw = deepcopy(dict(overrides or {})) + enabled = bool(raw.get("enabled", True)) + component_overrides = raw.get("component_overrides") or {} + + # Import lazily so the Product Catalog can import the profile registry without + # introducing a module cycle. + from .selection import resolve_harness_sidecar_selection + + plan = resolve_harness_sidecar_selection( + enabled=enabled, + profile=profile, + component_overrides=component_overrides, + catalog_version=raw.get("catalog_version"), + runtime_version=raw.get("runtime_version"), + ) + if plan.errors: + raise ValueError("; ".join(plan.errors)) + + selected = set(plan.effective_components) + model_components = [ + component for component in PROFILE_DEFAULTS["default"] if component in selected + ] + mcp_enabled = bool({"mcp_resilience", "sql_readonly"} & selected) + defaults: dict[str, Any] = { + "enabled": enabled, + "model_proxy": { + "enabled": bool(model_components), + "components": model_components, + "compression_provider": "noop", + "fail_open": True, + }, + "mcp_gateway": { + "enabled": mcp_enabled, + "fail_open": True, + "readonly_segments": ["*"] if "sql_readonly" in selected else [], + "presets": ["sql_readonly"] if "sql_readonly" in selected else [], + "policy": _mcp_policy() if "mcp_resilience" in selected else {}, + }, + } + _deep_merge(defaults, raw) + defaults["profile"] = profile + return defaults + + +def _mcp_policy() -> dict[str, Any]: + return { + "result_quality": { + "empty_is_unhealthy": True, + "max_consecutive_empty": 2, + }, + "large_result": {"max_bytes": 50_000}, + "budget": { + "max_calls_per_session": 70, + "session_idle_reset_seconds": 300.0, + }, + } + + +def _deep_merge(target: dict[str, Any], overrides: Mapping[str, Any]) -> None: + for key, value in overrides.items(): + current = target.get(key) + if isinstance(current, dict) and isinstance(value, Mapping): + _deep_merge(current, value) + else: + target[key] = deepcopy(value) + + +__all__ = [ + "PROFILE_DEFAULTS", + "PROFILE_METADATA", + "expand_sidecar_profile", + "profile_default_components", +] diff --git a/agentkit/extensions/harness_sidecar/runtime_components.py b/agentkit/extensions/harness_sidecar/runtime_components.py new file mode 100644 index 00000000..0b4badff --- /dev/null +++ b/agentkit/extensions/harness_sidecar/runtime_components.py @@ -0,0 +1,111 @@ +"""Resolve public Sidecar settings to private Runtime module requirements.""" + +from __future__ import annotations + +from collections.abc import Iterable + +# Internal artifact inventory. Product Catalog responses must never expose these +# module names as customer-selectable components. +RUNTIME_COMPONENT_ORDER = ( + "harness_core", + "ops", + "goal_runtime", + "model_proxy", + "mcp_gateway", + "browser_runtime", + "eval_runtime", + "shadow_runtime", +) +RUNTIME_COMPONENT_DEPENDENCIES = { + "harness_core": (), + "ops": ("harness_core",), + "goal_runtime": ("harness_core",), + "model_proxy": ("harness_core",), + "mcp_gateway": ("harness_core",), + "browser_runtime": ("harness_core",), + "eval_runtime": ("harness_core",), + "shadow_runtime": ("harness_core",), +} +RUNTIME_COMPONENT_ALIASES = { + "core": "harness_core", + "harness": "harness_core", + "harness_core": "harness_core", + "runtime": "harness_core", + "runtime_core": "harness_core", + "model": "model_proxy", + "model_proxy": "model_proxy", + "mcp": "mcp_gateway", + "mcp_gateway": "mcp_gateway", + "ops": "ops", + "ops_kernel": "ops", + "goal": "goal_runtime", + "goal_loop": "goal_runtime", + "goal_runtime": "goal_runtime", + "browser": "browser_runtime", + "browser_runtime": "browser_runtime", + "eval": "eval_runtime", + "evaluation": "eval_runtime", + "eval_runtime": "eval_runtime", + "shadow": "shadow_runtime", + "shadow_runtime": "shadow_runtime", +} +RUNTIME_FLAVOR = "harness-sidecar" +RUNTIME_FLAVOR_CORE = RUNTIME_FLAVOR +RUNTIME_FLAVOR_OPS = RUNTIME_FLAVOR +RUNTIME_FLAVOR_FULL = RUNTIME_FLAVOR +RUNTIME_FLAVOR_CUSTOM = RUNTIME_FLAVOR + + +def normalize_runtime_component(value: str) -> str: + name = value.strip().lower().replace("-", "_") + try: + return RUNTIME_COMPONENT_ALIASES[name] + except KeyError as error: + known = ", ".join(RUNTIME_COMPONENT_ORDER) + raise ValueError( + f"Unknown Harness Runtime component '{value}'. Known components: {known}" + ) from error + + +def resolve_runtime_components( + *, + model_proxy_enabled: bool, + mcp_gateway_enabled: bool, + components: Iterable[str] = (), +) -> list[str]: + requested = ["harness_core"] + if model_proxy_enabled: + requested.append("model_proxy") + if mcp_gateway_enabled: + requested.append("mcp_gateway") + requested.extend(normalize_runtime_component(item) for item in components) + selected: set[str] = set() + + def include(component: str) -> None: + if component in selected: + return + for dependency in RUNTIME_COMPONENT_DEPENDENCIES[component]: + include(dependency) + selected.add(component) + + for component in requested: + include(component) + return [component for component in RUNTIME_COMPONENT_ORDER if component in selected] + + +def runtime_flavor_for_components(components: Iterable[str]) -> str: + return RUNTIME_FLAVOR + + +__all__ = [ + "RUNTIME_COMPONENT_DEPENDENCIES", + "RUNTIME_COMPONENT_ORDER", + "RUNTIME_FLAVOR", + "RUNTIME_FLAVOR_CORE", + "RUNTIME_FLAVOR_CUSTOM", + "RUNTIME_FLAVOR_FULL", + "RUNTIME_FLAVOR_OPS", + "normalize_runtime_component", + "resolve_runtime_components", + "runtime_flavor_for_components", +] diff --git a/agentkit/extensions/harness_sidecar/selection.py b/agentkit/extensions/harness_sidecar/selection.py new file mode 100644 index 00000000..a719d9d2 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/selection.py @@ -0,0 +1,293 @@ +"""Deterministic Harness Sidecar Product Component selection resolver.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + +from .component_catalog import ( + CATALOG_VERSION, + PRODUCT_COMPONENT_ORDER, + ComponentAvailability, + get_harness_sidecar_catalog, +) +from .profiles import PROFILE_DEFAULTS +from .runtime_components import resolve_runtime_components + + +PLAN_SCHEMA_VERSION = "agentkit.harness-sidecar.plan/v1" +_MODEL_COMPONENTS = ( + "context_engine", + "compressor", + "verifier", + "long_run_control", +) +_VEADK_PLUGIN_TARGETS = { + "context_engine": "invocation_context", + "compressor": "compactor", + "verifier": "response_verification", + "long_run_control": "long_run_control", +} +_OPTIONAL_RUNTIME_TARGETS = { + "long_run_control": "goal_runtime", + "browser": "browser_runtime", + "evaluation": "eval_runtime", + "shadow": "shadow_runtime", +} + + +class SelectionModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class HarnessSelectionIntent(SelectionModel): + enabled: bool = True + profile: str = "ops" + component_overrides: dict[str, bool] = Field(default_factory=dict) + catalog_version: str | None = None + runtime_version: str | None = None + + +class AutoAddedComponent(SelectionModel): + id: str + required_by: list[str] + + +class ModelProxyActivation(SelectionModel): + enabled: bool + components: list[str] = Field(default_factory=list) + + +class MCPGatewayActivation(SelectionModel): + enabled: bool + presets: list[str] = Field(default_factory=list) + readonly_segments: list[str] = Field(default_factory=list) + + +class HarnessActivationTargets(SelectionModel): + veadk_plugins: list[str] = Field(default_factory=list) + model_proxy: ModelProxyActivation + mcp_gateway: MCPGatewayActivation + runtime_components: list[str] = Field(default_factory=list) + + +class ResolvedHarnessPlan(SelectionModel): + schema_version: str = PLAN_SCHEMA_VERSION + valid: bool + enabled: bool + profile: str + requested_components: list[str] = Field(default_factory=list) + effective_components: list[str] = Field(default_factory=list) + auto_added_components: list[AutoAddedComponent] = Field(default_factory=list) + profile_component_count: int + total_component_count: int + selectable_component_count: int + effective_component_count: int + activation_targets: HarnessActivationTargets + warnings: list[str] = Field(default_factory=list) + errors: list[str] = Field(default_factory=list) + catalog_version: str = CATALOG_VERSION + runtime_version: str | None = None + plan_hash: str + + +def resolve_harness_sidecar_selection( + *, + enabled: bool = True, + profile: str = "ops", + component_overrides: Mapping[str, bool] | None = None, + catalog_version: str | None = None, + runtime_version: str | None = None, + availability_overrides: Mapping[str, ComponentAvailability | Mapping[str, Any]] + | None = None, +) -> ResolvedHarnessPlan: + """Resolve a profile and explicit overrides into an immutable plan snapshot.""" + + errors: list[str] = [] + warnings: list[str] = [] + overrides = dict(component_overrides or {}) + unknown_components = sorted(set(overrides) - set(PRODUCT_COMPONENT_ORDER)) + if unknown_components: + errors.append( + "Unknown Harness Product Component override: " + + ", ".join(unknown_components) + ) + invalid_values = sorted( + component_id + for component_id, value in overrides.items() + if not isinstance(value, bool) + ) + if invalid_values: + errors.append( + "Harness Product Component overrides must be boolean: " + + ", ".join(invalid_values) + ) + if catalog_version is not None and catalog_version != CATALOG_VERSION: + errors.append( + f"Unsupported Harness Catalog version '{catalog_version}'; " + f"expected '{CATALOG_VERSION}'" + ) + + if profile in PROFILE_DEFAULTS: + catalog = get_harness_sidecar_catalog( + profile, availability_overrides=availability_overrides + ) + profile_defaults = set(catalog.selected_profile.default_components) + profile_component_count = catalog.selected_profile.profile_component_count + else: + errors.append( + f"Unknown Harness Sidecar profile '{profile}'. " + f"Known profiles: {', '.join(sorted(PROFILE_DEFAULTS))}" + ) + catalog = get_harness_sidecar_catalog( + "default", availability_overrides=availability_overrides + ) + profile_defaults = set() + profile_component_count = 0 + + requested: set[str] = set() + if enabled: + requested.update(profile_defaults) + for component_id, selected in overrides.items(): + if component_id not in PRODUCT_COMPONENT_ORDER or not isinstance( + selected, bool + ): + continue + if selected: + requested.add(component_id) + else: + requested.discard(component_id) + + effective = set(requested) + required_by: dict[str, set[str]] = {} + definitions = {component.id: component for component in catalog.components} + + def include_dependencies(component_id: str) -> None: + for dependency in definitions[component_id].dependencies: + required_by.setdefault(dependency, set()).add(component_id) + if dependency not in effective: + effective.add(dependency) + include_dependencies(dependency) + + for component_id in _ordered(requested): + include_dependencies(component_id) + + if enabled and not effective: + errors.append("Enabled Harness Sidecar must resolve at least one component") + + for component_id in _ordered(effective): + availability = definitions[component_id].availability + if not availability.available: + detail = f": {availability.reason}" if availability.reason else "" + errors.append( + f"Harness Product Component '{component_id}' is unavailable{detail}" + ) + + if enabled and profile == "ops" and overrides.get("sql_readonly") is False: + warnings.append("SQL readonly protection is disabled for the ops profile") + + requested_ordered = _ordered(requested) + effective_ordered = _ordered(effective) + auto_added = [ + AutoAddedComponent( + id=component_id, + required_by=_ordered(required_by.get(component_id, set())), + ) + for component_id in effective_ordered + if component_id not in requested + ] + activation_targets = _activation_targets(enabled, profile, effective_ordered) + plan_values: dict[str, Any] = { + "valid": not errors, + "enabled": enabled, + "profile": profile, + "requested_components": requested_ordered, + "effective_components": effective_ordered, + "auto_added_components": auto_added, + "profile_component_count": profile_component_count, + "total_component_count": catalog.total_component_count, + "selectable_component_count": catalog.selectable_component_count, + "effective_component_count": len(effective_ordered), + "activation_targets": activation_targets, + "warnings": warnings, + "errors": errors, + "catalog_version": CATALOG_VERSION, + "runtime_version": runtime_version, + } + plan_values["plan_hash"] = _plan_hash(plan_values) + return ResolvedHarnessPlan.model_validate(plan_values) + + +def _activation_targets( + enabled: bool, profile: str, effective_components: list[str] +) -> HarnessActivationTargets: + selected = set(effective_components) if enabled else set() + model_components = [item for item in _MODEL_COMPONENTS if item in selected] + mcp_enabled = bool({"mcp_resilience", "sql_readonly"} & selected) + optional_runtime = (["ops"] if profile == "ops" and selected else []) + [ + runtime_component + for product_component, runtime_component in _OPTIONAL_RUNTIME_TARGETS.items() + if product_component in selected + ] + runtime_components = ( + resolve_runtime_components( + model_proxy_enabled=bool(model_components), + mcp_gateway_enabled=mcp_enabled, + components=optional_runtime, + ) + if selected + else [] + ) + return HarnessActivationTargets( + veadk_plugins=[ + _VEADK_PLUGIN_TARGETS[item] + for item in _MODEL_COMPONENTS + if item in selected + ], + model_proxy=ModelProxyActivation( + enabled=bool(model_components), components=model_components + ), + mcp_gateway=MCPGatewayActivation( + enabled=mcp_enabled, + presets=["sql_readonly"] if "sql_readonly" in selected else [], + readonly_segments=["*"] if "sql_readonly" in selected else [], + ), + runtime_components=runtime_components, + ) + + +def _ordered(values: set[str]) -> list[str]: + return [item for item in PRODUCT_COMPONENT_ORDER if item in values] + + +def _plan_hash(values: Mapping[str, Any]) -> str: + payload = json.dumps( + dict(values), + default=_json_default, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + return f"sha256:{hashlib.sha256(payload).hexdigest()}" + + +def _json_default(value: Any) -> Any: + if isinstance(value, BaseModel): + return value.model_dump(mode="json") + raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") + + +__all__ = [ + "PLAN_SCHEMA_VERSION", + "AutoAddedComponent", + "HarnessActivationTargets", + "HarnessSelectionIntent", + "MCPGatewayActivation", + "ModelProxyActivation", + "ResolvedHarnessPlan", + "resolve_harness_sidecar_selection", +] diff --git a/agentkit/extensions/harness_sidecar/sidecar.py b/agentkit/extensions/harness_sidecar/sidecar.py new file mode 100644 index 00000000..b94732e0 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/sidecar.py @@ -0,0 +1,445 @@ +"""Public lifecycle wrapper for the private Harness Sidecar runtime artifact.""" + +from __future__ import annotations + +import atexit +import json +import os +import queue +import shlex +import shutil +import subprocess +import tempfile +import threading +import warnings +from collections.abc import Mapping, MutableMapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from .failover_proxy import StableHttpRelay +from .sidecar_config import ( + HarnessSidecarConfig, + SidecarBindingSpec, + resolve_sidecar_config, +) + + +RUNTIME_EXECUTABLE = "agentkit-harness-sidecar-runtime" +INSTALL_HINT = "run inside an AgentKit managed cloud runtime" + + +class HarnessSidecarError(RuntimeError): + pass + + +class HarnessSidecarRuntimeUnavailable(HarnessSidecarError): + pass + + +@dataclass +class SidecarBinding: + config: HarnessSidecarConfig + spec: SidecarBindingSpec + process: subprocess.Popen[str] | None = None + config_path: Path | None = None + stderr_lines: list[str] = field(default_factory=list) + _environ: MutableMapping[str, str] | None = field(default=None, repr=False) + _previous_env: dict[str, str | None] = field(default_factory=dict, repr=False) + _relays: list[StableHttpRelay] = field(default_factory=list, repr=False) + _relay_env_keys: set[str] = field(default_factory=set, repr=False) + _state_lock: threading.RLock = field(default_factory=threading.RLock, repr=False) + _monitor_thread: threading.Thread | None = field(default=None, repr=False) + _runtime_exit_code: int | None = field(default=None, init=False, repr=False) + _stopped: bool = field(default=False, init=False, repr=False) + + @property + def env(self) -> dict[str, str]: + return dict(self.spec.env) + + def apply_env( + self, environ: MutableMapping[str, str] | None = None + ) -> dict[str, str]: + target = environ if environ is not None else os.environ + if self._environ is None: + self._environ = target + self._previous_env = {key: target.get(key) for key in self.spec.env} + for key, value in self.spec.env.items(): + target[key] = value + return self.env + + def stop(self) -> None: + with self._state_lock: + if self._stopped: + return + self._stopped = True + if self.process is not None and self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait(timeout=5) + if self.config_path is not None: + self.config_path.unlink(missing_ok=True) + for relay in self._relays: + relay.close() + self._relays.clear() + if self._environ is not None: + for key, previous in self._previous_env.items(): + if self._environ.get(key) != self.spec.env.get(key): + continue + if previous is None: + self._environ.pop(key, None) + else: + self._environ[key] = previous + self._environ = None + + def _configure_failover_relays(self, runtime_env: Mapping[str, str]) -> None: + try: + model_upstream = ( + self.config.model_proxy.upstream_base_url + or runtime_env.get(self.config.model_proxy.upstream_base_url_env) + ) + if self.spec.model_proxy_url and model_upstream: + relay = StableHttpRelay(self.spec.model_proxy_url, model_upstream) + self._relays.append(relay) + self.spec.model_proxy_url = relay.url + if "MODEL_AGENT_API_BASE" in self.spec.env: + self.spec.env["MODEL_AGENT_API_BASE"] = relay.url + self._relay_env_keys.add("MODEL_AGENT_API_BASE") + + mcp_upstreams = list(self.config.mcp_gateway.upstreams) + if not mcp_upstreams: + mcp_upstreams = _csv_urls( + runtime_env.get(self.config.mcp_gateway.upstreams_env) + ) + if self.spec.mcp_urls and len(self.spec.mcp_urls) == len(mcp_upstreams): + relay_urls: list[str] = [] + for sidecar_url, direct_url in zip( + self.spec.mcp_urls, mcp_upstreams, strict=True + ): + relay = StableHttpRelay(sidecar_url, direct_url) + self._relays.append(relay) + relay_urls.append(relay.url) + self.spec.mcp_urls = relay_urls + if "MCP_URLS" in self.spec.env: + self.spec.env["MCP_URLS"] = ",".join(relay_urls) + self._relay_env_keys.add("MCP_URLS") + except Exception: + for relay in self._relays: + relay.close() + self._relays.clear() + self._relay_env_keys.clear() + raise + + def _start_runtime_monitor(self) -> None: + if self.process is None: + return + + def monitor() -> None: + exit_code = self.process.wait() + with self._state_lock: + if self._stopped: + return + self._runtime_exit_code = exit_code + for relay in self._relays: + relay.activate_fallback() + self.spec.status = "degraded" + self.spec.diagnostics.append( + { + "component": "harness_sidecar", + "status": "degraded", + "message": "Sidecar exited; direct upstream fallback is active", + } + ) + self._restore_nonrelay_env() + if self.config_path is not None: + self.config_path.unlink(missing_ok=True) + + self._monitor_thread = threading.Thread( + target=monitor, + name="harness-sidecar-runtime-monitor", + daemon=True, + ) + self._monitor_thread.start() + + def _restore_nonrelay_env(self) -> None: + for key, previous in self._previous_env.items(): + if key in self._relay_env_keys: + continue + current = self.spec.env.get(key) + if self._environ is not None and self._environ.get(key) == current: + if previous is None: + self._environ.pop(key, None) + else: + self._environ[key] = previous + if previous is None: + self.spec.env.pop(key, None) + else: + self.spec.env[key] = previous + + def __enter__(self) -> "SidecarBinding": + return self + + def __exit__(self, *_args: Any) -> None: + self.stop() + + +def start_harness_sidecar( + config: HarnessSidecarConfig | Mapping[str, Any] | bool | None = None, + *, + profile: str | None = None, + apply_env: bool = False, + environ: MutableMapping[str, str] | None = None, + process_env: Mapping[str, str] | None = None, +) -> SidecarBinding: + resolved = resolve_sidecar_config(config, profile=profile) + if not resolved.enabled: + return SidecarBinding( + config=resolved, + spec=SidecarBindingSpec(status="disabled", profile=resolved.profile), + ) + runtime_env = dict( + process_env + if process_env is not None + else (environ if environ is not None else os.environ) + ) + stderr_lines: list[str] = [] + config_path: Path | None = None + process: subprocess.Popen[str] | None = None + binding: SidecarBinding | None = None + try: + command = _resolve_runtime_command(resolved, runtime_env) + config_path = _write_runtime_config(resolved) + process = subprocess.Popen( + [*command, "serve", "--config", str(config_path)], + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=runtime_env, + bufsize=1, + ) + _drain_stream(process.stderr, stderr_lines) + line = _read_startup_line(process, resolved.startup_timeout_seconds) + discovery = json.loads(line) + if not isinstance(discovery, dict): + raise HarnessSidecarError("runtime discovery must be a JSON object") + spec = SidecarBindingSpec.from_discovery(discovery) + if spec.status == "error" or ( + spec.status == "degraded" and not resolved.fail_open + ): + detail = discovery.get("error") or discovery.get("diagnostics") or line + raise HarnessSidecarError(f"Harness Sidecar failed to start: {detail}") + binding = SidecarBinding( + config=resolved, + spec=spec, + process=process, + config_path=config_path, + stderr_lines=stderr_lines, + ) + binding._configure_failover_relays(runtime_env) + if apply_env: + binding.apply_env(environ) + binding._start_runtime_monitor() + atexit.register(binding.stop) + return binding + except Exception as error: + if binding is not None: + binding.stop() + else: + if config_path is not None: + config_path.unlink(missing_ok=True) + if process is not None: + _terminate_process(process) + if isinstance(error, HarnessSidecarError): + raise + raise HarnessSidecarError(f"Harness Sidecar startup failed: {error}") from error + + +def export_sidecar_env( + binding: SidecarBinding, + base_env: Mapping[str, str] | None = None, +) -> dict[str, str]: + result = dict(base_env or {}) + result.update(binding.spec.env) + return result + + +def run_with_harness_sidecar( + config: HarnessSidecarConfig | Mapping[str, Any] | bool | None, + command: Sequence[str], + *, + profile: str | None = None, + env: Mapping[str, str] | None = None, + cwd: str | os.PathLike[str] | None = None, +) -> int: + if not command: + raise ValueError("a child command is required") + base_env = dict(env if env is not None else os.environ) + resolved = resolve_sidecar_config(config, profile=profile) + try: + binding = start_harness_sidecar(resolved, environ=base_env) + except HarnessSidecarError as error: + if not resolved.fail_open: + raise + warnings.warn( + f"Harness Sidecar unavailable; running command directly: {error}", + RuntimeWarning, + stacklevel=2, + ) + binding = SidecarBinding( + config=resolved, + spec=SidecarBindingSpec(status="degraded", profile=resolved.profile), + ) + with binding: + child_env = export_sidecar_env(binding, base_env) + try: + completed = subprocess.run(list(command), env=child_env, cwd=cwd) + except KeyboardInterrupt: + return 130 + return int(completed.returncode) + + +def doctor_harness_sidecar( + config: HarnessSidecarConfig | Mapping[str, Any] | bool | None = None, + *, + env: Mapping[str, str] | None = None, +) -> dict[str, Any]: + resolved = resolve_sidecar_config(config) + process_env = dict(env if env is not None else os.environ) + command = _resolve_runtime_command(resolved, process_env) + try: + completed = subprocess.run( + [ + *command, + "doctor", + "--json", + "--components", + ",".join(resolved.required_runtime_components), + ], + text=True, + capture_output=True, + env=process_env, + timeout=max(0.1, resolved.startup_timeout_seconds), + ) + except subprocess.TimeoutExpired as error: + raise HarnessSidecarError( + "Harness Sidecar doctor timed out after " + f"{resolved.startup_timeout_seconds:g}s" + ) from error + except OSError as error: + raise HarnessSidecarError( + f"Harness Sidecar doctor could not start: {error}" + ) from error + try: + report = json.loads(completed.stdout.strip() or "{}") + except ValueError as error: + raise HarnessSidecarError( + f"invalid doctor response: {completed.stdout or completed.stderr}" + ) from error + if completed.returncode != 0: + raise HarnessSidecarError( + str(report.get("error") or completed.stderr or "runtime doctor failed") + ) + return report + + +def _resolve_runtime_command( + config: HarnessSidecarConfig, env: Mapping[str, str] +) -> list[str]: + if config.runtime_command: + return list(config.runtime_command) + configured = env.get("AGENTKIT_HARNESS_RUNTIME_COMMAND") + if configured: + return shlex.split(configured) + executable = shutil.which(RUNTIME_EXECUTABLE) + if executable: + return [executable] + raise HarnessSidecarRuntimeUnavailable( + "AgentKit Harness Sidecar Runtime is not available in this environment. " + f"The private Runtime is cloud-only; {_install_hint(config)} or disable " + "harness.sidecar for local debugging." + ) + + +def _install_hint(_config: HarnessSidecarConfig) -> str: + return INSTALL_HINT + + +def _csv_urls(value: str | None) -> list[str]: + return [item.strip() for item in (value or "").split(",") if item.strip()] + + +def _write_runtime_config(config: HarnessSidecarConfig) -> Path: + descriptor, name = tempfile.mkstemp( + prefix="agentkit-harness-sidecar-", suffix=".json" + ) + path = Path(name) + try: + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as stream: + json.dump(config.runtime_payload(), stream, ensure_ascii=False) + except Exception: + path.unlink(missing_ok=True) + raise + return path + + +def _drain_stream(stream, target: list[str]) -> None: + if stream is None: + return + + def drain() -> None: + for line in stream: + target.append(line.rstrip()) + + threading.Thread(target=drain, name="harness-sidecar-stderr", daemon=True).start() + + +def _terminate_process(process: subprocess.Popen[str]) -> None: + if process.poll() is not None: + return + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + pass + + +def _read_startup_line(process: subprocess.Popen[str], timeout: float) -> str: + if process.stdout is None: + raise HarnessSidecarError("runtime stdout is unavailable") + lines: queue.Queue[str] = queue.Queue(maxsize=1) + + def read() -> None: + lines.put(process.stdout.readline()) + + threading.Thread(target=read, name="harness-sidecar-discovery", daemon=True).start() + try: + line = lines.get(timeout=max(0.1, timeout)).strip() + except queue.Empty as error: + raise HarnessSidecarError( + f"Harness Sidecar did not become ready within {timeout:g}s" + ) from error + if not line: + raise HarnessSidecarError( + f"Harness Sidecar exited before discovery (exit={process.poll()})" + ) + return line + + +__all__ = [ + "HarnessSidecarError", + "HarnessSidecarRuntimeUnavailable", + "SidecarBinding", + "doctor_harness_sidecar", + "export_sidecar_env", + "run_with_harness_sidecar", + "start_harness_sidecar", +] diff --git a/agentkit/extensions/harness_sidecar/sidecar_config.py b/agentkit/extensions/harness_sidecar/sidecar_config.py new file mode 100644 index 00000000..d8a46858 --- /dev/null +++ b/agentkit/extensions/harness_sidecar/sidecar_config.py @@ -0,0 +1,335 @@ +"""Public configuration contract for AgentKit Harness Sidecar.""" + +from __future__ import annotations + +import json +import os +import shlex +from collections.abc import Mapping +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from .component_catalog import CATALOG_VERSION +from .profiles import expand_sidecar_profile +from .runtime_components import ( + resolve_runtime_components, + runtime_flavor_for_components, +) +from .selection import ResolvedHarnessPlan, resolve_harness_sidecar_selection + + +class SidecarConfigModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class ModelProxyConfig(SidecarConfigModel): + enabled: bool = True + host: str = "127.0.0.1" + port: int = 0 + upstream_base_url: str | None = None + upstream_base_url_env: str = "MODEL_AGENT_API_BASE" + upstream_api_key_env: str = "MODEL_AGENT_API_KEY" + components: list[str] = Field(default_factory=list) + compression_provider: str = "noop" + headroom_base_url_env: str = "HARNESS_HEADROOM_BASE_URL" + fail_open: bool = True + trace_dir: str = ".harness-service/traces" + state_path: str = ".harness-service/state.sqlite3" + + +class MCPGatewayConfig(SidecarConfigModel): + enabled: bool = False + host: str = "127.0.0.1" + port: int = 0 + upstreams: list[str] = Field(default_factory=list) + upstreams_env: str = "MCP_URLS" + readonly_segments: list[str] = Field(default_factory=list) + presets: list[str] = Field(default_factory=list) + fail_open: bool = True + policy: dict[str, Any] = Field(default_factory=dict) + + +class HarnessSidecarConfig(SidecarConfigModel): + enabled: bool = True + profile: str = "default" + catalog_version: str = CATALOG_VERSION + runtime_version: str | None = None + component_overrides: dict[str, bool] = Field(default_factory=dict) + fail_open: bool = True + startup_timeout_seconds: float = 20.0 + runtime_command: list[str] | None = None + components: list[str] = Field(default_factory=list) + model_proxy: ModelProxyConfig = Field(default_factory=ModelProxyConfig) + mcp_gateway: MCPGatewayConfig = Field(default_factory=MCPGatewayConfig) + + @model_validator(mode="before") + @classmethod + def _expand_profile(cls, value: Any) -> Any: + if isinstance(value, cls): + return value + raw = dict(value or {}) + profile = str(raw.get("profile") or "default") + return expand_sidecar_profile(profile, raw) + + def runtime_payload(self) -> dict[str, Any]: + plan = self.resolved_plan + if not plan.valid: + raise ValueError("; ".join(plan.errors)) + return { + "schema_version": "1", + "enabled": self.enabled, + "profile": self.profile, + "runtime_components": self.required_runtime_components, + "resolved_plan": plan.model_dump(mode="json", exclude_none=True), + "model_proxy": self.model_proxy.model_dump(exclude_none=True), + "mcp_gateway": self.mcp_gateway.model_dump(exclude_none=True), + } + + @property + def resolved_plan(self) -> ResolvedHarnessPlan: + return resolve_harness_sidecar_selection( + enabled=self.enabled, + profile=self.profile, + component_overrides=self.component_overrides, + catalog_version=self.catalog_version, + runtime_version=self.runtime_version, + ) + + @property + def required_runtime_components(self) -> list[str]: + if not self.enabled: + return [] + vertical_components = list(self.components) + if self.profile == "ops": + vertical_components.append("ops") + if ( + self.model_proxy.enabled + and "long_run_control" in self.model_proxy.components + ): + vertical_components.append("goal_runtime") + return resolve_runtime_components( + model_proxy_enabled=self.model_proxy.enabled, + mcp_gateway_enabled=self.mcp_gateway.enabled, + components=vertical_components, + ) + + @property + def runtime_flavor(self) -> str: + return runtime_flavor_for_components(self.required_runtime_components) + + @classmethod + def from_env(cls, env: Mapping[str, str] | None = None) -> "HarnessSidecarConfig": + values = dict(env if env is not None else os.environ) + profile = values.get("HARNESS_PROFILE") or "default" + model_enabled = _env_bool(values.get("HARNESS_MODEL_PROXY_ENABLED"), True) + mcp_enabled = _env_bool( + values.get("HARNESS_MCP_GATEWAY_ENABLED"), profile == "ops" + ) + runtime_command = values.get("AGENTKIT_HARNESS_RUNTIME_COMMAND") + component_overrides = _env_json_object( + values.get("HARNESS_SIDECAR_COMPONENT_OVERRIDES") + ) + mcp_upstreams_env = values.get("HARNESS_MCP_UPSTREAMS_ENV", "MCP_URLS") + mcp_gateway: dict[str, Any] = { + "enabled": mcp_enabled, + "upstreams": _csv(values.get(mcp_upstreams_env)), + "upstreams_env": mcp_upstreams_env, + } + if "HARNESS_MCP_READONLY_SEGMENTS" in values: + mcp_gateway["readonly_segments"] = _csv( + values.get("HARNESS_MCP_READONLY_SEGMENTS") + ) + if "HARNESS_MCP_PRESETS" in values: + mcp_gateway["presets"] = _csv(values.get("HARNESS_MCP_PRESETS")) + return cls.model_validate( + { + "enabled": _env_bool(values.get("HARNESS_SIDECAR_ENABLED"), False), + "profile": profile, + "catalog_version": values.get( + "HARNESS_SIDECAR_CATALOG_VERSION", CATALOG_VERSION + ), + "runtime_version": values.get("HARNESS_SIDECAR_RUNTIME_VERSION"), + "component_overrides": component_overrides, + "fail_open": _env_bool(values.get("HARNESS_SIDECAR_FAIL_OPEN"), True), + "components": _csv(values.get("HARNESS_RUNTIME_COMPONENTS")), + "runtime_command": shlex.split(runtime_command) + if runtime_command + else None, + "model_proxy": { + "enabled": model_enabled, + "upstream_base_url_env": values.get( + "HARNESS_MODEL_UPSTREAM_BASE_URL_ENV", + "MODEL_AGENT_API_BASE", + ), + "upstream_api_key_env": values.get( + "HARNESS_MODEL_UPSTREAM_API_KEY_ENV", + "MODEL_AGENT_API_KEY", + ), + "compression_provider": values.get( + "HARNESS_MODEL_COMPRESSION_PROVIDER", "noop" + ), + }, + "mcp_gateway": mcp_gateway, + } + ) + + +class SidecarBindingSpec(SidecarConfigModel): + schema_version: str = "1" + status: str + profile: str + env: dict[str, str] = Field(default_factory=dict) + model_proxy_url: str | None = None + mcp_urls: list[str] = Field(default_factory=list) + runtime_flavor: str = "unknown" + runtime_components: list[str] = Field(default_factory=list) + requested_components: list[str] = Field(default_factory=list) + effective_components: list[str] = Field(default_factory=list) + active_components: list[str] = Field(default_factory=list) + failed_components: list[str] = Field(default_factory=list) + plan_hash: str | None = None + diagnostics: list[dict[str, Any]] = Field(default_factory=list) + + @classmethod + def from_discovery(cls, discovery: Mapping[str, Any]) -> "SidecarBindingSpec": + model_proxy = dict(discovery.get("model_proxy") or {}) + mcp_gateway = dict(discovery.get("mcp_gateway") or {}) + endpoints = dict(discovery.get("endpoints") or {}) + runtime = dict(discovery.get("runtime") or {}) + return cls( + schema_version=str(discovery.get("schema_version") or "1"), + status=str(discovery.get("status") or "error"), + profile=str(discovery.get("profile") or "default"), + env={ + str(key): str(value) + for key, value in dict(discovery.get("env") or {}).items() + }, + model_proxy_url=model_proxy.get("url") or endpoints.get("model_proxy_url"), + mcp_urls=[ + str(item) + for item in (mcp_gateway.get("urls") or endpoints.get("mcp_urls") or []) + ], + runtime_flavor=str(runtime.get("flavor") or "unknown"), + runtime_components=[ + str(item) + for item in ( + runtime.get("installed_internal_components") + or runtime.get("installed_components") + or [] + ) + ], + requested_components=[ + str(item) for item in discovery.get("requested_components") or [] + ], + effective_components=[ + str(item) for item in discovery.get("effective_components") or [] + ], + active_components=[ + str(item) for item in discovery.get("active_components") or [] + ], + failed_components=[ + str(item) for item in discovery.get("failed_components") or [] + ], + plan_hash=discovery.get("plan_hash"), + diagnostics=[dict(item) for item in discovery.get("diagnostics") or []], + ) + + +def resolve_sidecar_config( + value: HarnessSidecarConfig | Mapping[str, Any] | bool | None = None, + *, + profile: str | None = None, +) -> HarnessSidecarConfig: + if isinstance(value, HarnessSidecarConfig): + if profile is None or value.profile == profile: + return value + raw = value.model_dump(exclude_unset=True) + raw["profile"] = profile + return HarnessSidecarConfig.model_validate(raw) + if isinstance(value, bool): + raw: dict[str, Any] = {"enabled": value} + else: + raw = dict(value or {}) + if profile is not None: + raw["profile"] = profile + return HarnessSidecarConfig.model_validate(raw) + + +def sidecar_config_to_env( + value: HarnessSidecarConfig | Mapping[str, Any] | bool | None, + *, + profile: str | None = None, +) -> dict[str, str]: + config = resolve_sidecar_config(value, profile=profile) + plan = config.resolved_plan + if not plan.valid: + raise ValueError("; ".join(plan.errors)) + env = { + "HARNESS_SIDECAR_ENABLED": _bool_string(config.enabled), + "HARNESS_SIDECAR_FAIL_OPEN": _bool_string(config.fail_open), + "HARNESS_PROFILE": config.profile, + "HARNESS_SIDECAR_CATALOG_VERSION": config.catalog_version, + "HARNESS_SIDECAR_COMPONENT_OVERRIDES": json.dumps( + config.component_overrides, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ), + "HARNESS_SIDECAR_PLAN": json.dumps( + plan.model_dump(mode="json", exclude_none=True), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ), + "HARNESS_RUNTIME_COMPONENTS": ",".join(config.components), + "HARNESS_MODEL_PROXY_ENABLED": _bool_string(config.model_proxy.enabled), + "HARNESS_MODEL_UPSTREAM_BASE_URL_ENV": ( + config.model_proxy.upstream_base_url_env + ), + "HARNESS_MODEL_UPSTREAM_API_KEY_ENV": config.model_proxy.upstream_api_key_env, + "HARNESS_MODEL_COMPRESSION_PROVIDER": (config.model_proxy.compression_provider), + "HARNESS_MCP_GATEWAY_ENABLED": _bool_string(config.mcp_gateway.enabled), + "HARNESS_MCP_UPSTREAMS_ENV": config.mcp_gateway.upstreams_env, + "HARNESS_MCP_READONLY_SEGMENTS": ",".join(config.mcp_gateway.readonly_segments), + "HARNESS_MCP_PRESETS": ",".join(config.mcp_gateway.presets), + } + if config.runtime_version is not None: + env["HARNESS_SIDECAR_RUNTIME_VERSION"] = config.runtime_version + return env + + +def _csv(value: str | None) -> list[str]: + return [item.strip() for item in (value or "").split(",") if item.strip()] + + +def _env_bool(value: str | None, default: bool) -> bool: + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + + +def _env_json_object(value: str | None) -> dict[str, Any]: + if not value: + return {} + try: + parsed = json.loads(value) + except ValueError as error: + raise ValueError("HARNESS_SIDECAR_COMPONENT_OVERRIDES must be JSON") from error + if not isinstance(parsed, dict): + raise ValueError("HARNESS_SIDECAR_COMPONENT_OVERRIDES must be a JSON object") + return {str(key): item for key, item in parsed.items()} + + +def _bool_string(value: bool) -> str: + return "true" if value else "false" + + +__all__ = [ + "HarnessSidecarConfig", + "MCPGatewayConfig", + "ModelProxyConfig", + "SidecarBindingSpec", + "resolve_sidecar_config", + "sidecar_config_to_env", +] diff --git a/agentkit/toolkit/cli/cli.py b/agentkit/toolkit/cli/cli.py index 1d830863..85c9ceeb 100644 --- a/agentkit/toolkit/cli/cli.py +++ b/agentkit/toolkit/cli/cli.py @@ -49,6 +49,7 @@ from agentkit.toolkit.cli.cli_list import list_app from agentkit.toolkit.cli.cli_delete import delete_app from agentkit.toolkit.cli.cli_logs import logs_command +from agentkit.extensions.harness_sidecar.cli import harness_app # Note: Avoid importing heavy packages at the top to keep CLI startup fast @@ -132,6 +133,7 @@ def main( app.add_typer(add_app, name="add") app.add_typer(list_app, name="list") app.add_typer(delete_app, name="delete") +app.add_typer(harness_app, name="harness") if __name__ == "__main__": diff --git a/agentkit/toolkit/harness/config_builder.py b/agentkit/toolkit/harness/config_builder.py index f457a559..a40b6782 100644 --- a/agentkit/toolkit/harness/config_builder.py +++ b/agentkit/toolkit/harness/config_builder.py @@ -23,6 +23,7 @@ def build_agentkit_config( envs: Dict[str, str], auth: Optional[Dict[str, Any]] = None, runtime_id: str = "Auto", + cloud_config_overrides: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Build the cloud AgentKit launch config dict (auto-provision). @@ -64,6 +65,8 @@ def build_agentkit_config( cloud["runtime_apikey_name"] = "Auto" cloud["runtime_apikey"] = "Auto" cloud["runtime_jwt_allowed_clients"] = [] + if cloud_config_overrides: + cloud.update(cloud_config_overrides) return { "common": { "agent_name": runtime_name, diff --git a/agentkit/toolkit/harness/deploy.py b/agentkit/toolkit/harness/deploy.py index 9ad925fd..7c0fa906 100644 --- a/agentkit/toolkit/harness/deploy.py +++ b/agentkit/toolkit/harness/deploy.py @@ -198,6 +198,8 @@ def deploy_harness( secret_key: Optional[str] = None, discovery_url: Optional[str] = None, allowed_id: Optional[str] = None, + runtime_env_builder: Optional[Callable[[Dict[str, Any]], Dict[str, str]]] = None, + cloud_config_overrides: Optional[Dict[str, Any]] = None, reporter: Optional[Reporter] = None, on_conflict: Optional[Callable[[Dict[str, Any]], bool]] = None, ) -> LifecycleResult: @@ -228,6 +230,8 @@ def deploy_harness( region: AgentKit region (default ``cn-beijing`` or ``VOLCENGINE_REGION``). access_key / secret_key: Volcengine credentials (default: ``VOLCENGINE_*`` env). discovery_url / allowed_id: OAuth2/JWT overrides for the spec ``auth`` block. + runtime_env_builder: Optional environment builder used by extensions. + cloud_config_overrides: Optional cloud launch configuration overrides. reporter: Progress reporter forwarded to the launch (default: silent). on_conflict: Callback consulted when a single same-name harness exists; returns True to update it, False to abort. @@ -259,7 +263,7 @@ def deploy_harness( os.environ.setdefault(key, value) spec = _load_harness_spec(proj_dir / f"{name}.harness.json") - runtime_envs = to_runtime_env(spec) + runtime_envs = (runtime_env_builder or to_runtime_env)(spec) runtime_name = name auth = _resolve_auth(spec.get("auth"), discovery_url, allowed_id) @@ -328,6 +332,7 @@ def deploy_harness( runtime_envs, auth, runtime_id=update_runtime_id or "Auto", + cloud_config_overrides=cloud_config_overrides, ) # AgentKit's launch path exposes no hook for runtime tags, so tag the runtime diff --git a/agentkit/version.py b/agentkit/version.py index fc10de71..d7c6a752 100644 --- a/agentkit/version.py +++ b/agentkit/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -VERSION = "0.8.0" +VERSION = "0.8.1" diff --git a/pyproject.toml b/pyproject.toml index 550c6d46..710fe6e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentkit-sdk-python" -version = "0.8.0" +version = "0.8.1" description = "Python SDK for transforming any AI agent into a production-ready application. Framework-agnostic primitives for runtime, memory, authentication, and tools with volcengine-managed infrastructure." readme = "README.md" requires-python = ">=3.10" @@ -83,6 +83,7 @@ langgraph = ["langgraph"] strands = ["strands-agents"] agentcore = ["bedrock-agentcore"] frameworks = ["langchain", "langgraph", "strands-agents", "bedrock-agentcore"] +harness-sidecar = [] [tool.ruff.lint] diff --git a/tests/extensions/harness_sidecar/cli/test_cli.py b/tests/extensions/harness_sidecar/cli/test_cli.py new file mode 100644 index 00000000..673ddf56 --- /dev/null +++ b/tests/extensions/harness_sidecar/cli/test_cli.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from types import SimpleNamespace + +from typer.testing import CliRunner + +from agentkit.toolkit.cli.cli import app +from agentkit.extensions.harness_sidecar.cli import _config + + +runner = CliRunner() + + +def test_harness_sidecar_export_env_uses_ops_product_profile() -> None: + result = runner.invoke( + app, + [ + "harness", + "sidecar", + "export-env", + "--profile", + "ops", + "--shell", + ], + ) + + assert result.exit_code == 0, result.output + assert "export HARNESS_SIDECAR_ENABLED=true" in result.output + assert "export HARNESS_PROFILE=ops" in result.output + assert "export HARNESS_MCP_GATEWAY_ENABLED=true" in result.output + assert "export HARNESS_MCP_READONLY_SEGMENTS='*'" in result.output + assert "export HARNESS_MCP_PRESETS=sql_readonly" in result.output + + +def test_harness_sidecar_catalog_and_resolve_commands() -> None: + catalog = runner.invoke(app, ["harness", "sidecar", "catalog", "--profile", "ops"]) + resolved = runner.invoke( + app, + [ + "harness", + "sidecar", + "resolve", + "--profile", + "ops", + "--component", + "verifier=false", + ], + ) + + assert catalog.exit_code == 0, catalog.output + assert '"total_component_count": 9' in catalog.output + assert resolved.exit_code == 0, resolved.output + assert '"effective_component_count": 5' in resolved.output + + +def test_harness_sidecar_doctor_uses_runtime_command(tmp_path: Path) -> None: + runtime = tmp_path / "doctor.py" + runtime.write_text( + "import json; print(json.dumps({'status': 'ok', 'version': 'test'}))", + encoding="utf-8", + ) + result = runner.invoke( + app, + ["harness", "sidecar", "doctor"], + env={"AGENTKIT_HARNESS_RUNTIME_COMMAND": f"{sys.executable} {runtime}"}, + ) + + assert result.exit_code == 0, result.output + assert '"status": "ok"' in result.output + + +def test_config_file_values_are_preserved_without_explicit_cli_overrides( + tmp_path: Path, +) -> None: + config_path = tmp_path / "sidecar.json" + config_path.write_text( + '{"profile":"default","model_proxy":{"enabled":false},' + '"mcp_gateway":{"enabled":true,"presets":["sql_readonly"]}}', + encoding="utf-8", + ) + + config = _config( + profile=None, + config_path=config_path, + model_proxy=None, + mcp_gateway=None, + model_upstream_env=None, + compression_provider=None, + mcp_upstreams_env=None, + presets=None, + readonly_segments=None, + ) + + assert config.profile == "default" + assert config.model_proxy.enabled is False + assert config.mcp_gateway.enabled is True + assert config.mcp_gateway.presets == ["sql_readonly"] + + +def test_export_env_preserves_profile_from_config_file(tmp_path: Path) -> None: + config_path = tmp_path / "sidecar.json" + config_path.write_text( + '{"profile":"default","mcp_gateway":{"enabled":false}}', + encoding="utf-8", + ) + + result = runner.invoke( + app, + ["harness", "sidecar", "export-env", "--config", str(config_path)], + ) + + assert result.exit_code == 0, result.output + assert '"HARNESS_PROFILE": "default"' in result.output + assert '"HARNESS_MCP_GATEWAY_ENABLED": "false"' in result.output + + +def test_sidecar_deploy_forwards_vpc_options(monkeypatch) -> None: + import agentkit.extensions.harness_sidecar.cli as cli_module + + captured = {} + + def fake_deploy_harness(**kwargs): + captured.update(kwargs) + return SimpleNamespace( + success=True, + deploy_result=SimpleNamespace(metadata={"runtime_id": "runtime-test"}), + ) + + monkeypatch.setattr(cli_module, "deploy_harness", fake_deploy_harness) + + result = runner.invoke( + app, + [ + "harness", + "sidecar", + "deploy", + "private-harness", + "--runtime-vpc-id", + "vpc-example", + "--runtime-subnet-id", + "subnet-example", + "--no-runtime-enable-shared-internet-access", + "--yes", + ], + ) + + assert result.exit_code == 0, result.output + assert captured["runtime_vpc_id"] == "vpc-example" + assert captured["runtime_subnet_ids"] == ["subnet-example"] + assert captured["runtime_enable_shared_internet_access"] is False + + +def test_core_deploy_command_does_not_expose_sidecar_vpc_flags() -> None: + result = runner.invoke(app, ["deploy", "--help"]) + + assert result.exit_code == 0, result.output + assert "--runtime-vpc-id" not in result.output diff --git a/tests/extensions/harness_sidecar/test_component_catalog.py b/tests/extensions/harness_sidecar/test_component_catalog.py new file mode 100644 index 00000000..a4f1ecd5 --- /dev/null +++ b/tests/extensions/harness_sidecar/test_component_catalog.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import json + +from agentkit.extensions.harness_sidecar import ( + CATALOG_VERSION, + PRODUCT_COMPONENT_ORDER, + ComponentAvailability, + get_harness_sidecar_catalog, + resolve_harness_sidecar_selection, +) + + +def test_catalog_exposes_nine_product_components_and_two_profiles() -> None: + catalog = get_harness_sidecar_catalog(profile="ops") + + assert catalog.catalog_version == CATALOG_VERSION + assert catalog.profile_count == 2 + assert catalog.total_component_count == 9 + assert catalog.selectable_component_count == 6 + assert catalog.selected_profile.profile_component_count == 6 + assert catalog.selected_profile.default_components == list( + PRODUCT_COMPONENT_ORDER[:6] + ) + assert [component.id for component in catalog.components] == list( + PRODUCT_COMPONENT_ORDER + ) + + +def test_catalog_hides_internal_runtime_module_names() -> None: + payload = get_harness_sidecar_catalog(profile="ops").model_dump_json() + + for internal_name in ( + "harness_core", + "runtime_core", + "goal_runtime", + "model_proxy", + "mcp_gateway", + "browser_runtime", + ): + assert internal_name not in payload + + +def test_preview_components_are_visible_with_unavailable_reasons() -> None: + catalog = get_harness_sidecar_catalog(profile="default") + components = {component.id: component for component in catalog.components} + + for component_id in ("browser", "evaluation", "shadow"): + availability = components[component_id].availability + assert availability.available is False + assert availability.status == "preview" + assert availability.reason + + +def test_catalog_dependencies_exist_and_are_acyclic() -> None: + catalog = get_harness_sidecar_catalog(profile="ops") + dependencies = { + component.id: component.dependencies for component in catalog.components + } + + def visit(component_id: str, path: tuple[str, ...] = ()) -> None: + assert component_id not in path + for dependency in dependencies[component_id]: + assert dependency in dependencies + visit(dependency, (*path, component_id)) + + for component_id in dependencies: + visit(component_id) + + +def test_catalog_accepts_control_plane_availability_overrides() -> None: + catalog = get_harness_sidecar_catalog( + profile="default", + availability_overrides={ + "browser": ComponentAvailability( + available=True, + status="ga", + min_runtime_version="0.2.0", + regions=["cn-beijing"], + ) + }, + ) + + browser = next(item for item in catalog.components if item.id == "browser") + assert browser.availability.available is True + assert browser.availability.reason is None + assert browser.availability.min_runtime_version == "0.2.0" + assert catalog.selectable_component_count == 7 + + +def test_default_and_ops_resolve_exact_product_defaults() -> None: + default = resolve_harness_sidecar_selection(profile="default") + ops = resolve_harness_sidecar_selection(profile="ops") + + assert default.valid is True + assert default.effective_components == list(PRODUCT_COMPONENT_ORDER[:4]) + assert default.effective_component_count == 4 + assert ops.valid is True + assert ops.effective_components == list(PRODUCT_COMPONENT_ORDER[:6]) + assert ops.effective_component_count == 6 + assert ops.activation_targets.mcp_gateway.presets == ["sql_readonly"] + assert ops.activation_targets.mcp_gateway.readonly_segments == ["*"] + + +def test_resolver_applies_overrides_and_product_dependencies() -> None: + plan = resolve_harness_sidecar_selection( + profile="default", + component_overrides={"sql_readonly": True, "verifier": False}, + ) + + assert plan.valid is True + assert plan.requested_components == [ + "context_engine", + "compressor", + "long_run_control", + "sql_readonly", + ] + assert plan.effective_components == [ + "context_engine", + "compressor", + "long_run_control", + "mcp_resilience", + "sql_readonly", + ] + assert [item.model_dump() for item in plan.auto_added_components] == [ + {"id": "mcp_resilience", "required_by": ["sql_readonly"]} + ] + + +def test_disabled_selection_resolves_to_zero_components() -> None: + plan = resolve_harness_sidecar_selection( + enabled=False, + profile="ops", + component_overrides={"browser": True}, + ) + + assert plan.valid is True + assert plan.requested_components == [] + assert plan.effective_components == [] + assert plan.effective_component_count == 0 + assert plan.activation_targets.runtime_components == [] + + +def test_unavailable_dependency_chain_returns_actionable_errors() -> None: + plan = resolve_harness_sidecar_selection( + profile="default", + component_overrides={"shadow": True}, + ) + + assert plan.valid is False + assert plan.effective_components[-3:] == ["browser", "evaluation", "shadow"] + assert {item.id for item in plan.auto_added_components} == { + "browser", + "evaluation", + } + assert len(plan.errors) == 3 + assert all("unavailable" in error for error in plan.errors) + + +def test_plan_hash_is_stable_across_mapping_order_and_json_round_trip() -> None: + first = resolve_harness_sidecar_selection( + profile="ops", + component_overrides={"verifier": False, "sql_readonly": True}, + runtime_version="0.1.0", + ) + second = resolve_harness_sidecar_selection( + profile="ops", + component_overrides={"sql_readonly": True, "verifier": False}, + runtime_version="0.1.0", + ) + + assert first.plan_hash == second.plan_hash + assert first.plan_hash.startswith("sha256:") + assert json.loads(first.model_dump_json()) == json.loads(second.model_dump_json()) + + +def test_unknown_component_and_catalog_version_return_invalid_plan() -> None: + plan = resolve_harness_sidecar_selection( + profile="ops", + component_overrides={"not_a_component": True}, + catalog_version="old", + ) + + assert plan.valid is False + assert any("not_a_component" in error for error in plan.errors) + assert any("Catalog version" in error for error in plan.errors) diff --git a/tests/extensions/harness_sidecar/test_deploy.py b/tests/extensions/harness_sidecar/test_deploy.py new file mode 100644 index 00000000..cdb974b3 --- /dev/null +++ b/tests/extensions/harness_sidecar/test_deploy.py @@ -0,0 +1,96 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Runtime networking and core-extension contracts.""" + +from inspect import Parameter, signature + +from agentkit.extensions.harness_sidecar import deploy as deploy_module +from agentkit.extensions.harness_sidecar.deploy import ( + build_runtime_network, + deploy_harness, + to_runtime_env, +) + + +def test_runtime_network_is_empty_without_explicit_options(): + assert build_runtime_network() == {} + assert build_runtime_network(subnet_ids=[]) == {} + + +def test_runtime_network_preserves_explicit_false(): + assert build_runtime_network( + mode="private", + vpc_id="vpc-example", + subnet_ids=["subnet-example"], + enable_shared_internet_access=False, + ) == { + "mode": "private", + "vpc_id": "vpc-example", + "subnet_ids": ["subnet-example"], + "enable_shared_internet_access": False, + } + + +def test_deploy_network_options_are_optional_keyword_only_parameters(): + parameters = signature(deploy_harness).parameters + + for name in ( + "runtime_network_mode", + "runtime_vpc_id", + "runtime_subnet_ids", + "runtime_enable_shared_internet_access", + ): + assert parameters[name].kind is Parameter.KEYWORD_ONLY + assert parameters[name].default is None + + +def test_deploy_forwards_vpc_through_generic_core_hooks(monkeypatch): + captured = {} + expected = object() + + def fake_core_deploy(**kwargs): + captured.update(kwargs) + return expected + + monkeypatch.setattr(deploy_module, "_deploy_harness", fake_core_deploy) + + result = deploy_harness( + "private-harness", + runtime_vpc_id="vpc-example", + runtime_subnet_ids=["subnet-example"], + ) + + assert result is expected + assert captured["runtime_env_builder"] is to_runtime_env + assert captured["cloud_config_overrides"] == { + "runtime_network": { + "vpc_id": "vpc-example", + "subnet_ids": ["subnet-example"], + } + } + + +def test_deploy_without_vpc_does_not_override_cloud_config(monkeypatch): + captured = {} + + def fake_core_deploy(**kwargs): + captured.update(kwargs) + return object() + + monkeypatch.setattr(deploy_module, "_deploy_harness", fake_core_deploy) + + deploy_harness("legacy-harness") + + assert captured["cloud_config_overrides"] is None diff --git a/tests/extensions/harness_sidecar/test_failover_proxy.py b/tests/extensions/harness_sidecar/test_failover_proxy.py new file mode 100644 index 00000000..07499173 --- /dev/null +++ b/tests/extensions/harness_sidecar/test_failover_proxy.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json +import threading +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +from agentkit.extensions.harness_sidecar.failover_proxy import StableHttpRelay + + +def _upstream(label: str): + requests: list[dict[str, object]] = [] + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_args: object) -> None: + return + + def do_POST(self) -> None: # noqa: N802 + length = int(self.headers.get("Content-Length", "0")) + body = self.rfile.read(length) + requests.append({"path": self.path, "body": body.decode("utf-8")}) + payload = json.dumps({"upstream": label}).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + host, port = server.server_address[:2] + return server, thread, f"http://{host}:{port}", requests + + +def test_stable_relay_switches_to_direct_upstream_without_changing_url() -> None: + sidecar, sidecar_thread, sidecar_url, sidecar_requests = _upstream("sidecar") + direct, direct_thread, direct_url, direct_requests = _upstream("direct") + relay = StableHttpRelay(f"{sidecar_url}/api/v3", f"{direct_url}/direct/v3") + stable_url = relay.url + request_url = f"{stable_url}/chat/completions?trace=test" + + try: + request = urllib.request.Request( + request_url, data=b'{"messages":[]}', method="POST" + ) + with urllib.request.urlopen(request, timeout=5) as response: + assert json.loads(response.read()) == {"upstream": "sidecar"} + + relay.activate_fallback() + request = urllib.request.Request( + request_url, data=b'{"messages":[]}', method="POST" + ) + with urllib.request.urlopen(request, timeout=5) as response: + assert json.loads(response.read()) == {"upstream": "direct"} + finally: + relay.close() + for server, thread in ( + (sidecar, sidecar_thread), + (direct, direct_thread), + ): + server.shutdown() + server.server_close() + thread.join(timeout=5) + + assert relay.url == stable_url + assert sidecar_requests == [ + {"path": "/api/v3/chat/completions?trace=test", "body": '{"messages":[]}'} + ] + assert direct_requests == [ + {"path": "/direct/v3/chat/completions?trace=test", "body": '{"messages":[]}'} + ] diff --git a/tests/extensions/harness_sidecar/test_sidecar.py b/tests/extensions/harness_sidecar/test_sidecar.py new file mode 100644 index 00000000..498b1ac9 --- /dev/null +++ b/tests/extensions/harness_sidecar/test_sidecar.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +import json +import os +import stat +import sys +import textwrap +import threading +import time +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +import pytest + +from agentkit.extensions.harness_sidecar.sidecar import ( + HarnessSidecarError, + HarnessSidecarRuntimeUnavailable, + doctor_harness_sidecar, + run_with_harness_sidecar, + start_harness_sidecar, +) +from agentkit.extensions.harness_sidecar.sidecar_config import ( + HarnessSidecarConfig, + SidecarBindingSpec, +) + + +@pytest.fixture +def fake_runtime(tmp_path: Path) -> Path: + path = tmp_path / "fake_runtime.py" + path.write_text( + textwrap.dedent( + """ + import json + import signal + import sys + import time + + if sys.argv[1] == "doctor": + print(json.dumps({"status": "ok", "internal_kernel": True})) + raise SystemExit(0) + + config_path = sys.argv[sys.argv.index("--config") + 1] + config = json.load(open(config_path, encoding="utf-8")) + capture = __import__("os").environ.get("FAKE_RUNTIME_CAPTURE") + if capture: + open(capture, "w", encoding="utf-8").write(json.dumps(config)) + discovery = { + "schema_version": "1", + "status": "ok", + "profile": config["profile"], + "model_proxy": {"url": "http://127.0.0.1:18787/api/v3"}, + "mcp_gateway": {"urls": ["http://127.0.0.1:18899/metrics"]}, + "env": { + "MODEL_AGENT_API_BASE": "http://127.0.0.1:18787/api/v3", + "MCP_URLS": "http://127.0.0.1:18899/metrics", + "HARNESS_SIDECAR_ENABLED": "true", + "HARNESS_PROFILE": config["profile"], + }, + "diagnostics": [], + } + print(json.dumps(discovery), flush=True) + if __import__("os").environ.get("FAKE_RUNTIME_EXIT_AFTER_DISCOVERY"): + time.sleep(0.1) + raise SystemExit(23) + running = True + def stop(*_args): + global running + running = False + signal.signal(signal.SIGTERM, stop) + signal.signal(signal.SIGINT, stop) + while running: + time.sleep(0.05) + """ + ), + encoding="utf-8", + ) + return path + + +def _config(fake_runtime: Path) -> HarnessSidecarConfig: + return HarnessSidecarConfig( + profile="ops", runtime_command=[sys.executable, str(fake_runtime)] + ) + + +def test_start_applies_and_restores_binding_env( + fake_runtime: Path, tmp_path: Path +) -> None: + capture = tmp_path / "runtime-config.json" + environ = { + "MODEL_AGENT_API_BASE": "https://real-model/api/v3", + "MCP_URLS": "https://real-mcp/metrics", + "FAKE_RUNTIME_CAPTURE": str(capture), + } + binding = start_harness_sidecar( + _config(fake_runtime), apply_env=True, environ=environ + ) + try: + assert binding.process is not None and binding.process.poll() is None + assert environ["MODEL_AGENT_API_BASE"].startswith("http://127.0.0.1") + assert environ["MCP_URLS"].endswith("/metrics") + assert stat.S_IMODE(binding.config_path.stat().st_mode) == 0o600 + runtime_config = json.loads(capture.read_text(encoding="utf-8")) + assert runtime_config["profile"] == "ops" + assert "runtime_command" not in runtime_config + finally: + binding.stop() + assert environ["MODEL_AGENT_API_BASE"] == "https://real-model/api/v3" + assert environ["MCP_URLS"] == "https://real-mcp/metrics" + assert binding.config_path.exists() is False + + +def test_runtime_process_env_is_separate_from_binding_target( + fake_runtime: Path, +) -> None: + target_env = {"ORIGINAL": "target"} + runtime_env = {"ORIGINAL": "runtime"} + + binding = start_harness_sidecar( + _config(fake_runtime), + apply_env=True, + environ=target_env, + process_env=runtime_env, + ) + try: + assert target_env["ORIGINAL"] == "target" + assert target_env["MODEL_AGENT_API_BASE"].startswith("http://127.0.0.1") + finally: + binding.stop() + + +def test_runtime_exit_switches_stable_model_url_to_direct_upstream( + fake_runtime: Path, +) -> None: + requests: list[str] = [] + + class DirectHandler(BaseHTTPRequestHandler): + def log_message(self, *_args: object) -> None: + return + + def do_GET(self) -> None: # noqa: N802 + requests.append(self.path) + payload = b"direct-ok" + self.send_response(200) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + direct = ThreadingHTTPServer(("127.0.0.1", 0), DirectHandler) + direct_thread = threading.Thread(target=direct.serve_forever, daemon=True) + direct_thread.start() + host, port = direct.server_address[:2] + direct_url = f"http://{host}:{port}/api/v3" + environ = {"MODEL_AGENT_API_BASE": direct_url} + config = HarnessSidecarConfig( + profile="default", + runtime_command=[sys.executable, str(fake_runtime)], + model_proxy={"enabled": True, "upstream_base_url": direct_url}, + mcp_gateway={"enabled": False}, + ) + binding = start_harness_sidecar( + config, + apply_env=True, + environ=environ, + process_env={"FAKE_RUNTIME_EXIT_AFTER_DISCOVERY": "1"}, + ) + stable_url = environ["MODEL_AGENT_API_BASE"] + + try: + deadline = time.monotonic() + 5 + while binding.spec.status != "degraded" and time.monotonic() < deadline: + time.sleep(0.02) + assert binding.spec.status == "degraded" + assert stable_url.startswith("http://127.0.0.1:") + assert stable_url != direct_url + assert environ["MODEL_AGENT_API_BASE"] == stable_url + assert "HARNESS_SIDECAR_ENABLED" not in environ + with urllib.request.urlopen( + f"{stable_url}/chat/completions", timeout=5 + ) as response: + assert response.read() == b"direct-ok" + assert binding.spec.diagnostics[-1]["status"] == "degraded" + finally: + binding.stop() + direct.shutdown() + direct.server_close() + direct_thread.join(timeout=5) + + assert environ["MODEL_AGENT_API_BASE"] == direct_url + assert requests == ["/api/v3/chat/completions"] + + +def test_runtime_exit_switches_stable_mcp_url_to_env_upstream( + fake_runtime: Path, +) -> None: + requests: list[str] = [] + + class DirectHandler(BaseHTTPRequestHandler): + def log_message(self, *_args: object) -> None: + return + + def do_POST(self) -> None: # noqa: N802 + requests.append(self.path) + payload = b"direct-mcp-ok" + self.send_response(200) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + direct = ThreadingHTTPServer(("127.0.0.1", 0), DirectHandler) + direct_thread = threading.Thread(target=direct.serve_forever, daemon=True) + direct_thread.start() + host, port = direct.server_address[:2] + direct_url = f"http://{host}:{port}/metrics" + environ = {"MCP_URLS": direct_url} + config = HarnessSidecarConfig( + profile="default", + runtime_command=[sys.executable, str(fake_runtime)], + model_proxy={"enabled": False}, + mcp_gateway={"enabled": True, "upstreams_env": "MCP_URLS"}, + ) + binding = start_harness_sidecar( + config, + apply_env=True, + environ=environ, + process_env={ + "FAKE_RUNTIME_EXIT_AFTER_DISCOVERY": "1", + "MCP_URLS": direct_url, + }, + ) + stable_url = environ["MCP_URLS"] + + try: + deadline = time.monotonic() + 5 + while binding.spec.status != "degraded" and time.monotonic() < deadline: + time.sleep(0.02) + assert binding.spec.status == "degraded" + assert stable_url.startswith("http://127.0.0.1:") + assert stable_url != direct_url + assert environ["MCP_URLS"] == stable_url + request = urllib.request.Request( + f"{stable_url}/tools/call", data=b"{}", method="POST" + ) + with urllib.request.urlopen(request, timeout=5) as response: + assert response.read() == b"direct-mcp-ok" + finally: + binding.stop() + direct.shutdown() + direct.server_close() + direct_thread.join(timeout=5) + + assert environ["MCP_URLS"] == direct_url + assert requests == ["/metrics/tools/call"] + + +def test_run_wraps_child_with_sidecar_environment( + fake_runtime: Path, tmp_path: Path +) -> None: + output = tmp_path / "child-env.json" + child = tmp_path / "child.py" + child.write_text( + "import json, os, sys; " + "json.dump({'model': os.getenv('MODEL_AGENT_API_BASE'), " + "'mcp': os.getenv('MCP_URLS')}, open(sys.argv[1], 'w'))", + encoding="utf-8", + ) + + exit_code = run_with_harness_sidecar( + _config(fake_runtime), [sys.executable, str(child), str(output)] + ) + + assert exit_code == 0 + values = json.loads(output.read_text(encoding="utf-8")) + assert values["model"] == "http://127.0.0.1:18787/api/v3" + assert values["mcp"] == "http://127.0.0.1:18899/metrics" + + +def test_doctor_uses_product_runtime_entrypoint(fake_runtime: Path) -> None: + report = doctor_harness_sidecar(_config(fake_runtime)) + + assert report == {"status": "ok", "internal_kernel": True} + + +def test_missing_runtime_has_customer_facing_install_hint(monkeypatch) -> None: + monkeypatch.delenv("AGENTKIT_HARNESS_RUNTIME_COMMAND", raising=False) + monkeypatch.setattr("shutil.which", lambda _name: None) + + with pytest.raises( + HarnessSidecarRuntimeUnavailable, + match="private Runtime is cloud-only", + ): + start_harness_sidecar({"profile": "ops"}) + + +def test_run_fails_open_to_direct_child( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + output = tmp_path / "direct.txt" + monkeypatch.delenv("AGENTKIT_HARNESS_RUNTIME_COMMAND", raising=False) + monkeypatch.setattr("shutil.which", lambda _name: None) + + with pytest.warns(RuntimeWarning, match="running command directly"): + exit_code = run_with_harness_sidecar( + {"profile": "ops", "fail_open": True}, + [sys.executable, "-c", f"open({str(output)!r}, 'w').write('ok')"], + ) + + assert exit_code == 0 + assert output.read_text(encoding="utf-8") == "ok" + + +def test_run_fails_open_when_runtime_discovery_is_invalid(tmp_path: Path) -> None: + runtime = tmp_path / "invalid-runtime.py" + runtime.write_text("print('not-json', flush=True)", encoding="utf-8") + output = tmp_path / "direct-after-invalid-runtime.txt" + + with pytest.warns(RuntimeWarning, match="running command directly"): + exit_code = run_with_harness_sidecar( + { + "profile": "ops", + "fail_open": True, + "runtime_command": [sys.executable, str(runtime)], + }, + [sys.executable, "-c", f"open({str(output)!r}, 'w').write('ok')"], + ) + + assert exit_code == 0 + assert output.read_text(encoding="utf-8") == "ok" + + +def test_doctor_has_a_bounded_timeout(tmp_path: Path) -> None: + runtime = tmp_path / "slow-runtime.py" + runtime.write_text("import time; time.sleep(30)", encoding="utf-8") + + with pytest.raises(HarnessSidecarError, match="doctor timed out"): + doctor_harness_sidecar( + { + "runtime_command": [sys.executable, str(runtime)], + "startup_timeout_seconds": 0.1, + } + ) + + +def test_discovery_v2_reports_product_activation_state() -> None: + spec = SidecarBindingSpec.from_discovery( + { + "schema_version": "agentkit.harness-sidecar.discovery/v2", + "status": "degraded", + "profile": "ops", + "requested_components": ["context_engine", "verifier"], + "effective_components": ["context_engine", "verifier"], + "active_components": ["context_engine"], + "failed_components": ["verifier"], + "endpoints": {"model_proxy_url": "http://127.0.0.1:18787/api/v3"}, + "runtime": { + "installed_internal_components": [ + "runtime_core", + "ops", + "goal_runtime", + "model_proxy", + ] + }, + "plan_hash": "sha256:test", + } + ) + + assert spec.model_proxy_url == "http://127.0.0.1:18787/api/v3" + assert spec.requested_components == ["context_engine", "verifier"] + assert spec.effective_components == ["context_engine", "verifier"] + assert spec.active_components == ["context_engine"] + assert spec.failed_components == ["verifier"] + assert spec.runtime_components == [ + "runtime_core", + "ops", + "goal_runtime", + "model_proxy", + ] + assert spec.plan_hash == "sha256:test" diff --git a/tests/extensions/harness_sidecar/test_sidecar_config.py b/tests/extensions/harness_sidecar/test_sidecar_config.py new file mode 100644 index 00000000..9c524cb3 --- /dev/null +++ b/tests/extensions/harness_sidecar/test_sidecar_config.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +import tomllib + +from agentkit.extensions.harness_sidecar.deploy import to_runtime_env +from agentkit.extensions.harness_sidecar.runtime_components import ( + RUNTIME_COMPONENT_DEPENDENCIES, + RUNTIME_COMPONENT_ORDER, +) +from agentkit.extensions.harness_sidecar.sidecar_config import ( + HarnessSidecarConfig, + resolve_sidecar_config, + sidecar_config_to_env, +) + + +def test_internal_runtime_inventory_matches_single_wheel_contract() -> None: + assert RUNTIME_COMPONENT_ORDER == ( + "harness_core", + "ops", + "goal_runtime", + "model_proxy", + "mcp_gateway", + "browser_runtime", + "eval_runtime", + "shadow_runtime", + ) + + +def test_vertical_runtime_components_depend_only_on_harness_core() -> None: + assert RUNTIME_COMPONENT_DEPENDENCIES["harness_core"] == () + assert { + component: dependencies + for component, dependencies in RUNTIME_COMPONENT_DEPENDENCIES.items() + if component != "harness_core" + } == { + component: ("harness_core",) + for component in RUNTIME_COMPONENT_ORDER + if component != "harness_core" + } + + +def test_public_extras_never_install_the_private_runtime_wheel() -> None: + pyproject = tomllib.loads( + (Path(__file__).resolve().parents[3] / "pyproject.toml").read_text( + encoding="utf-8" + ) + ) + + runtime_requirement = "bytedance.agentkit_harness_sidecar" + project = pyproject["project"] + all_requirements = [ + *project.get("dependencies", []), + *( + requirement + for extra in project["optional-dependencies"].values() + for requirement in extra + ), + ] + + assert not any(runtime_requirement in item for item in all_requirements) + assert project["optional-dependencies"]["harness-sidecar"] == [] + + +def test_core_wheel_package_discovery_includes_sidecar_extension() -> None: + root = Path(__file__).resolve().parents[3] + pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + package_finder = pyproject["tool"]["setuptools"]["packages"]["find"] + extension_dir = root / "agentkit" / "extensions" / "harness_sidecar" + + assert package_finder["include"] == ["agentkit*"] + assert (extension_dir / "__init__.py").is_file() + assert not list(extension_dir.glob("**/pyproject.toml")) + + +def test_ops_profile_expands_product_defaults() -> None: + config = HarnessSidecarConfig(profile="ops") + + assert config.model_proxy.enabled is True + assert config.mcp_gateway.enabled is True + assert config.mcp_gateway.presets == ["sql_readonly"] + assert config.mcp_gateway.readonly_segments == ["*"] + assert config.mcp_gateway.policy["result_quality"]["empty_is_unhealthy"] is True + assert config.mcp_gateway.policy["budget"]["max_calls_per_session"] == 70 + assert config.runtime_flavor == "harness-sidecar" + assert config.required_runtime_components == [ + "harness_core", + "ops", + "goal_runtime", + "model_proxy", + "mcp_gateway", + ] + + +def test_explicit_values_override_profile_defaults() -> None: + config = resolve_sidecar_config( + { + "profile": "ops", + "model_proxy": {"enabled": False}, + "mcp_gateway": {"policy": {"large_result": {"max_bytes": 65_536}}}, + } + ) + + assert config.model_proxy.enabled is False + assert config.mcp_gateway.policy["large_result"]["max_bytes"] == 65_536 + assert config.mcp_gateway.policy["budget"]["max_calls_per_session"] == 70 + + +def test_unknown_profile_fails_fast() -> None: + with pytest.raises(ValueError, match="Unknown Harness Sidecar profile"): + HarnessSidecarConfig(profile="unknown") + + +def test_runtime_payload_excludes_public_launcher_details() -> None: + config = HarnessSidecarConfig( + profile="ops", runtime_command=["python", "fake-runtime.py"] + ) + + payload = config.runtime_payload() + + assert "runtime_command" not in payload + assert "startup_timeout_seconds" not in payload + assert payload["profile"] == "ops" + assert payload["runtime_components"] == config.required_runtime_components + + +def test_explicit_browser_component_resolves_dependency_closure() -> None: + config = HarnessSidecarConfig( + profile="default", + components=["browser"], + model_proxy={"enabled": False}, + ) + + assert config.runtime_flavor == "harness-sidecar" + assert config.required_runtime_components == [ + "harness_core", + "browser_runtime", + ] + + +@pytest.mark.parametrize("legacy_alias", ["ops", "ops_kernel"]) +def test_legacy_ops_component_alias_resolves_internal_dependency( + legacy_alias: str, +) -> None: + config = HarnessSidecarConfig( + profile="default", + components=[legacy_alias], + model_proxy={"enabled": False}, + ) + + assert config.required_runtime_components == ["harness_core", "ops"] + + +def test_full_optional_component_set_resolves_full_flavor() -> None: + config = HarnessSidecarConfig( + profile="ops", + components=["browser", "eval", "shadow"], + ) + + assert config.runtime_flavor == "harness-sidecar" + assert config.required_runtime_components[-3:] == [ + "browser_runtime", + "eval_runtime", + "shadow_runtime", + ] + + +def test_env_round_trip_keeps_product_semantics() -> None: + env = sidecar_config_to_env( + { + "profile": "ops", + "mcp_gateway": { + "presets": ["sql_readonly"], + "readonly_segments": ["bqmcp"], + }, + "components": ["browser"], + } + ) + config = HarnessSidecarConfig.from_env(env) + + assert config.enabled is True + assert config.profile == "ops" + assert config.mcp_gateway.presets == ["sql_readonly"] + assert config.mcp_gateway.readonly_segments == ["bqmcp"] + assert config.components == ["browser"] + + +def test_default_env_profile_does_not_enable_mcp_gateway_implicitly() -> None: + config = HarnessSidecarConfig.from_env({"HARNESS_SIDECAR_ENABLED": "true"}) + + assert config.profile == "default" + assert config.model_proxy.enabled is True + assert config.mcp_gateway.enabled is False + assert config.required_runtime_components == [ + "harness_core", + "goal_runtime", + "model_proxy", + ] + + +def test_ops_env_profile_keeps_profile_readonly_defaults() -> None: + config = HarnessSidecarConfig.from_env( + {"HARNESS_SIDECAR_ENABLED": "true", "HARNESS_PROFILE": "ops"} + ) + + assert config.mcp_gateway.enabled is True + assert config.mcp_gateway.presets == ["sql_readonly"] + assert config.mcp_gateway.readonly_segments == ["*"] + + +def test_mcp_upstreams_are_materialized_from_configured_env() -> None: + config = HarnessSidecarConfig.from_env( + { + "HARNESS_SIDECAR_ENABLED": "true", + "HARNESS_PROFILE": "ops", + "HARNESS_MCP_UPSTREAMS_ENV": "YUMC_MCP_URLS", + "YUMC_MCP_URLS": "http://mcp-a.example/mcp, http://mcp-b.example/mcp", + } + ) + + assert config.mcp_gateway.upstreams_env == "YUMC_MCP_URLS" + assert config.mcp_gateway.upstreams == [ + "http://mcp-a.example/mcp", + "http://mcp-b.example/mcp", + ] + + +def test_deploy_env_mapping_supports_nested_harness_sidecar() -> None: + env = to_runtime_env( + { + "description": "Yum China operations agent", + "harness": { + "enabled": True, + "profile": "ops", + "sidecar": { + "enabled": True, + "model_proxy": {"enabled": True}, + "mcp_gateway": {"enabled": True}, + }, + }, + } + ) + + assert env["HARNESS_SIDECAR_ENABLED"] == "true" + assert env["HARNESS_PROFILE"] == "ops" + assert env["HARNESS_MCP_PRESETS"] == "sql_readonly" + assert env["HARNESS_MCP_READONLY_SEGMENTS"] == "*" + assert env["HARNESS_ENABLED"] == "true" + assert env["DESCRIPTION"] == "Yum China operations agent" + assert not any(key.startswith("HARNESS_SIDECAR_MCP_GATEWAY_") for key in env) + + +def test_deploy_env_mapping_supports_boolean_sidecar_shorthand() -> None: + env = to_runtime_env({"harness": {"profile": "ops", "sidecar": True}}) + + assert env["HARNESS_SIDECAR_ENABLED"] == "true" + assert env["HARNESS_PROFILE"] == "ops" + assert env["HARNESS_MCP_GATEWAY_ENABLED"] == "true" + assert env["HARNESS_MCP_PRESETS"] == "sql_readonly" + assert env["HARNESS_MCP_READONLY_SEGMENTS"] == "*" + + +def test_product_component_overrides_drive_technical_config_and_plan_env() -> None: + env = sidecar_config_to_env( + { + "profile": "ops", + "component_overrides": { + "verifier": False, + "sql_readonly": False, + }, + } + ) + + plan = json.loads(env["HARNESS_SIDECAR_PLAN"]) + assert "verifier" not in plan["effective_components"] + assert "sql_readonly" not in plan["effective_components"] + assert env["HARNESS_MCP_PRESETS"] == "" + assert env["HARNESS_MCP_READONLY_SEGMENTS"] == "" + + +def test_disabled_sidecar_has_no_runtime_or_product_components() -> None: + config = HarnessSidecarConfig(profile="ops", enabled=False) + + assert config.required_runtime_components == [] + assert config.resolved_plan.effective_components == [] diff --git a/tests/toolkit/test_harness_extension_hooks.py b/tests/toolkit/test_harness_extension_hooks.py new file mode 100644 index 00000000..8c6d2d08 --- /dev/null +++ b/tests/toolkit/test_harness_extension_hooks.py @@ -0,0 +1,53 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Backward-compatible extension hooks used by optional Harness capabilities.""" + +from inspect import Parameter, signature + +from agentkit.toolkit.harness.config_builder import build_agentkit_config +from agentkit.toolkit.harness.deploy import deploy_harness + + +def test_legacy_config_builder_call_is_unchanged(): + config = build_agentkit_config( + "legacy-harness", + "cn-beijing", + {"EXISTING_ENV": "value"}, + None, + "Auto", + ) + + cloud = config["launch_types"]["cloud"] + assert cloud["runtime_id"] == "Auto" + assert "runtime_network" not in cloud + + +def test_cloud_config_overrides_are_opt_in(): + config = build_agentkit_config( + "extended-harness", + "cn-beijing", + {}, + cloud_config_overrides={"extension_value": "enabled"}, + ) + + assert config["launch_types"]["cloud"]["extension_value"] == "enabled" + + +def test_new_deploy_hooks_are_optional_keyword_only_parameters(): + parameters = signature(deploy_harness).parameters + + for name in ("runtime_env_builder", "cloud_config_overrides"): + assert parameters[name].kind is Parameter.KEYWORD_ONLY + assert parameters[name].default is None