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
13 changes: 13 additions & 0 deletions src/fi/alk/harness/authoring_entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ def _load_provider_import_profile(
"system_prompt": str(job.agent.config["target_system_prompt"]),
"tools": [],
}
if (
job.agent.mode is ProviderExecutionMode.CONNECT_ONLY
and job.agent.connector.strip().lower() == "livekit"
):
if secrets_path is not None:
secrets_path.unlink(missing_ok=True)
return {
"provider": "livekit",
"modality": "voice",
"agent_name": str(job.agent.config["agent_name"]),
"system_prompt": str(job.agent.config["target_system_prompt"]),
"tools": [],
}
inspect_connect_only_provider = (
job.agent.mode is ProviderExecutionMode.CONNECT_ONLY
and job.agent.connector.strip().lower() in {"vapi", "retell", "retell_chat"}
Expand Down
13 changes: 10 additions & 3 deletions src/fi/alk/harness/call_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,16 @@ def _canonical_simulator_secrets(values: Mapping[str, str]) -> dict[str, str]:
}


def _dispatch_agent_name(runtime: EnvironmentRuntime) -> str | None:
def _dispatch_agent_name(runtime: EnvironmentRuntime, job: HarnessJob) -> str | None:
"""The ONLY place this repo reads the dispatch-identity metadata key, so a
change to the key name/convention is a one-line adapt. The provisioner
mirrors the agent process's rendered LIVEKIT_AGENT_NAME here; a bundle
that declares none (or an ambiguous set) leaves the key absent and the
caller's typed `CallAborted` below fires."""
value = runtime.metadata.get("livekit_agent_name")
if job.agent.mode is ProviderExecutionMode.CONNECT_ONLY:
value = job.agent.config.get("agent_name")
else:
value = runtime.metadata.get("livekit_agent_name")
return value.strip() if isinstance(value, str) and value.strip() else None


