feat(interfaces): add server client stubs
CI / lint (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 30s
CI / build (pull_request) Successful in 15s
CI / security (pull_request) Successful in 29s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m35s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m39s
CI / lint (push) Successful in 24s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 38s
CI / quality (push) Successful in 32s
CI / build (push) Successful in 15s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m5s
CI / integration_tests (push) Successful in 4m42s
CI / docker (push) Successful in 42s
CI / coverage (push) Successful in 6m53s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 24m12s
CI / lint (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 30s
CI / build (pull_request) Successful in 15s
CI / security (pull_request) Successful in 29s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m35s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m39s
CI / lint (push) Successful in 24s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 38s
CI / quality (push) Successful in 32s
CI / build (push) Successful in 15s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m5s
CI / integration_tests (push) Successful in 4m42s
CI / docker (push) Successful in 42s
CI / coverage (push) Successful in 6m53s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 24m12s
Add protocol stubs for remote server communication infrastructure: - ServerClient: Health check and version negotiation protocol - RemoteExecutionClient: Remote plan execution and status protocol - AuthClient: Authentication and token management protocol - StubServerClient/StubRemoteExecutionClient/StubAuthClient: Stub implementations raising NotImplementedError for all methods - ServerConnectionConfig: Validated config model (server_url, namespace, auth_token_ref, tls_verify) - Config keys: core.server_url, core.server_namespace, core.server_tls_verify - CLI: agents connect <url> stub command with explicit warning - CLI: agents info shows Server Mode (disabled/stubbed) ISSUES CLOSED: #201
This commit was merged in pull request #507.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""ASV benchmarks for server client stubs and connection config.
|
||||
|
||||
Measures the performance of:
|
||||
- ServerConnectionConfig construction and validation
|
||||
- StubServerClient/StubRemoteExecutionClient/StubAuthClient instantiation
|
||||
- Config key registry lookups for server.* keys
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure the local *source* tree is importable even when ASV has an
|
||||
# older build of the package installed.
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
# Force-reload so ASV picks up the source tree version.
|
||||
import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from cleveragents.acp.clients import ( # noqa: E402
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config resolution benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ServerConfigResolutionSuite:
|
||||
"""Benchmark ServerConnectionConfig construction overhead."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def time_create_config_minimal(self) -> None:
|
||||
ServerConnectionConfig(server_url="https://agents.example.com")
|
||||
|
||||
def time_create_config_full(self) -> None:
|
||||
ServerConnectionConfig(
|
||||
server_url="https://agents.example.com",
|
||||
namespace="my-team",
|
||||
auth_token_ref="CLEVERAGENTS_SERVER_TOKEN",
|
||||
tls_verify=False,
|
||||
)
|
||||
|
||||
def time_registry_lookup_server_url(self) -> None:
|
||||
_ = _REGISTRY["server.url"]
|
||||
|
||||
def time_registry_lookup_server_tls(self) -> None:
|
||||
_ = _REGISTRY["server.tls-verify"]
|
||||
|
||||
def time_registry_lookup_server_namespace(self) -> None:
|
||||
_ = _REGISTRY["server.namespace"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub client benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class StubClientInstantiationSuite:
|
||||
"""Benchmark stub client instantiation overhead."""
|
||||
|
||||
timeout = 60
|
||||
|
||||
def time_instantiate_stub_server_client(self) -> None:
|
||||
StubServerClient()
|
||||
|
||||
def time_instantiate_stub_execution_client(self) -> None:
|
||||
StubRemoteExecutionClient()
|
||||
|
||||
def time_instantiate_stub_auth_client(self) -> None:
|
||||
StubAuthClient()
|
||||
|
||||
def time_instantiate_all_stubs(self) -> None:
|
||||
StubServerClient()
|
||||
StubRemoteExecutionClient()
|
||||
StubAuthClient()
|
||||
@@ -0,0 +1,83 @@
|
||||
# Server Client Stubs
|
||||
|
||||
## Overview
|
||||
|
||||
CleverAgents defines three protocol interfaces for server communication. In
|
||||
the current release all implementations are **stubs** that raise
|
||||
`NotImplementedError`. When server mode is implemented as a separate project
|
||||
the concrete clients will replace these stubs.
|
||||
|
||||
## Client Protocols
|
||||
|
||||
### ServerClient
|
||||
|
||||
Health check and version negotiation with the server.
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `health_check()` | `bool` | Check server health |
|
||||
| `get_version()` | `str` | Get server version |
|
||||
|
||||
### RemoteExecutionClient
|
||||
|
||||
Remote plan execution and status tracking.
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `execute_plan(plan_id)` | `dict` | Submit plan for remote execution |
|
||||
| `get_plan_status(plan_id)` | `dict` | Query remote plan status |
|
||||
| `cancel_plan(plan_id)` | `bool` | Cancel remote plan execution |
|
||||
|
||||
### AuthClient
|
||||
|
||||
Authentication and token management.
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `authenticate(token)` | `bool` | Authenticate with server |
|
||||
| `validate_token(token)` | `bool` | Validate token validity |
|
||||
|
||||
## Stub Implementations
|
||||
|
||||
- `StubServerClient` — raises `NotImplementedError` for all methods
|
||||
- `StubRemoteExecutionClient` — validates arguments, then raises
|
||||
`NotImplementedError`
|
||||
- `StubAuthClient` — validates arguments, then raises `NotImplementedError`
|
||||
|
||||
## ServerConnectionConfig
|
||||
|
||||
Validated Pydantic model for server connection parameters.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `server_url` | `str` | *(required)* | Server URL (must be `http://` or `https://`) |
|
||||
| `namespace` | `str` | `"default"` | Namespace on the server |
|
||||
| `auth_token_ref` | `str \| None` | `None` | Reference to auth token |
|
||||
| `tls_verify` | `bool` | `True` | Verify TLS certificates |
|
||||
|
||||
## Config Keys
|
||||
|
||||
| Key | Type | Default | Env Variable | Description |
|
||||
|-----|------|---------|-------------|-------------|
|
||||
| `server.url` | string | *(not set)* | `CLEVERAGENTS_SERVER_URL` | Server URL |
|
||||
| `server.token` | string | *(not set)* | `CLEVERAGENTS_SERVER_TOKEN` | Auth token |
|
||||
| `server.tls-verify` | bool | `true` | `CLEVERAGENTS_SERVER_TLS_VERIFY` | TLS verification |
|
||||
| `server.namespace` | string | `"default"` | `CLEVERAGENTS_SERVER_NAMESPACE` | Server namespace |
|
||||
|
||||
## Connection Behavior
|
||||
|
||||
**Current mode: stub**
|
||||
|
||||
When `server.url` is configured, the system reports `Server Mode: stubbed` in
|
||||
`agents info` output. No actual network communication occurs. The `agents server
|
||||
connect <url>` command persists the URL to configuration and prints an explicit
|
||||
warning that server connection is not yet implemented.
|
||||
|
||||
When `server.url` is not configured, the system reports `Server Mode: disabled`.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```
|
||||
agents server connect <url> [--namespace NS] [--tls-verify|--no-tls-verify]
|
||||
agents server status
|
||||
```
|
||||
@@ -0,0 +1,183 @@
|
||||
@phase2 @acp @server-stubs
|
||||
Feature: Server Client Stubs and Connection Config
|
||||
As a developer
|
||||
I want protocol stubs for server communication
|
||||
So that the server client interfaces are defined before implementation
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ServerConnectionConfig validation
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: ServerConnectionConfig accepts valid HTTPS URL
|
||||
When I create a ServerConnectionConfig with url "https://agents.example.com"
|
||||
Then the config server_url should be "https://agents.example.com"
|
||||
And the config namespace should be "default"
|
||||
And the config tls_verify should be true
|
||||
|
||||
Scenario: ServerConnectionConfig accepts valid HTTP URL
|
||||
When I create a ServerConnectionConfig with url "http://localhost:8080"
|
||||
Then the config server_url should be "http://localhost:8080"
|
||||
|
||||
Scenario: ServerConnectionConfig accepts custom namespace
|
||||
When I create a ServerConnectionConfig with url "https://agents.example.com" and namespace "my-team"
|
||||
Then the config namespace should be "my-team"
|
||||
|
||||
Scenario: ServerConnectionConfig accepts auth_token_ref
|
||||
When I create a ServerConnectionConfig with url "https://agents.example.com" and auth_token_ref "CLEVERAGENTS_SERVER_TOKEN"
|
||||
Then the config auth_token_ref should be "CLEVERAGENTS_SERVER_TOKEN"
|
||||
|
||||
Scenario: ServerConnectionConfig rejects empty URL
|
||||
When I try to create a ServerConnectionConfig with empty url
|
||||
Then a server config validation error should be raised
|
||||
|
||||
Scenario: ServerConnectionConfig rejects non-HTTP URL
|
||||
When I try to create a ServerConnectionConfig with url "ftp://agents.example.com"
|
||||
Then a server config validation error should be raised
|
||||
|
||||
Scenario: ServerConnectionConfig rejects whitespace-only URL
|
||||
When I try to create a ServerConnectionConfig with url " "
|
||||
Then a server config validation error should be raised
|
||||
|
||||
Scenario: ServerConnectionConfig rejects empty namespace
|
||||
When I try to create a ServerConnectionConfig with url "https://ok.com" and empty namespace
|
||||
Then a server config validation error should be raised
|
||||
|
||||
Scenario: ServerConnectionConfig is frozen
|
||||
When I create a ServerConnectionConfig with url "https://agents.example.com"
|
||||
Then the config should be immutable
|
||||
|
||||
Scenario: ServerConnectionConfig tls_verify defaults to true
|
||||
When I create a ServerConnectionConfig with url "https://agents.example.com"
|
||||
Then the config tls_verify should be true
|
||||
|
||||
Scenario: ServerConnectionConfig with tls_verify false
|
||||
When I create a ServerConnectionConfig with url "http://localhost:8080" and tls_verify false
|
||||
Then the config tls_verify should be false
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# StubServerClient — all methods raise NotImplementedError
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: StubServerClient health_check raises NotImplementedError
|
||||
Given a new StubServerClient
|
||||
When I call health_check on the stub server client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubServerClient get_version raises NotImplementedError
|
||||
Given a new StubServerClient
|
||||
When I call get_version on the stub server client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# StubRemoteExecutionClient — all methods raise NotImplementedError
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: StubRemoteExecutionClient execute_plan raises NotImplementedError
|
||||
Given a new StubRemoteExecutionClient
|
||||
When I call execute_plan with plan_id "PLAN001" on the stub execution client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubRemoteExecutionClient get_plan_status raises NotImplementedError
|
||||
Given a new StubRemoteExecutionClient
|
||||
When I call get_plan_status with plan_id "PLAN001" on the stub execution client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubRemoteExecutionClient cancel_plan raises NotImplementedError
|
||||
Given a new StubRemoteExecutionClient
|
||||
When I call cancel_plan with plan_id "PLAN001" on the stub execution client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubRemoteExecutionClient rejects empty plan_id
|
||||
Given a new StubRemoteExecutionClient
|
||||
When I call execute_plan with empty plan_id on the stub execution client
|
||||
Then a ValueError should be raised for empty plan_id
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# StubAuthClient — all methods raise NotImplementedError
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: StubAuthClient authenticate raises NotImplementedError
|
||||
Given a new StubAuthClient
|
||||
When I call authenticate with token "tok_test" on the stub auth client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubAuthClient validate_token raises NotImplementedError
|
||||
Given a new StubAuthClient
|
||||
When I call validate_token with token "tok_test" on the stub auth client
|
||||
Then a NotImplementedError should be raised with message containing "not yet implemented"
|
||||
|
||||
Scenario: StubAuthClient rejects empty token
|
||||
Given a new StubAuthClient
|
||||
When I call authenticate with empty token on the stub auth client
|
||||
Then a ValueError should be raised for empty token
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Protocol conformance
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: StubServerClient conforms to ServerClient protocol
|
||||
Then StubServerClient should be a runtime instance of ServerClient
|
||||
|
||||
Scenario: StubRemoteExecutionClient conforms to RemoteExecutionClient protocol
|
||||
Then StubRemoteExecutionClient should be a runtime instance of RemoteExecutionClient
|
||||
|
||||
Scenario: StubAuthClient conforms to AuthClient protocol
|
||||
Then StubAuthClient should be a runtime instance of AuthClient
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Config keys registration
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: server.url config key is registered
|
||||
Then config key "server.url" should be registered
|
||||
|
||||
Scenario: server.token config key is registered
|
||||
Then config key "server.token" should be registered
|
||||
|
||||
Scenario: server.tls-verify config key is registered
|
||||
Then config key "server.tls-verify" should be registered
|
||||
|
||||
Scenario: server.namespace config key is registered
|
||||
Then config key "server.namespace" should be registered
|
||||
|
||||
Scenario: server.tls-verify defaults to true
|
||||
Then config key "server.tls-verify" should have default true
|
||||
|
||||
Scenario: server.namespace defaults to "default"
|
||||
Then config key "server.namespace" should have default "default"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Config keys load from environment
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: server.url loads from CLEVERAGENTS_SERVER_URL environment
|
||||
Then config key "server.url" should use env var "CLEVERAGENTS_SERVER_URL"
|
||||
|
||||
Scenario: server.tls-verify loads from CLEVERAGENTS_SERVER_TLS_VERIFY environment
|
||||
Then config key "server.tls-verify" should use env var "CLEVERAGENTS_SERVER_TLS_VERIFY"
|
||||
|
||||
Scenario: server.namespace loads from CLEVERAGENTS_SERVER_NAMESPACE environment
|
||||
Then config key "server.namespace" should use env var "CLEVERAGENTS_SERVER_NAMESPACE"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# CLI connect command
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: CLI server connect prints stub warning
|
||||
Given a temporary home directory for server tests
|
||||
When I invoke server connect with url "https://stub.example.com" and format "json"
|
||||
Then the server connect output should contain "not yet implemented"
|
||||
And the server connect output should contain "stubbed"
|
||||
|
||||
Scenario: CLI server connect rejects invalid URL
|
||||
Given a temporary home directory for server tests
|
||||
When I invoke server connect with url "ftp://bad-url" and format "json"
|
||||
Then the server connect should exit with non-zero code
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Server mode resolution
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
Scenario: resolve_server_mode returns disabled when no URL configured
|
||||
Given no server URL is configured
|
||||
Then resolve_server_mode should return "disabled"
|
||||
@@ -159,7 +159,7 @@ def step_verify_catalog_sections(context: Any) -> None:
|
||||
|
||||
@then("the registry should contain at least 20 keys")
|
||||
def step_verify_catalog_count(context: Any) -> None:
|
||||
# _build_catalog registers ~103 keys from the source
|
||||
# _build_catalog registers ~105 keys from the source
|
||||
assert len(_REGISTRY) >= 20, f"Expected >=20 keys, got {len(_REGISTRY)}"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
"""Step definitions for server client stubs Behave scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from behave import given, then, use_step_matcher, when
|
||||
from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.acp.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.application.services.config_service import _REGISTRY
|
||||
|
||||
use_step_matcher("re")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ServerConnectionConfig — creation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when(r'I create a ServerConnectionConfig with url "(?P<url>[^"]+)"')
|
||||
def step_create_config(context: Context, url: str) -> None:
|
||||
context.server_config = ServerConnectionConfig(server_url=url)
|
||||
|
||||
|
||||
@when(
|
||||
r'I create a ServerConnectionConfig with url "(?P<url>[^"]+)" '
|
||||
r'and namespace "(?P<ns>[^"]+)"'
|
||||
)
|
||||
def step_create_config_ns(context: Context, url: str, ns: str) -> None:
|
||||
context.server_config = ServerConnectionConfig(server_url=url, namespace=ns)
|
||||
|
||||
|
||||
@when(
|
||||
r'I create a ServerConnectionConfig with url "(?P<url>[^"]+)" '
|
||||
r'and auth_token_ref "(?P<ref>[^"]+)"'
|
||||
)
|
||||
def step_create_config_auth(context: Context, url: str, ref: str) -> None:
|
||||
context.server_config = ServerConnectionConfig(server_url=url, auth_token_ref=ref)
|
||||
|
||||
|
||||
@when(
|
||||
r'I create a ServerConnectionConfig with url "(?P<url>[^"]+)" '
|
||||
r"and tls_verify false"
|
||||
)
|
||||
def step_create_config_tls_false(context: Context, url: str) -> None:
|
||||
context.server_config = ServerConnectionConfig(server_url=url, tls_verify=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ServerConnectionConfig — assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then(r'the config server_url should be "(?P<expected>[^"]+)"')
|
||||
def step_config_url(context: Context, expected: str) -> None:
|
||||
assert context.server_config.server_url == expected, (
|
||||
f"Expected '{expected}', got '{context.server_config.server_url}'"
|
||||
)
|
||||
|
||||
|
||||
@then(r'the config namespace should be "(?P<expected>[^"]+)"')
|
||||
def step_config_namespace(context: Context, expected: str) -> None:
|
||||
assert context.server_config.namespace == expected, (
|
||||
f"Expected '{expected}', got '{context.server_config.namespace}'"
|
||||
)
|
||||
|
||||
|
||||
@then(r'the config auth_token_ref should be "(?P<expected>[^"]+)"')
|
||||
def step_config_auth_ref(context: Context, expected: str) -> None:
|
||||
assert context.server_config.auth_token_ref == expected, (
|
||||
f"Expected '{expected}', got '{context.server_config.auth_token_ref}'"
|
||||
)
|
||||
|
||||
|
||||
@then(r"the config tls_verify should be true")
|
||||
def step_config_tls_true(context: Context) -> None:
|
||||
assert context.server_config.tls_verify is True
|
||||
|
||||
|
||||
@then(r"the config tls_verify should be false")
|
||||
def step_config_tls_false(context: Context) -> None:
|
||||
assert context.server_config.tls_verify is False
|
||||
|
||||
|
||||
@then(r"the config should be immutable")
|
||||
def step_config_immutable(context: Context) -> None:
|
||||
cfg = context.server_config
|
||||
raised = False
|
||||
try:
|
||||
cfg.__setattr__("server_url", "http://changed.example.com")
|
||||
except (ValidationError, AttributeError, TypeError):
|
||||
raised = True
|
||||
assert raised, "Setting attribute on frozen model should have raised an error"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ServerConnectionConfig — validation errors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when(r"I try to create a ServerConnectionConfig with empty url")
|
||||
def step_create_config_empty_url(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
try:
|
||||
ServerConnectionConfig(server_url="")
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
|
||||
|
||||
@when(r'I try to create a ServerConnectionConfig with url "(?P<url>[^"]+)"')
|
||||
def step_create_config_invalid_url(context: Context, url: str) -> None:
|
||||
context.caught_error = None
|
||||
try:
|
||||
ServerConnectionConfig(server_url=url)
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
|
||||
|
||||
@when(
|
||||
r'I try to create a ServerConnectionConfig with url "(?P<url>[^"]+)" '
|
||||
r"and empty namespace"
|
||||
)
|
||||
def step_create_config_empty_ns(context: Context, url: str) -> None:
|
||||
context.caught_error = None
|
||||
try:
|
||||
ServerConnectionConfig(server_url=url, namespace="")
|
||||
except ValidationError as exc:
|
||||
context.caught_error = exc
|
||||
|
||||
|
||||
@then(r"a server config validation error should be raised")
|
||||
def step_server_config_validation_error(context: Context) -> None:
|
||||
assert context.caught_error is not None, "Expected a ValidationError"
|
||||
assert isinstance(context.caught_error, ValidationError), (
|
||||
f"Expected ValidationError, got {type(context.caught_error)}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StubServerClient
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"a new StubServerClient")
|
||||
def step_stub_server_client(context: Context) -> None:
|
||||
context.stub_server = StubServerClient()
|
||||
|
||||
|
||||
@when(r"I call health_check on the stub server client")
|
||||
def step_call_health_check(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_server.health_check()
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(r"I call get_version on the stub server client")
|
||||
def step_call_get_version(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_server.get_version()
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StubRemoteExecutionClient
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"a new StubRemoteExecutionClient")
|
||||
def step_stub_exec_client(context: Context) -> None:
|
||||
context.stub_exec = StubRemoteExecutionClient()
|
||||
|
||||
|
||||
@when(
|
||||
r'I call execute_plan with plan_id "(?P<plan_id>[^"]+)" '
|
||||
r"on the stub execution client"
|
||||
)
|
||||
def step_call_execute_plan(context: Context, plan_id: str) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_exec.execute_plan(plan_id)
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(
|
||||
r'I call get_plan_status with plan_id "(?P<plan_id>[^"]+)" '
|
||||
r"on the stub execution client"
|
||||
)
|
||||
def step_call_get_plan_status(context: Context, plan_id: str) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_exec.get_plan_status(plan_id)
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(
|
||||
r'I call cancel_plan with plan_id "(?P<plan_id>[^"]+)" '
|
||||
r"on the stub execution client"
|
||||
)
|
||||
def step_call_cancel_plan(context: Context, plan_id: str) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_exec.cancel_plan(plan_id)
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(r"I call execute_plan with empty plan_id on the stub execution client")
|
||||
def step_call_execute_plan_empty(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
try:
|
||||
context.stub_exec.execute_plan("")
|
||||
except ValueError as exc:
|
||||
context.caught_error = exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StubAuthClient
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"a new StubAuthClient")
|
||||
def step_stub_auth_client(context: Context) -> None:
|
||||
context.stub_auth = StubAuthClient()
|
||||
|
||||
|
||||
@when(r'I call authenticate with token "(?P<token>[^"]+)" on the stub auth client')
|
||||
def step_call_authenticate(context: Context, token: str) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_auth.authenticate(token)
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(r'I call validate_token with token "(?P<token>[^"]+)" on the stub auth client')
|
||||
def step_call_validate_token(context: Context, token: str) -> None:
|
||||
context.caught_error = None
|
||||
context.ctx_error = None
|
||||
try:
|
||||
context.stub_auth.validate_token(token)
|
||||
except NotImplementedError as exc:
|
||||
context.caught_error = exc
|
||||
context.ctx_error = exc
|
||||
|
||||
|
||||
@when(r"I call authenticate with empty token on the stub auth client")
|
||||
def step_call_authenticate_empty(context: Context) -> None:
|
||||
context.caught_error = None
|
||||
try:
|
||||
context.stub_auth.authenticate("")
|
||||
except ValueError as exc:
|
||||
context.caught_error = exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotImplementedError and ValueError assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Note: the step "a NotImplementedError should be raised with message
|
||||
# containing ..." is already defined in project_context_cli_steps.py
|
||||
# and looks at context.ctx_error. Our stub steps store errors in
|
||||
# context.caught_error, so we bridge by also writing to ctx_error
|
||||
# in each step that catches NotImplementedError.
|
||||
|
||||
|
||||
@then(r"a ValueError should be raised for empty plan_id")
|
||||
def step_value_error_plan_id(context: Context) -> None:
|
||||
assert context.caught_error is not None, "Expected ValueError"
|
||||
assert isinstance(context.caught_error, ValueError), (
|
||||
f"Expected ValueError, got {type(context.caught_error)}"
|
||||
)
|
||||
|
||||
|
||||
@then(r"a ValueError should be raised for empty token")
|
||||
def step_value_error_token(context: Context) -> None:
|
||||
assert context.caught_error is not None, "Expected ValueError"
|
||||
assert isinstance(context.caught_error, ValueError), (
|
||||
f"Expected ValueError, got {type(context.caught_error)}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol conformance
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then(r"StubServerClient should be a runtime instance of ServerClient")
|
||||
def step_server_protocol(context: Context) -> None:
|
||||
assert isinstance(StubServerClient(), ServerClient)
|
||||
|
||||
|
||||
@then(
|
||||
r"StubRemoteExecutionClient should be a runtime instance of RemoteExecutionClient"
|
||||
)
|
||||
def step_exec_protocol(context: Context) -> None:
|
||||
assert isinstance(StubRemoteExecutionClient(), RemoteExecutionClient)
|
||||
|
||||
|
||||
@then(r"StubAuthClient should be a runtime instance of AuthClient")
|
||||
def step_auth_protocol(context: Context) -> None:
|
||||
assert isinstance(StubAuthClient(), AuthClient)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config key registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then(r'config key "(?P<key>[^"]+)" should be registered')
|
||||
def step_config_key_registered(context: Context, key: str) -> None:
|
||||
assert key in _REGISTRY, f"Config key '{key}' not found in registry"
|
||||
|
||||
|
||||
@then(r'config key "(?P<key>[^"]+)" should have default true')
|
||||
def step_config_key_default_true(context: Context, key: str) -> None:
|
||||
entry = _REGISTRY[key]
|
||||
assert entry.default is True, f"Expected True, got {entry.default}"
|
||||
|
||||
|
||||
@then(r'config key "(?P<key>[^"]+)" should have default "(?P<default>[^"]+)"')
|
||||
def step_config_key_default_str(context: Context, key: str, default: str) -> None:
|
||||
entry = _REGISTRY[key]
|
||||
assert str(entry.default) == default, f"Expected '{default}', got '{entry.default}'"
|
||||
|
||||
|
||||
@then(r'config key "(?P<key>[^"]+)" should use env var "(?P<env_var>[^"]+)"')
|
||||
def step_config_key_env_var(context: Context, key: str, env_var: str) -> None:
|
||||
entry = _REGISTRY[key]
|
||||
assert entry.env_var == env_var, (
|
||||
f"Expected env var '{env_var}', got '{entry.env_var}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI integration (isolated — uses a temporary HOME to avoid config bleed)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"a temporary home directory for server tests")
|
||||
def step_tmp_home_for_server(context: Context) -> None:
|
||||
"""Set HOME to a fresh temporary directory so config writes are isolated."""
|
||||
context.server_test_home = tempfile.mkdtemp()
|
||||
context.server_test_old_home = os.environ.get("HOME")
|
||||
os.environ["HOME"] = context.server_test_home
|
||||
|
||||
|
||||
@when(
|
||||
r'I invoke server connect with url "(?P<url>[^"]+)" '
|
||||
r'and format "(?P<fmt>[^"]+)"'
|
||||
)
|
||||
def step_invoke_server_connect(context: Context, url: str, fmt: str) -> None:
|
||||
"""Run ``server connect <url> --format <fmt>`` and capture output."""
|
||||
import io
|
||||
import sys
|
||||
|
||||
from cleveragents.cli.main import main as cli_main
|
||||
|
||||
captured = io.StringIO()
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = captured
|
||||
try:
|
||||
context.server_connect_exit = cli_main(
|
||||
["server", "connect", url, "--format", fmt]
|
||||
)
|
||||
except SystemExit as exc:
|
||||
context.server_connect_exit = exc.code if exc.code is not None else 0
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
# Restore HOME
|
||||
old_home = getattr(context, "server_test_old_home", None)
|
||||
if old_home is not None:
|
||||
os.environ["HOME"] = old_home
|
||||
else:
|
||||
os.environ.pop("HOME", None)
|
||||
context.server_connect_output = captured.getvalue()
|
||||
|
||||
|
||||
@then(r'the server connect output should contain "(?P<text>[^"]+)"')
|
||||
def step_server_connect_output_contains(context: Context, text: str) -> None:
|
||||
assert text in context.server_connect_output, (
|
||||
f"Expected '{text}' in output: {context.server_connect_output!r}"
|
||||
)
|
||||
|
||||
|
||||
@then(r"the server connect should exit with non-zero code")
|
||||
def step_server_connect_non_zero(context: Context) -> None:
|
||||
assert context.server_connect_exit != 0, (
|
||||
f"Expected non-zero exit, got {context.server_connect_exit}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server mode resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(r"no server URL is configured")
|
||||
def step_no_server_url(context: Context) -> None:
|
||||
# Ensure the env var is not set and use a clean temp HOME
|
||||
os.environ.pop("CLEVERAGENTS_SERVER_URL", None)
|
||||
context.clean_home = tempfile.mkdtemp()
|
||||
context.old_home = os.environ.get("HOME")
|
||||
os.environ["HOME"] = context.clean_home
|
||||
|
||||
|
||||
@then(r'resolve_server_mode should return "(?P<mode>[^"]+)"')
|
||||
def step_resolve_server_mode(context: Context, mode: str) -> None:
|
||||
from cleveragents.cli.commands.server import resolve_server_mode
|
||||
|
||||
try:
|
||||
result = resolve_server_mode()
|
||||
assert result == mode, f"Expected '{mode}', got '{result}'"
|
||||
finally:
|
||||
old_home = getattr(context, "old_home", None)
|
||||
if old_home is not None:
|
||||
os.environ["HOME"] = old_home
|
||||
else:
|
||||
os.environ.pop("HOME", None)
|
||||
@@ -9,7 +9,7 @@ ${HELPER} ${CURDIR}/helper_config_resolution.py
|
||||
|
||||
*** Test Cases ***
|
||||
Config Service Registers All Spec Keys
|
||||
[Documentation] Verify that the registry contains exactly 103 keys (102 spec + 1 skills)
|
||||
[Documentation] Verify that the registry contains exactly 105 keys (102 spec + 1 skills + 2 server stubs)
|
||||
${result}= Run Process ${PYTHON} ${HELPER} registry-key-count cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
|
||||
@@ -46,13 +46,17 @@ def _cleanup(tmpdir: Path) -> None:
|
||||
|
||||
|
||||
def registry_key_count() -> None:
|
||||
"""Verify that the registry contains exactly 103 keys."""
|
||||
"""Verify that the registry contains exactly 105 keys.
|
||||
|
||||
102 spec-defined + 1 skills extension + 2 server stub keys
|
||||
(server.tls-verify, server.namespace).
|
||||
"""
|
||||
registry = ConfigService.registry()
|
||||
count = len(registry)
|
||||
if count == 103:
|
||||
if count == 105:
|
||||
print("config-registry-key-count-ok")
|
||||
else:
|
||||
print(f"FAIL: expected 103 keys, got {count}", file=sys.stderr)
|
||||
print(f"FAIL: expected 105 keys, got {count}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Helper script for server_stubs.robot smoke tests.
|
||||
|
||||
Each subcommand is a self-contained check that prints a sentinel on success.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure local source tree is importable
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from cleveragents.acp.clients import ( # noqa: E402
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig # noqa: E402
|
||||
from cleveragents.application.services.config_service import _REGISTRY # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def stub_clients() -> None:
|
||||
"""Verify all stub clients raise NotImplementedError."""
|
||||
server = StubServerClient()
|
||||
try:
|
||||
server.health_check()
|
||||
print("FAIL: should have raised NotImplementedError", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
try:
|
||||
server.get_version()
|
||||
print("FAIL: should have raised NotImplementedError", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
exec_client = StubRemoteExecutionClient()
|
||||
try:
|
||||
exec_client.execute_plan("PLAN001")
|
||||
print("FAIL: should have raised NotImplementedError", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
auth_client = StubAuthClient()
|
||||
try:
|
||||
auth_client.authenticate("tok_test")
|
||||
print("FAIL: should have raised NotImplementedError", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
# Verify protocol conformance
|
||||
if not isinstance(server, ServerClient):
|
||||
print("FAIL: StubServerClient not ServerClient", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not isinstance(exec_client, RemoteExecutionClient):
|
||||
print(
|
||||
"FAIL: StubRemoteExecutionClient not RemoteExecutionClient", file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
if not isinstance(auth_client, AuthClient):
|
||||
print("FAIL: StubAuthClient not AuthClient", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("server-stub-clients-ok")
|
||||
|
||||
|
||||
def config_validation() -> None:
|
||||
"""Verify ServerConnectionConfig validation."""
|
||||
# Valid config
|
||||
cfg = ServerConnectionConfig(server_url="https://agents.example.com")
|
||||
if cfg.server_url != "https://agents.example.com":
|
||||
print("FAIL: wrong url", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if cfg.namespace != "default":
|
||||
print("FAIL: wrong namespace", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if cfg.tls_verify is not True:
|
||||
print("FAIL: wrong tls_verify", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Invalid: empty URL
|
||||
try:
|
||||
ServerConnectionConfig(server_url="")
|
||||
print("FAIL: should have rejected empty URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Invalid: non-HTTP URL
|
||||
try:
|
||||
ServerConnectionConfig(server_url="ftp://bad.example.com")
|
||||
print("FAIL: should have rejected ftp URL", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("server-config-validation-ok")
|
||||
|
||||
|
||||
def config_keys() -> None:
|
||||
"""Verify server config keys are registered."""
|
||||
required_keys = [
|
||||
"server.url",
|
||||
"server.token",
|
||||
"server.tls-verify",
|
||||
"server.namespace",
|
||||
]
|
||||
for key in required_keys:
|
||||
if key not in _REGISTRY:
|
||||
print(f"FAIL: '{key}' not in registry", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check defaults
|
||||
if _REGISTRY["server.tls-verify"].default is not True:
|
||||
print("FAIL: server.tls-verify default not True", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if _REGISTRY["server.namespace"].default != "default":
|
||||
print("FAIL: server.namespace default not 'default'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("server-config-keys-ok")
|
||||
|
||||
|
||||
def server_mode() -> None:
|
||||
"""Verify resolve_server_mode returns disabled."""
|
||||
# Ensure no server URL is set
|
||||
os.environ.pop("CLEVERAGENTS_SERVER_URL", None)
|
||||
|
||||
from cleveragents.cli.commands.server import resolve_server_mode
|
||||
|
||||
mode = resolve_server_mode()
|
||||
if mode != "disabled":
|
||||
print(f"FAIL: expected 'disabled', got '{mode}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("server-mode-ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMMANDS = {
|
||||
"stub-clients": stub_clients,
|
||||
"config-validation": config_validation,
|
||||
"config-keys": config_keys,
|
||||
"server-mode": server_mode,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
||||
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
_COMMANDS[sys.argv[1]]()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
*** Settings ***
|
||||
Documentation Smoke tests for server client stubs and connection config
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_server_stubs.py
|
||||
|
||||
*** Test Cases ***
|
||||
Server Stub Client NotImplementedError
|
||||
[Documentation] Verify StubServerClient raises NotImplementedError
|
||||
${result}= Run Process ${PYTHON} ${HELPER} stub-clients cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} server-stub-clients-ok
|
||||
|
||||
Server Connection Config Validation
|
||||
[Documentation] Verify ServerConnectionConfig validates URLs
|
||||
${result}= Run Process ${PYTHON} ${HELPER} config-validation cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} server-config-validation-ok
|
||||
|
||||
Server Config Keys Registered
|
||||
[Documentation] Verify server config keys are in the registry
|
||||
${result}= Run Process ${PYTHON} ${HELPER} config-keys cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} server-config-keys-ok
|
||||
|
||||
Server Mode Resolution
|
||||
[Documentation] Verify resolve_server_mode returns disabled when no URL
|
||||
${result}= Run Process ${PYTHON} ${HELPER} server-mode cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} server-mode-ok
|
||||
|
||||
CLI Server Connect Stub
|
||||
[Documentation] Verify CLI server connect prints stub warning
|
||||
${result}= Run Process ${PYTHON} -m cleveragents server connect https://stub.example.com --format json cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} not yet implemented
|
||||
@@ -10,10 +10,22 @@ no network, no authentication.
|
||||
In **server mode** the :class:`AcpHttpTransport` is a stub that raises
|
||||
:class:`AcpNotAvailableError` for every operation. When server mode is
|
||||
implemented the concrete transport will replace these stubs.
|
||||
|
||||
Server client protocols (:class:`ServerClient`, :class:`RemoteExecutionClient`,
|
||||
:class:`AuthClient`) and their stub implementations are provided for forward
|
||||
compatibility. :class:`ServerConnectionConfig` validates connection parameters.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cleveragents.acp.clients import (
|
||||
AuthClient,
|
||||
RemoteExecutionClient,
|
||||
ServerClient,
|
||||
StubAuthClient,
|
||||
StubRemoteExecutionClient,
|
||||
StubServerClient,
|
||||
)
|
||||
from cleveragents.acp.errors import (
|
||||
AcpError,
|
||||
AcpNotAvailableError,
|
||||
@@ -29,6 +41,7 @@ from cleveragents.acp.models import (
|
||||
AcpResponse,
|
||||
AcpVersion,
|
||||
)
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.acp.transport import AcpHttpTransport
|
||||
from cleveragents.acp.versioning import AcpVersionNegotiator
|
||||
|
||||
@@ -46,4 +59,11 @@ __all__ = [
|
||||
"AcpVersion",
|
||||
"AcpVersionMismatchError",
|
||||
"AcpVersionNegotiator",
|
||||
"AuthClient",
|
||||
"RemoteExecutionClient",
|
||||
"ServerClient",
|
||||
"ServerConnectionConfig",
|
||||
"StubAuthClient",
|
||||
"StubRemoteExecutionClient",
|
||||
"StubServerClient",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Server client protocol stubs for remote server communication.
|
||||
|
||||
Defines three Protocol interfaces (``ServerClient``, ``RemoteExecutionClient``,
|
||||
``AuthClient``) and their stub implementations that raise
|
||||
:class:`NotImplementedError`. When server mode is implemented as a separate
|
||||
project the concrete clients will replace these stubs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ServerClient(Protocol):
|
||||
"""Protocol for server health and version negotiation."""
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""Check server health.
|
||||
|
||||
Returns:
|
||||
``True`` when the server is healthy and reachable.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_version(self) -> str:
|
||||
"""Get server version string.
|
||||
|
||||
Returns:
|
||||
The semantic version reported by the server.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RemoteExecutionClient(Protocol):
|
||||
"""Protocol for remote plan execution."""
|
||||
|
||||
def execute_plan(self, plan_id: str) -> dict[str, Any]:
|
||||
"""Submit a plan for remote execution.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan to execute.
|
||||
|
||||
Returns:
|
||||
Execution metadata including status and any server-assigned ID.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_plan_status(self, plan_id: str) -> dict[str, Any]:
|
||||
"""Query the status of a remotely-executing plan.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan.
|
||||
|
||||
Returns:
|
||||
Status metadata including phase, progress, and elapsed time.
|
||||
"""
|
||||
...
|
||||
|
||||
def cancel_plan(self, plan_id: str) -> bool:
|
||||
"""Request cancellation of a remotely-executing plan.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan to cancel.
|
||||
|
||||
Returns:
|
||||
``True`` if the cancellation request was accepted.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AuthClient(Protocol):
|
||||
"""Protocol for authentication with the server."""
|
||||
|
||||
def authenticate(self, token: str) -> bool:
|
||||
"""Authenticate with the server using a bearer token.
|
||||
|
||||
Args:
|
||||
token: Authentication token.
|
||||
|
||||
Returns:
|
||||
``True`` when authentication succeeds.
|
||||
"""
|
||||
...
|
||||
|
||||
def validate_token(self, token: str) -> bool:
|
||||
"""Validate that a token is still valid without a full auth flow.
|
||||
|
||||
Args:
|
||||
token: Token to validate.
|
||||
|
||||
Returns:
|
||||
``True`` when the token is valid and not expired.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub implementations — raise NotImplementedError for all operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STUB_MSG = "Server client is not yet implemented"
|
||||
|
||||
|
||||
class StubServerClient:
|
||||
"""Stub implementation of :class:`ServerClient`.
|
||||
|
||||
All methods raise :class:`NotImplementedError`.
|
||||
"""
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""Raise :class:`NotImplementedError`."""
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
def get_version(self) -> str:
|
||||
"""Raise :class:`NotImplementedError`."""
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
|
||||
class StubRemoteExecutionClient:
|
||||
"""Stub implementation of :class:`RemoteExecutionClient`.
|
||||
|
||||
All methods raise :class:`NotImplementedError`.
|
||||
"""
|
||||
|
||||
def execute_plan(self, plan_id: str) -> dict[str, Any]:
|
||||
"""Raise :class:`NotImplementedError`.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan to execute.
|
||||
"""
|
||||
if not plan_id or not isinstance(plan_id, str):
|
||||
raise ValueError("plan_id must be a non-empty string")
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
def get_plan_status(self, plan_id: str) -> dict[str, Any]:
|
||||
"""Raise :class:`NotImplementedError`.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan.
|
||||
"""
|
||||
if not plan_id or not isinstance(plan_id, str):
|
||||
raise ValueError("plan_id must be a non-empty string")
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
def cancel_plan(self, plan_id: str) -> bool:
|
||||
"""Raise :class:`NotImplementedError`.
|
||||
|
||||
Args:
|
||||
plan_id: Identifier of the plan to cancel.
|
||||
"""
|
||||
if not plan_id or not isinstance(plan_id, str):
|
||||
raise ValueError("plan_id must be a non-empty string")
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
|
||||
class StubAuthClient:
|
||||
"""Stub implementation of :class:`AuthClient`.
|
||||
|
||||
All methods raise :class:`NotImplementedError`.
|
||||
"""
|
||||
|
||||
def authenticate(self, token: str) -> bool:
|
||||
"""Raise :class:`NotImplementedError`.
|
||||
|
||||
Args:
|
||||
token: Authentication token.
|
||||
"""
|
||||
if not token or not isinstance(token, str):
|
||||
raise ValueError("token must be a non-empty string")
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
def validate_token(self, token: str) -> bool:
|
||||
"""Raise :class:`NotImplementedError`.
|
||||
|
||||
Args:
|
||||
token: Token to validate.
|
||||
"""
|
||||
if not token or not isinstance(token, str):
|
||||
raise ValueError("token must be a non-empty string")
|
||||
raise NotImplementedError(_STUB_MSG)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AuthClient",
|
||||
"RemoteExecutionClient",
|
||||
"ServerClient",
|
||||
"StubAuthClient",
|
||||
"StubRemoteExecutionClient",
|
||||
"StubServerClient",
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Server connection configuration model.
|
||||
|
||||
Provides :class:`ServerConnectionConfig`, a validated Pydantic model for
|
||||
server connection parameters. In the current stub implementation the model
|
||||
is used by the CLI ``connect`` command and ``info`` output but no actual
|
||||
network calls are made.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
|
||||
class ServerConnectionConfig(BaseModel):
|
||||
"""Validated configuration for connecting to a CleverAgents server.
|
||||
|
||||
Attributes:
|
||||
server_url: Base URL of the server (must start with ``http://`` or
|
||||
``https://``).
|
||||
namespace: Namespace on the server (defaults to ``"default"``).
|
||||
auth_token_ref: Optional reference to an authentication token
|
||||
(e.g. environment variable name or secret store key).
|
||||
tls_verify: Whether to verify TLS certificates (defaults to
|
||||
``True``).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True, strict=False)
|
||||
|
||||
server_url: str
|
||||
namespace: str = "default"
|
||||
auth_token_ref: str | None = None
|
||||
tls_verify: bool = True
|
||||
|
||||
@field_validator("server_url")
|
||||
@classmethod
|
||||
def _validate_server_url(cls, value: str) -> str:
|
||||
if not value or not value.strip():
|
||||
raise ValueError("server_url must not be empty")
|
||||
stripped = value.strip()
|
||||
if not stripped.startswith(("http://", "https://")):
|
||||
raise ValueError("server_url must start with http:// or https://")
|
||||
return stripped
|
||||
|
||||
@field_validator("namespace")
|
||||
@classmethod
|
||||
def _validate_namespace(cls, value: str) -> str:
|
||||
if not value or not value.strip():
|
||||
raise ValueError("namespace must not be empty")
|
||||
return value.strip()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ServerConnectionConfig",
|
||||
]
|
||||
@@ -272,6 +272,24 @@ def _build_catalog() -> None:
|
||||
env_var="CLEVERAGENTS_SERVER_SYNC_INTERVAL",
|
||||
description="Seconds between automatic background syncs.",
|
||||
)
|
||||
_register(
|
||||
"server",
|
||||
"tls-verify",
|
||||
bool,
|
||||
True,
|
||||
project_scopable=False,
|
||||
env_var="CLEVERAGENTS_SERVER_TLS_VERIFY",
|
||||
description="Verify TLS certificates when connecting to the server.",
|
||||
)
|
||||
_register(
|
||||
"server",
|
||||
"namespace",
|
||||
str,
|
||||
"default",
|
||||
project_scopable=False,
|
||||
env_var="CLEVERAGENTS_SERVER_NAMESPACE",
|
||||
description="Default namespace on the remote server.",
|
||||
)
|
||||
|
||||
# ── actor.* (5 keys) ────────────────────────────────────────────────
|
||||
_register(
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Server connection CLI commands.
|
||||
|
||||
Provides the ``agents server connect`` stub command and server-mode
|
||||
status helpers. All actual server communication is stubbed out —
|
||||
commands persist configuration and print explicit warnings.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cleveragents.acp.server_config import ServerConnectionConfig
|
||||
from cleveragents.application.services.config_service import ConfigService
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
|
||||
app: Any = typer.Typer(help="Server connection management (stub).")
|
||||
console: Console = Console()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STUB_WARNING: str = (
|
||||
"Server connection is not yet implemented. "
|
||||
"The URL has been saved to configuration but no connection will be made."
|
||||
)
|
||||
|
||||
|
||||
def _get_config_service() -> ConfigService:
|
||||
"""Return a ``ConfigService`` wired to the standard config paths.
|
||||
|
||||
Paths are computed lazily from ``Path.home()`` so that changes to
|
||||
``HOME`` (e.g. in tests) are picked up correctly.
|
||||
"""
|
||||
config_dir = Path.home() / ".cleveragents"
|
||||
config_path = config_dir / "config.toml"
|
||||
return ConfigService(config_dir=config_dir, config_path=config_path)
|
||||
|
||||
|
||||
def resolve_server_mode() -> str:
|
||||
"""Determine the current server mode.
|
||||
|
||||
Returns:
|
||||
``"disabled"`` when no server URL is configured, ``"stubbed"``
|
||||
when a URL is present but the client is not yet implemented.
|
||||
"""
|
||||
svc = _get_config_service()
|
||||
try:
|
||||
resolved = svc.resolve("server.url")
|
||||
if resolved.value is not None and str(resolved.value).strip():
|
||||
return "stubbed"
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
return "disabled"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command("connect")
|
||||
def server_connect(
|
||||
server_url: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
help="Server URL to connect to (e.g. https://agents.example.com)"
|
||||
),
|
||||
],
|
||||
namespace: Annotated[
|
||||
str,
|
||||
typer.Option("--namespace", "-n", help="Namespace on the server"),
|
||||
] = "default",
|
||||
tls_verify: Annotated[
|
||||
bool,
|
||||
typer.Option("--tls-verify/--no-tls-verify", help="Verify TLS certificates"),
|
||||
] = True,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help="Output format: rich, plain, json, yaml"),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Connect to a CleverAgents server (stub).
|
||||
|
||||
Validates and persists the server URL, namespace, and TLS settings
|
||||
to the global configuration file. No actual connection is made —
|
||||
server mode is not yet implemented.
|
||||
|
||||
Examples::
|
||||
|
||||
agents server connect https://agents.example.com
|
||||
agents server connect https://agents.example.com --namespace my-team
|
||||
agents server connect http://localhost:8080 --no-tls-verify
|
||||
"""
|
||||
# Validate the connection config
|
||||
try:
|
||||
config = ServerConnectionConfig(
|
||||
server_url=server_url,
|
||||
namespace=namespace,
|
||||
tls_verify=tls_verify,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
console.print(f"[red]Invalid server configuration:[/red] {exc}")
|
||||
raise typer.Exit(code=1) from exc
|
||||
|
||||
# Persist to config file
|
||||
svc = _get_config_service()
|
||||
config_data = svc.read_config()
|
||||
config_data["server.url"] = config.server_url
|
||||
config_data["server.namespace"] = config.namespace
|
||||
config_data["server.tls-verify"] = config.tls_verify
|
||||
svc.write_config(config_data)
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"server_url": config.server_url,
|
||||
"namespace": config.namespace,
|
||||
"tls_verify": config.tls_verify,
|
||||
"status": "stubbed",
|
||||
"warning": _STUB_WARNING,
|
||||
}
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
typer.echo(format_output(result, fmt))
|
||||
return
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Server URL:[/bold] {config.server_url}\n"
|
||||
f"[bold]Namespace:[/bold] {config.namespace}\n"
|
||||
f"[bold]TLS Verify:[/bold] {config.tls_verify}\n"
|
||||
f"[bold]Status:[/bold] stubbed\n"
|
||||
f"\n[yellow]{_STUB_WARNING}[/yellow]",
|
||||
title="Server Connection (Stub)",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def server_status(
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help="Output format: rich, plain, json, yaml"),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Show current server connection status.
|
||||
|
||||
Displays the configured server URL (if any) and whether server mode
|
||||
is disabled or stubbed.
|
||||
"""
|
||||
mode = resolve_server_mode()
|
||||
svc = _get_config_service()
|
||||
|
||||
server_url: str | None = None
|
||||
namespace: str = "default"
|
||||
tls_verify: bool = True
|
||||
|
||||
try:
|
||||
resolved_url = svc.resolve("server.url")
|
||||
if resolved_url.value is not None:
|
||||
server_url = str(resolved_url.value)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
resolved_ns = svc.resolve("server.namespace")
|
||||
if resolved_ns.value is not None:
|
||||
namespace = str(resolved_ns.value)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
resolved_tls = svc.resolve("server.tls-verify")
|
||||
if resolved_tls.value is not None:
|
||||
tls_verify = bool(resolved_tls.value)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"server_mode": mode,
|
||||
"server_url": server_url,
|
||||
"namespace": namespace,
|
||||
"tls_verify": tls_verify,
|
||||
}
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
typer.echo(format_output(result, fmt))
|
||||
return
|
||||
|
||||
url_display = server_url if server_url else "(not configured)"
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Server Mode:[/bold] {mode}\n"
|
||||
f"[bold]Server URL:[/bold] {url_display}\n"
|
||||
f"[bold]Namespace:[/bold] {namespace}\n"
|
||||
f"[bold]TLS Verify:[/bold] {tls_verify}",
|
||||
title="Server Status",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"app",
|
||||
"resolve_server_mode",
|
||||
"server_connect",
|
||||
"server_status",
|
||||
]
|
||||
@@ -125,12 +125,16 @@ def build_info_data() -> dict[str, Any]:
|
||||
else:
|
||||
storage["logs"] = "0 MB"
|
||||
|
||||
from cleveragents.cli.commands.server import resolve_server_mode
|
||||
|
||||
server_mode = resolve_server_mode()
|
||||
|
||||
return {
|
||||
"version": __version__,
|
||||
"data_dir": str(data_dir),
|
||||
"config_path": str(config_path),
|
||||
"database": db_url,
|
||||
"server_mode": "disabled",
|
||||
"server_mode": server_mode,
|
||||
"platform": f"{platform.system()} {platform.release()} ({platform.machine()})",
|
||||
"automation": settings.default_automation_profile,
|
||||
"providers_configured": len(configured_providers),
|
||||
|
||||
@@ -96,6 +96,7 @@ def _register_subcommands() -> None:
|
||||
)
|
||||
from cleveragents.cli.commands.auto_debug import app as auto_debug_app
|
||||
from cleveragents.cli.commands.repl import _repl_app
|
||||
from cleveragents.cli.commands.server import app as server_app
|
||||
except Exception as exc: # pragma: no cover
|
||||
import traceback
|
||||
|
||||
@@ -188,6 +189,11 @@ def _register_subcommands() -> None:
|
||||
name="repl",
|
||||
help="Start an interactive REPL session",
|
||||
)
|
||||
app.add_typer(
|
||||
server_app,
|
||||
name="server",
|
||||
help="Server connection management (stub)",
|
||||
)
|
||||
|
||||
_subcommands_registered = True
|
||||
|
||||
@@ -595,6 +601,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"automation-profile", # Automation profile management
|
||||
"invariant", # Invariant constraint management
|
||||
"repl", # Interactive REPL
|
||||
"server", # Server connection management
|
||||
"tell", # Shortcut for plan tell
|
||||
"build", # Shortcut for plan build
|
||||
"apply", # Shortcut for plan apply
|
||||
|
||||
@@ -483,3 +483,16 @@ validation_errors # noqa: B018, F821
|
||||
get_spawn_decisions # noqa: B018, F821
|
||||
build_spawn_entries # noqa: B018, F821
|
||||
validate_spawn # noqa: B018, F821
|
||||
|
||||
# Server client stubs — public API for server mode (#201)
|
||||
ServerClient # noqa: B018, F821
|
||||
RemoteExecutionClient # noqa: B018, F821
|
||||
AuthClient # noqa: B018, F821
|
||||
StubServerClient # noqa: B018, F821
|
||||
StubRemoteExecutionClient # noqa: B018, F821
|
||||
StubAuthClient # noqa: B018, F821
|
||||
ServerConnectionConfig # noqa: B018, F821
|
||||
resolve_server_mode # noqa: B018, F821
|
||||
server_connect # noqa: B018, F821
|
||||
server_status # noqa: B018, F821
|
||||
_STUB_WARNING # noqa: B018, F821
|
||||
|
||||
Reference in New Issue
Block a user