Skip to content
Open
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
9 changes: 3 additions & 6 deletions src/agentarts/toolkit/operations/runtime/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,16 @@ def create_agentarts_runtime(

if runtime_cfg.storage_config:
sc = runtime_cfg.storage_config.to_dict()
# to_dict() returns {"sfs_turbo": [{...}]} (list) or {} (unconfigured).
# Only enforce required fields when the user explicitly opted
# into SFS Turbo by providing sfs_turbo_id. An unset/all-null
# storage_config (e.g. from `agentarts init` / `agentarts config`)
# is treated as "not configured" so deploy is never blocked.
# Validate sfs_turbo: mount_path is required when sfs_turbo_id is set.
st_list = sc.get("sfs_turbo") or []
if st_list:
st = st_list[0]
if st.get("sfs_turbo_id"):
if not st.get("mount_path"):
msg = "storage_config.sfs_turbo.mount_path is required when sfs_turbo_id is set"
raise ValueError(msg)
storage_config = sc
if sc:
storage_config = sc

if runtime_cfg.environment_variables:
env_vars = [{"key": kv.key, "value": kv.value} for kv in runtime_cfg.environment_variables if kv.value]
Expand Down
2 changes: 2 additions & 0 deletions src/agentarts/toolkit/operations/runtime/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ def create_config_file(
sfs_path: null
mount_path: null # required when using SFS Turbo
read_only: false
session_storage:
mount_path: null # Session storage mount path in the container

environment_variables:{env_vars_yaml}

Expand Down
40 changes: 33 additions & 7 deletions src/agentarts/toolkit/utils/runtime/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,35 @@ def to_dict(self) -> dict[str, Any]:
return {k: v for k, v in data.items() if v not in ([], {})}


class SessionStorageConfig(BaseModel):
"""Session storage configuration."""

mount_path: str | None = Field(
default=None,
description="Session storage mount path in the container",
)

model_config = {
"extra": "allow",
}

def to_dict(self) -> dict[str, Any]:
"""Convert configuration to dictionary."""
data = self.model_dump(mode="json", exclude_none=True)
return {k: v for k, v in data.items() if v not in ([], {})}


class StorageConfig(BaseModel):
"""Storage configuration for the runtime."""

sfs_turbo: SfsTurboConfig | None = Field(
default_factory=SfsTurboConfig,
description="SFS Turbo storage configuration",
)
session_storage: SessionStorageConfig | None = Field(
default_factory=SessionStorageConfig,
description="Session storage configuration",
)

model_config = {
"extra": "allow",
Expand All @@ -242,15 +264,19 @@ def to_dict(self) -> dict[str, Any]:

The API expects ``sfs_turbo`` as an **array** of config objects
(not a single object). This method wraps the single config object
in a list. Returns ``{}`` when SFS is not configured (no sfs_turbo_id).
in a list. Returns ``{}`` when nothing is configured.
"""
result: dict[str, Any] = {}
st = self.sfs_turbo
if st is None or not st.sfs_turbo_id:
return {}
item = st.to_dict()
if not item:
return {}
return {"sfs_turbo": [item]}
if st is not None and st.sfs_turbo_id:
item = st.to_dict()
if item:
result["sfs_turbo"] = [item]
if self.session_storage is not None:
ss = self.session_storage.to_dict()
if ss:
result["session_storage"] = ss
return result


class NetworkConfig(BaseModel):
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/toolkit/operations/runtime/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
CustomJWTAuthConfig,
InboundIdentityConfig,
SfsTurboConfig,
SessionStorageConfig,
StorageConfig,
)

Expand Down Expand Up @@ -588,6 +589,32 @@ def test_storage_config_default_to_dict_is_empty(self):
"""A default StorageConfig (no sfs_turbo_id) serializes to {}."""
assert StorageConfig().to_dict() == {}

def test_storage_config_to_dict_session_storage_only(self):
"""StorageConfig with only session_storage serializes correctly."""
cfg = StorageConfig(
session_storage=SessionStorageConfig(mount_path="/home/user/sessions"),
)
result = cfg.to_dict()
assert result == {"session_storage": {"mount_path": "/home/user/sessions"}}

def test_storage_config_to_dict_both_fields(self):
"""StorageConfig with both sfs_turbo and session_storage."""
cfg = StorageConfig(
sfs_turbo=SfsTurboConfig(
sfs_turbo_id="12345678-1234-1234-1234-123456789012",
mount_path="/data",
),
session_storage=SessionStorageConfig(mount_path="/home/user/sessions"),
)
result = cfg.to_dict()
assert result == {
"sfs_turbo": [{
"sfs_turbo_id": "12345678-1234-1234-1234-123456789012",
"mount_path": "/data",
}],
"session_storage": {"mount_path": "/home/user/sessions"},
}

def test_invalid_sfs_turbo_id_rejected(self):
"""A non-UUID sfs_turbo_id is rejected by validation."""
import pytest
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/toolkit/operations/runtime/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from agentarts.toolkit.utils.runtime.config import (
AgentArtsConfig,
AgentArtsRuntimeConfig,
SessionStorageConfig,
SfsTurboConfig,
StorageConfig,
)
Expand Down Expand Up @@ -221,6 +222,78 @@ def test_storage_config_forwarded(self, mock_client, tmp_path, monkeypatch):
}]
}

@patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient")
def test_storage_config_session_storage_only_forwarded(self, mock_client, tmp_path, monkeypatch):
"""session_storage alone is forwarded without sfs_turbo."""
monkeypatch.chdir(tmp_path)

mock_client_instance = MagicMock()
mock_client.return_value = mock_client_instance
mock_client_instance.create_or_update_agent.return_value = {
"id": "agent-123",
"latest_version": "v1",
}

agent_config = AgentArtsConfig(
runtime=AgentArtsRuntimeConfig(
storage_config=StorageConfig(
session_storage=SessionStorageConfig(mount_path="/home/user/sessions"),
),
),
)

create_agentarts_runtime(
agent_name="test-agent",
swr_image="swr.cn-north-4.myhuaweicloud.com/org/repo:latest",
region="cn-north-4",
agent_config=agent_config,
)

call_args = mock_client_instance.create_or_update_agent.call_args
assert call_args.kwargs["storage_config"] == {
"session_storage": {"mount_path": "/home/user/sessions"},
}

@patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient")
def test_storage_config_both_fields_forwarded(self, mock_client, tmp_path, monkeypatch):
"""Both sfs_turbo and session_storage are forwarded together."""
monkeypatch.chdir(tmp_path)

mock_client_instance = MagicMock()
mock_client.return_value = mock_client_instance
mock_client_instance.create_or_update_agent.return_value = {
"id": "agent-123",
"latest_version": "v1",
}

agent_config = AgentArtsConfig(
runtime=AgentArtsRuntimeConfig(
storage_config=StorageConfig(
sfs_turbo=SfsTurboConfig(
sfs_turbo_id="12345678-1234-1234-1234-123456789012",
mount_path="/data",
),
session_storage=SessionStorageConfig(mount_path="/home/user/sessions"),
),
),
)

create_agentarts_runtime(
agent_name="test-agent",
swr_image="swr.cn-north-4.myhuaweicloud.com/org/repo:latest",
region="cn-north-4",
agent_config=agent_config,
)

call_args = mock_client_instance.create_or_update_agent.call_args
assert call_args.kwargs["storage_config"] == {
"sfs_turbo": [{
"sfs_turbo_id": "12345678-1234-1234-1234-123456789012",
"mount_path": "/data",
}],
"session_storage": {"mount_path": "/home/user/sessions"},
}

@patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient")
def test_storage_config_requires_mount_path_when_id_set(self, mock_client, tmp_path, monkeypatch):
"""When sfs_turbo_id is set, mount_path is required (returns None)."""
Expand Down
Loading