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
10 changes: 10 additions & 0 deletions src/agent_env/task_step/task_steps/sync_env_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Arms the clock (set_time) and reads the gateway's server-reachable env_get_time_url (state), both as the
stored env card declares clock/v1, then invokes sync_time on each backing server, MCP server or website, the
env card lists (``legacy_protocol.backing_servers``) whose own card advertises it, at the route that card gives.
An env whose card offers no set_time has no gateway clock to arm, as a computer deployed alone, and is refused
saying to deploy it in a multi env; a record that kept no card is refused saying so.
Non-advertisers are skipped when tolerate_missing_sync_time (default True), and websites always. Place right
before prompt_agent so the virtual timeline starts at agent-start; when setup needs the clock armed, arm it at rate 0
and start it with set_env_clock_rate right before prompt_agent.
Expand All @@ -19,6 +21,7 @@
import tzdata # noqa: F401
from agentenv_protocol import client as protocol_v1

from agent_env.env.env import EnvCapabilityUnsupported
from agent_env.env.gateway.constants import EXT_CLOCK_URI
from agent_env.task_step.context import StepContextError, TaskStepContext
from agent_env.entity.refs import EntityRef
Expand Down Expand Up @@ -132,6 +135,13 @@ async def execute(self, context: TaskStepContext) -> TaskStepContext:
timeout=self.timeout_seconds)
except httpx.HTTPStatusError as e:
raise RuntimeError(f"clock arm failed (HTTP {e.response.status_code}): {e.response.text}") from e
except EnvCapabilityUnsupported as e:
if not deployed.environment_card or not deployed.environment_url:
raise StepContextError(f"env '{self.env_id}': its deploy record keeps no env card or card URL, so "
"its clock can't be reached; deploy it again") from e
raise StepContextError(f"env '{self.env_id}' has no clock to arm: its card offers no {EXT_CLOCK_URI} "
"set_time, which a gateway serves. A computer deployed alone follows no clock; "
"deploy it in a multi env, whose gateway it follows") from e
state = await deployed.invoke(EXT_CLOCK_URI, "state", timeout=self.timeout_seconds)
env_get_time_url = state.get("env_get_time_url")

Expand Down
38 changes: 27 additions & 11 deletions tst/unit/task_step/sync_env_clock_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import pytest

from agent_env.env.env import DeployedEnv, DeployedGatewayEnv, EnvCapabilityUnsupported
from agent_env.env.gateway.constants import EXT_CLOCK_URI
from agent_env.env.env import DeployedEnv, DeployedGatewayEnv, DeployedSandboxEnv
from agent_env.env.gateway.constants import EXT_CLOCK_URI, WELL_KNOWN_PATH
from agent_env.task_step.context import StepContextError, TaskStepContext
from agent_env.task_step.task_steps import sync_env_clock as mod
from agent_env.task_step.task_steps.sync_env_clock import SyncEnvClockTaskStep, resolve_virtual_time
Expand Down Expand Up @@ -108,15 +108,6 @@ async def test_execute_syncs_each_child_env_through_the_stored_card(monkeypatch)
assert [s["service"] for s in context.metadata["clock_configurations"][0]["synced"]] == ["email"]


@pytest.mark.asyncio
async def test_execute_raises_before_any_request_when_the_card_lacks_set_time(monkeypatch):
sent = _mock_http(monkeypatch)
record = _carded({"name": "gw", "capabilities": {"extensions": []}, "children_environments": []})
with pytest.raises(EnvCapabilityUnsupported, match="does not offer 'set_time' on urn:agentenv:clock/v1"):
await _step().execute(TaskStepContext(deployed_envs=[record]))
assert sent == []


@pytest.mark.asyncio
async def test_execute_keeps_the_arm_failure_message(monkeypatch):
_mock_http(monkeypatch, set_time_status=409)
Expand Down Expand Up @@ -466,6 +457,31 @@ async def test_a_multi_envs_computer_is_synced_at_its_own_address_with_its_own_c
computer.control_headers


@pytest.mark.asyncio
async def test_an_env_with_no_gateway_clock_as_a_computer_deployed_alone_is_refused_saying_to_deploy_it_in_a_multi_env(
monkeypatch):
sent = _mock_http(monkeypatch)
alone = DeployedSandboxEnv(env_id="e1", env_version=1, sandbox_id="sb-desk", control_token="desk-token",
environment_card_url=f"https://desk.example{WELL_KNOWN_PATH}",
environment_card={"name": "desktop", "url": "/agentenv",
"capabilities": {"extensions": [_CLOCK_EXT]}})

with pytest.raises(StepContextError, match="has no clock to arm") as refused:
await _step().execute(TaskStepContext(deployed_envs=[alone]))

assert "deploy it in a multi env" in str(refused.value) and sent == []


@pytest.mark.asyncio
async def test_a_record_that_kept_no_card_is_refused_saying_so_not_told_to_deploy_a_multi_env(monkeypatch):
sent = _mock_http(monkeypatch)

with pytest.raises(StepContextError, match="its deploy record keeps no env card") as refused:
await _step().execute(TaskStepContext(deployed_envs=[_cardless()]))

assert "multi env" not in str(refused.value) and sent == []


def test_a_padded_zone_is_stored_and_sent_stripped():
s = _zoned(timezone=" America/New_York ")
assert s.timezone == "America/New_York" and s.to_dict()["timezone"] == "America/New_York"
Loading