Expand Down Expand Up @@ -1167,7 +1170,11 @@ async def run(
connector = _resolve_connector(
self._context.job, self._context.target_provider_secret_values
)
agent_name = _dispatch_agent_name(runtime) if connector == "livekit" else None
agent_name = (
_dispatch_agent_name(runtime, self._context.job)
if connector == "livekit"
else None
)
if connector == "livekit" and agent_name is None:
raise CallAborted(
"voice_dispatch_identity_unavailable: runtime.metadata['livekit_agent_name'] is "
Expand Down
9 changes: 9 additions & 0 deletions src/fi/alk/harness/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ def _provider_mode_is_explicit_and_safe(self) -> "AgentConnection":
if not prompt or len(prompt) > 65536:
raise ValueError("phone_connect_only_requires_target_system_prompt")
return self
if connector == "livekit":
prompt = str(self.config.get("target_system_prompt") or "").strip()
if not str(self.config.get("agent_name") or "").strip():
raise ValueError("livekit_connect_only_requires_agent_name")
if not prompt or len(prompt) > 65536:
raise ValueError(
"livekit_connect_only_requires_target_system_prompt"
)
return self
target_key = {"vapi": "assistant_id", "retell": "agent_id"}.get(
provider_connector
)
Expand Down
2 changes: 1 addition & 1 deletion src/fi/alk/harness/process_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ def _verify_secret_purposes(
# the phone connector dials with platform-owned telephony credentials.
connect_only_claims_target_provider = isinstance(connect_only, dict) and str(
connect_only.get("connector") or ""
).lower() in {"vapi", "retell", "retell_chat"}
).lower() in {"vapi", "retell", "retell_chat", "livekit"}
guest_claims_target_provider = (
process_claims_target_provider
or lifecycle_claims_target_provider
Expand Down
2 changes: 1 addition & 1 deletion src/fi/alk/harness/sandbox_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _one_source(self) -> "LocalSandboxRequest":
):
raise ValueError("exactly_one_source_required")
if self.provider_only and (
self.connector not in {"retell", "retell_chat", "vapi"}
self.connector not in {"retell", "retell_chat", "vapi", "livekit"}
or self.connector_mode is not ProviderExecutionMode.CONNECT_ONLY
):
raise ValueError("provider_only_requires_connect_only_provider")
Expand Down
2 changes: 1 addition & 1 deletion src/fi/alk/harness/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def servers(self) -> dict[str, Any]:
def briefing(self) -> str:
return (
"This is an external agent, not a repository. The sanitized definition below is "
"the only supplied source of truth. A phone-only connection may contain just the "
"the only supplied source of truth. A phone or LiveKit connection may contain just the "
"user-supplied prompt; it is not a verified provider inspection. There is no source "
"code to search or open. Do not invent behavior or tool inputs absent from this "
"definition.\n\n"
Expand Down
81 changes: 81 additions & 0 deletions tests/harness/test_call_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,87 @@ def test_phone_connect_only_authoring_uses_only_supplied_prompt() -> None:
}


_LIVEKIT_HOSTED_CONFIG = {
cr.LIVEKIT_URL_CONFIG_KEY: "wss://customer.livekit.cloud",
"agent_name": "returns-agent",
"target_system_prompt": "You handle returns.",
}


@pytest.mark.parametrize(
("config", "error"),
[
(
{"target_system_prompt": "You handle returns."},
"livekit_connect_only_requires_agent_name",
),
(
{"agent_name": "returns-agent"},
"livekit_connect_only_requires_target_system_prompt",
),
(
{"agent_name": "returns-agent", "target_system_prompt": "x" * 65537},
"livekit_connect_only_requires_target_system_prompt",
),
],
)
def test_livekit_connect_only_requires_agent_name_and_prompt(
config: dict[str, Any], error: str
) -> None:
with pytest.raises(ValueError, match=error):
AgentConnection(
connector="livekit", mode=ProviderExecutionMode.CONNECT_ONLY, config=config
)


def test_livekit_connect_only_authoring_uses_the_pasted_prompt_and_drops_secrets(
tmp_path: Path,
) -> None:
secrets = tmp_path / "authoring-target-secrets.json"
secrets.write_text('{"LIVEKIT_API_KEY": "lk-key"}', encoding="utf-8")
job = _job(
connector="livekit",
mode=ProviderExecutionMode.CONNECT_ONLY,
config=dict(_LIVEKIT_HOSTED_CONFIG),
)

assert _load_provider_import_profile(job, secrets) == {
"provider": "livekit",
"modality": "voice",
"agent_name": "returns-agent",
"system_prompt": "You handle returns.",
"tools": [],
}
assert not secrets.exists()


def test_livekit_connect_only_dispatches_the_configured_agent_name(
tmp_path: Path,
) -> None:
_job_obj, context = _context(
tmp_path=tmp_path,
mode=ProviderExecutionMode.CONNECT_ONLY,
config=dict(_LIVEKIT_HOSTED_CONFIG),
)
_write_scenario_doc(context.bundle_dir, scenario_key="livekit-call")
captured: dict[str, Any] = {}

async def place_call(spec):
captured["spec"] = spec
return _report()

runner = cr.CallRunnerImpl(FakeAdapter(), context, place_call=place_call)
_run(
runner,
_FakeScenario("livekit-call"),
_runtime(metadata={"livekit_agent_name": "agent-from-source"}),
)

config = captured["spec"].environment.config
assert config["agent_definition"]["agent_name"] == "returns-agent"
assert config["livekit_runtime"]["url"] == "wss://customer.livekit.cloud/"


def test_phone_connect_only_rejects_customer_dialer_override() -> None:
with pytest.raises(ValueError, match="phone_dialer_config_is_platform_owned"):
AgentConnection(
Expand Down
30 changes: 30 additions & 0 deletions tests/harness/test_process_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,36 @@ def mutate(body: dict[str, Any]) -> dict[str, Any]:
)


@pytest.mark.parametrize("connector", ["vapi", "retell", "retell_chat", "livekit"])
def test_connect_only_provider_claims_the_customer_target_secret(
tmp_path: Path, connector: str
) -> None:
def mutate(body: dict[str, Any]) -> dict[str, Any]:
body["processes"][1]["secret_purposes"] = []
body["metadata"] = {"provider_connect_only": {"connector": connector}}
return body

manifest = _build_bundle(tmp_path, body_overrides=mutate)
preflight_bundle(
tmp_path, manifest, parallelism=1, secret_refs=TARGET_PROVIDER_REFS
)


def test_connect_only_phone_does_not_claim_a_customer_target_secret(
tmp_path: Path,
) -> None:
def mutate(body: dict[str, Any]) -> dict[str, Any]:
body["processes"][1]["secret_purposes"] = []
body["metadata"] = {"provider_connect_only": {"connector": "phone"}}
return body

manifest = _build_bundle(tmp_path, body_overrides=mutate)
with pytest.raises(PreflightError, match="secret_unclaimed"):
preflight_bundle(
tmp_path, manifest, parallelism=1, secret_refs=TARGET_PROVIDER_REFS
)


def test_customer_process_cannot_claim_simulator_provider_secrets(
tmp_path: Path,
) -> None:
Expand Down
14 changes: 14 additions & 0 deletions tests/test_harness_sandbox_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ async def submit():
LocalSandboxRequest(provider_only=True, connector="auto")


def test_provider_only_accepts_a_connect_only_livekit_agent():
request = LocalSandboxRequest(
provider_only=True,
connector="livekit",
connector_mode="connect_only",
connector_config={
"agent_name": "returns-agent",
"target_system_prompt": "You handle returns.",
},
)

assert request.connector == "livekit"


def test_second_sandbox_instance_does_not_orphan_live_controller(tmp_path):
root = tmp_path / "state"
first = LocalSandbox(root)
Expand Down
Loading