Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 132 additions & 15 deletions .github/workflows/release-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:

jobs:
test:
name: Security tests (Python ${{ matrix.python-version }})
name: Release tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -52,7 +52,11 @@ jobs:
- name: Install package and test dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[identity-runtime]" pytest
python -m pip install ".[identity-runtime]" \
-e packages/agentkit-harness-sidecar-integration \
pytest \
"setuptools>=77" \
wheel

- name: Run identity and Agent Server security tests
run: |
Expand All @@ -62,6 +66,14 @@ jobs:
tests/apps/test_agent_server_invoke.py \
tests/apps/test_agent_server_loader.py

- name: Run Harness Sidecar integration tests
run: |
python -m pytest -q \
packages/agentkit-harness-sidecar-integration/tests \
tests/test_optional_sidecar_packaging.py \
tests/toolkit/cli/test_plugin_loader.py \
tests/toolkit/test_harness_extension_hooks.py

publish:
name: Build and publish
needs: test
Expand Down Expand Up @@ -153,13 +165,17 @@ jobs:
PY

- name: Build release artifacts
run: python -m build --outdir dist
run: |
python -m build --outdir dist/sdk
python -m build \
--outdir dist/integration \
packages/agentkit-harness-sidecar-integration

- name: Verify artifacts
shell: bash
run: |
set -euo pipefail
python -m twine check dist/*
python -m twine check dist/sdk/* dist/integration/*
python - <<'PY'
from __future__ import annotations

Expand All @@ -169,9 +185,17 @@ jobs:
from pathlib import Path

expected = "${{ steps.version.outputs.version }}"
dist = Path("dist")
wheel = next(dist.glob("agentkit_sdk_python-*.whl"))
sdist = next(dist.glob("agentkit_sdk_python-*.tar.gz"))
integration_version = "0.1.0"
sdk_dist = Path("dist/sdk")
integration_dist = Path("dist/integration")
wheel = next(sdk_dist.glob("agentkit_sdk_python-*.whl"))
sdist = next(sdk_dist.glob("agentkit_sdk_python-*.tar.gz"))
integration_wheel = next(
integration_dist.glob("agentkit_harness_sidecar_integration-*.whl")
)
integration_sdist = next(
integration_dist.glob("agentkit_harness_sidecar_integration-*.tar.gz")
)

with zipfile.ZipFile(wheel) as archive:
names = archive.namelist()
Expand All @@ -181,18 +205,61 @@ jobs:
)
metadata = archive.read(metadata_name).decode()

with zipfile.ZipFile(integration_wheel) as archive:
integration_names = archive.namelist()
integration_metadata_name = next(
name for name in integration_names
if name.endswith(".dist-info/METADATA")
)
integration_metadata = archive.read(
integration_metadata_name
).decode()

with tarfile.open(sdist) as archive:
root = archive.getnames()[0].split("/", 1)[0]
pyproject = archive.extractfile(f"{root}/pyproject.toml").read().decode()
sdist_version_py = (
archive.extractfile(f"{root}/agentkit/version.py").read().decode()
)

with tarfile.open(integration_sdist) as archive:
integration_root = archive.getnames()[0].split("/", 1)[0]
integration_pyproject = archive.extractfile(
f"{integration_root}/pyproject.toml"
).read().decode()

normalized_sdk_metadata = metadata.lower().replace("_", "-")
normalized_integration_metadata = (
integration_metadata.lower().replace("_", "-")
)
module_prefix = "agentkit/extensions/harness_sidecar/"

checks = {
"wheel metadata": f"Version: {expected}" in metadata,
"wheel version.py": f'VERSION = "{expected}"' in version_py,
"sdist pyproject": f'version = "{expected}"' in pyproject,
"sdist version.py": f'VERSION = "{expected}"' in sdist_version_py,
"SDK excludes optional integration modules": not any(
name.startswith(module_prefix) for name in names
),
"SDK declares the Sidecar extra": (
"provides-extra: harness-sidecar" in normalized_sdk_metadata
and "agentkit-harness-sidecar-integration" in normalized_sdk_metadata
),
"integration wheel version": (
f"version: {integration_version}"
in normalized_integration_metadata
),
"integration depends on SDK 0.8.2": (
"agentkit-sdk-python<0.9.0,>=0.8.2"
in normalized_integration_metadata
),
"integration wheel contains public modules": any(
name.startswith(module_prefix) for name in integration_names
),
"integration sdist version": (
f'version = "{integration_version}"' in integration_pyproject
),
}
failed = [name for name, ok in checks.items() if not ok]
if failed:
Expand All @@ -212,29 +279,79 @@ jobs:
import venv
from pathlib import Path

smoke = Path("/tmp/agentkit-wheel-smoke")
venv.EnvBuilder(with_pip=True, clear=True).create(smoke)
python = smoke / "bin" / "python"
wheel = next(Path("dist").glob("agentkit_sdk_python-*.whl"))
base_smoke = Path("/tmp/agentkit-base-wheel-smoke")
sidecar_smoke = Path("/tmp/agentkit-sidecar-wheel-smoke")
venv.EnvBuilder(with_pip=True, clear=True).create(base_smoke)
venv.EnvBuilder(with_pip=True, clear=True).create(sidecar_smoke)
base_python = base_smoke / "bin" / "python"
sidecar_python = sidecar_smoke / "bin" / "python"
sdk_dist = Path("dist/sdk")
integration_dist = Path("dist/integration")
wheel = next(sdk_dist.glob("agentkit_sdk_python-*.whl"))

subprocess.check_call(
[str(python), "-m", "pip", "install", f"{wheel}[identity-runtime]"]
[
str(base_python),
"-m",
"pip",
"install",
f"{wheel}[identity-runtime]",
]
)
subprocess.check_call(
[
str(python),
str(base_python),
"-I",
"-c",
(
"import importlib.util; "
"from agentkit.identity import IdentityRuntimeConfig, "
"RuntimeIdentity; "
"from agentkit.apps import AgentkitAgentServerApp"
"from agentkit.apps import AgentkitAgentServerApp; "
"assert importlib.util.find_spec("
"'agentkit.extensions.harness_sidecar') is None"
),
]
)

subprocess.check_call(
[
str(sidecar_python),
"-m",
"pip",
"install",
"--find-links",
str(integration_dist),
f"{wheel}[identity-runtime,harness-sidecar]",
]
)
subprocess.check_call(
[
str(sidecar_python),
"-I",
"-c",
(
"from agentkit.identity import IdentityRuntimeConfig, "
"RuntimeIdentity; "
"from agentkit.apps import AgentkitAgentServerApp; "
"from agentkit.extensions.harness_sidecar import "
"HarnessSidecarConfig, resolve_harness_sidecar_selection"
),
]
)
PY

- name: Publish to PyPI
- name: Publish Harness Sidecar integration to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_SIDECAR_INTEGRATION_API_TOKEN }}
packages-dir: dist/integration
skip-existing: true

- name: Publish AgentKit SDK to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist/sdk
17 changes: 17 additions & 0 deletions agentkit/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -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] = []
2 changes: 2 additions & 0 deletions agentkit/toolkit/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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.toolkit.cli.plugin_loader import load_cli_plugins

# Note: Avoid importing heavy packages at the top to keep CLI startup fast

Expand Down Expand Up @@ -134,6 +135,7 @@ def main(
app.add_typer(add_app, name="add")
app.add_typer(list_app, name="list")
app.add_typer(delete_app, name="delete")
load_cli_plugins(app)


if __name__ == "__main__":
Expand Down
72 changes: 72 additions & 0 deletions agentkit/toolkit/cli/plugin_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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.

"""Discover optional AgentKit CLI plugins without importing them from core."""

from __future__ import annotations

from collections.abc import Iterable
from importlib.metadata import EntryPoint, entry_points
from typing import Any

import typer

CLI_PLUGIN_GROUP = "agentkit.cli_plugins"


def load_cli_plugins(
root: typer.Typer,
*,
plugins: Iterable[EntryPoint] | None = None,
) -> None:
"""Register installed CLI plugins; an empty environment is a no-op."""

discovered = (
entry_points(group=CLI_PLUGIN_GROUP) if plugins is None else list(plugins)
)
for entry_point in sorted(discovered, key=lambda item: item.name):
try:
plugin: Any = entry_point.load()
except Exception as error:
_warn_plugin_failure(entry_point.name, "load", error)
continue

try:
if isinstance(plugin, typer.Typer):
root.add_typer(plugin, name=entry_point.name)
continue
if callable(plugin):
plugin(root)
continue
typer.echo(
"Warning: ignoring AgentKit CLI plugin "
f"{entry_point.name!r}: expected a Typer or callable, "
f"got {type(plugin).__name__}.",
err=True,
)
except Exception as error:
_warn_plugin_failure(entry_point.name, "registration", error)


def _warn_plugin_failure(name: str, stage: str, error: Exception) -> None:
"""Report an isolated plugin failure without leaking exception details."""

typer.echo(
f"Warning: AgentKit CLI plugin {name!r} failed during {stage} "
f"({type(error).__name__}); continuing without it.",
err=True,
)


__all__ = ["CLI_PLUGIN_GROUP", "load_cli_plugins"]
3 changes: 3 additions & 0 deletions agentkit/toolkit/harness/config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion agentkit/toolkit/harness/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
Loading