From e8491db178232d176bd39572661762b0ad49c44f Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 13 Jul 2026 21:08:04 +0800 Subject: [PATCH] feat(harness): support runtime network configuration --- agentkit/toolkit/cli/cli_deploy.py | 64 +++++++++++-- agentkit/toolkit/harness/config_builder.py | 6 ++ agentkit/toolkit/harness/deploy.py | 27 ++++++ .../cli/test_cli_deploy_harness_network.py | 90 +++++++++++++++++++ 4 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 tests/toolkit/cli/test_cli_deploy_harness_network.py diff --git a/agentkit/toolkit/cli/cli_deploy.py b/agentkit/toolkit/cli/cli_deploy.py index 2fc45d12..0a8dabad 100644 --- a/agentkit/toolkit/cli/cli_deploy.py +++ b/agentkit/toolkit/cli/cli_deploy.py @@ -15,7 +15,7 @@ """AgentKit CLI - Deploy command implementation.""" from pathlib import Path -from typing import Callable, Dict, Optional +from typing import Callable, Dict, List, Optional import typer from rich.console import Console @@ -63,6 +63,27 @@ def deploy_command( "--allowed-id", help="Comma-separated allowed client IDs for OAuth2/JWT auth (harness deploy).", ), + runtime_network_mode: Optional[str] = typer.Option( + None, + "--runtime-network-mode", + help="Harness Runtime network mode: public, private, or both.", + ), + runtime_vpc_id: Optional[str] = typer.Option( + None, + "--runtime-vpc-id", + help="Harness Runtime VPC ID (required for private/both).", + ), + runtime_subnet_ids: Optional[List[str]] = typer.Option( + None, + "--runtime-subnet-id", + "--runtime-subnet-ids", + help="Harness Runtime subnet ID. Repeat for multiple subnets.", + ), + runtime_enable_shared_internet_access: Optional[bool] = typer.Option( + None, + "--runtime-enable-shared-internet-access/--no-runtime-enable-shared-internet-access", + help="Enable shared internet egress for a private Harness Runtime.", + ), yes: bool = typer.Option( False, "--yes", @@ -71,10 +92,6 @@ def deploy_command( ), ): """Deploy the Agent to target environment.""" - from agentkit.toolkit.executors import DeployExecutor - from agentkit.toolkit.cli.console_reporter import ConsoleReporter - from agentkit.toolkit.context import ExecutionContext - if harness is not None: _deploy_harness( name=harness, @@ -83,10 +100,33 @@ def deploy_command( secret_key=volcengine_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 + ), assume_yes=yes, ) return + if any( + value is not None + for value in ( + runtime_network_mode, + runtime_vpc_id, + runtime_subnet_ids, + runtime_enable_shared_internet_access, + ) + ): + raise typer.BadParameter( + "Runtime network options require --harness.", param_hint="--harness" + ) + + from agentkit.toolkit.executors import DeployExecutor + from agentkit.toolkit.cli.console_reporter import ConsoleReporter + from agentkit.toolkit.context import ExecutionContext + console.print(f"[green]Deploying with {config_file}[/green]") # Set execution context - CLI uses ConsoleReporter (with colored output and progress) @@ -125,6 +165,10 @@ def _deploy_harness( secret_key, discovery_url, allowed_id, + runtime_network_mode, + runtime_vpc_id, + runtime_subnet_ids, + runtime_enable_shared_internet_access, assume_yes: bool = False, ): """Deploy a harness spec .harness.json from the current directory.""" @@ -159,6 +203,12 @@ def _deploy_harness( 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, ) @@ -185,6 +235,4 @@ def _deploy_harness( if meta.get("runtime_apikey"): console.print(f"[green]API key: {meta['runtime_apikey']}[/green]") if endpoint: - console.print( - f"[green]Recorded in {(Path.cwd() / 'harness.json')}[/green]" - ) + console.print(f"[green]Recorded in {(Path.cwd() / 'harness.json')}[/green]") diff --git a/agentkit/toolkit/harness/config_builder.py b/agentkit/toolkit/harness/config_builder.py index f457a559..1b8736cd 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", + runtime_network: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Build the cloud AgentKit launch config dict (auto-provision). @@ -38,6 +39,9 @@ def build_agentkit_config( When ``auth`` (a normalized ``{discovery_url, allowed_ids}`` block) is given, the runtime is gated by OAuth2/JWT (``custom_jwt``); otherwise it keeps the default API-key auth (``key_auth``). + + ``runtime_network`` is forwarded to the cloud runner for ``CreateRuntime``. + The runner converts it into the platform's public/private network fields. """ cloud: Dict[str, Any] = { "region": region, @@ -64,6 +68,8 @@ def build_agentkit_config( cloud["runtime_apikey_name"] = "Auto" cloud["runtime_apikey"] = "Auto" cloud["runtime_jwt_allowed_clients"] = [] + if runtime_network: + cloud["runtime_network"] = runtime_network return { "common": { "agent_name": runtime_name, diff --git a/agentkit/toolkit/harness/deploy.py b/agentkit/toolkit/harness/deploy.py index 9ad925fd..e170c147 100644 --- a/agentkit/toolkit/harness/deploy.py +++ b/agentkit/toolkit/harness/deploy.py @@ -198,6 +198,10 @@ def deploy_harness( secret_key: Optional[str] = None, discovery_url: Optional[str] = None, allowed_id: Optional[str] = None, + runtime_network_mode: Optional[str] = None, + runtime_vpc_id: Optional[str] = None, + runtime_subnet_ids: Optional[list[str]] = None, + runtime_enable_shared_internet_access: Optional[bool] = None, reporter: Optional[Reporter] = None, on_conflict: Optional[Callable[[Dict[str, Any]], bool]] = None, ) -> LifecycleResult: @@ -228,6 +232,12 @@ 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_network_mode: Runtime network mode: ``public``, ``private``, or + ``both``. Network settings apply only when creating a Runtime. + runtime_vpc_id: VPC ID required by ``private`` / ``both`` modes. + runtime_subnet_ids: Optional subnet IDs for the Runtime VPC attachment. + runtime_enable_shared_internet_access: Enable shared internet egress for + a private network. 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. @@ -279,6 +289,17 @@ def deploy_harness( resolved_region = region or os.getenv("VOLCENGINE_REGION") or "cn-beijing" + runtime_network = { + key: value + for key, value in { + "mode": runtime_network_mode, + "vpc_id": runtime_vpc_id, + "subnet_ids": runtime_subnet_ids, + "enable_shared_internet_access": runtime_enable_shared_internet_access, + }.items() + if value is not None + } + # Resolve a name collision into a deploy mode. The harness config defaults to # `runtime_id: Auto` (create new); an existing same-name harness can instead # be updated in place (new version) after confirmation. @@ -300,6 +321,11 @@ def deploy_harness( "Refusing to update it." ) existing_id = match["runtime_id"] + if runtime_network: + raise ValueError( + "Runtime network configuration is supported only when creating a " + "Runtime. Delete the existing harness Runtime and deploy again." + ) version = _get_runtime_version(client, existing_id) version_label = f"v{version}" if version is not None else "unknown" if reporter: @@ -328,6 +354,7 @@ def deploy_harness( runtime_envs, auth, runtime_id=update_runtime_id or "Auto", + runtime_network=runtime_network, ) # AgentKit's launch path exposes no hook for runtime tags, so tag the runtime diff --git a/tests/toolkit/cli/test_cli_deploy_harness_network.py b/tests/toolkit/cli/test_cli_deploy_harness_network.py new file mode 100644 index 00000000..8c5349da --- /dev/null +++ b/tests/toolkit/cli/test_cli_deploy_harness_network.py @@ -0,0 +1,90 @@ +# 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. + +"""Tests for Harness Runtime network deployment options.""" + +from typer.testing import CliRunner + +from agentkit.toolkit.cli import cli_deploy +from agentkit.toolkit.cli.cli import app +from agentkit.toolkit.harness.config_builder import build_agentkit_config + + +runner = CliRunner() + + +def test_harness_network_options_are_forwarded(monkeypatch): + captured = {} + + def fake_deploy_harness(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(cli_deploy, "_deploy_harness", fake_deploy_harness) + + result = runner.invoke( + app, + [ + "deploy", + "--harness", + "test-agent", + "--runtime-network-mode", + "private", + "--runtime-vpc-id", + "vpc-123", + "--runtime-subnet-id", + "subnet-a", + "--runtime-subnet-id", + "subnet-b", + "--runtime-enable-shared-internet-access", + ], + ) + + assert result.exit_code == 0, result.output + assert captured["runtime_network_mode"] == "private" + assert captured["runtime_vpc_id"] == "vpc-123" + assert captured["runtime_subnet_ids"] == ["subnet-a", "subnet-b"] + assert captured["runtime_enable_shared_internet_access"] is True + + +def test_network_options_require_harness(): + result = runner.invoke(app, ["deploy", "--runtime-network-mode", "private"]) + + assert result.exit_code != 0 + assert "Runtime network options require --harness" in result.output + + +def test_config_builder_includes_runtime_network(): + runtime_network = { + "mode": "private", + "vpc_id": "vpc-123", + "subnet_ids": ["subnet-a"], + "enable_shared_internet_access": True, + } + + config = build_agentkit_config( + runtime_name="test-agent", + region="cn-shanghai", + envs={}, + runtime_network=runtime_network, + ) + + assert config["launch_types"]["cloud"]["runtime_network"] == runtime_network + + +def test_config_builder_omits_empty_runtime_network(): + config = build_agentkit_config( + runtime_name="test-agent", region="cn-shanghai", envs={} + ) + + assert "runtime_network" not in config["launch_types"]["cloud"]