Skip to content
Closed
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
17 changes: 17 additions & 0 deletions src/fi/alk/harness/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,12 @@ def provision(
else:
compose = None
managed = False
# Prefer the agent's own Compose: the generated adapter models only agent plus datastore, so
# any other service its tools are served by is dropped and every tool call fails.
if compose is None:
shipped = compose_file(source_root)
if shipped is not None:
compose = shipped
if compose is None and contract is not None:
if not packaging.candidates and not (source_root / "Dockerfile").is_file():
try:
Expand Down Expand Up @@ -2227,6 +2233,15 @@ def _runtime_credential_mounts(
# the worker to see the placeholder. Platform credentials always get an
# ALK-owned destination that cannot collide with submitted mounts.
target = f"/run/harness-secrets/{runtime.name}"
if source is None or not _valid_google_credentials(source):
# The runner's own credential, handed over only when the operator opts in, so a
# hosted runner never mounts its platform key into a submitted agent.
if os.environ.get("ALK_ALLOW_HOST_GOOGLE_CREDENTIALS", "").strip() == "1":
env_value = os.environ.get(name, "").strip()
env_path = Path(env_value).expanduser() if env_value else None
if env_path is not None and _valid_google_credentials(env_path):
source = env_path
target = f"/run/harness-secrets/{env_path.name}"
if source is None or not _valid_google_credentials(source):
raise ProvisionError(
"the submitted runtime needs GOOGLE_APPLICATION_CREDENTIALS, but neither its "
Expand Down Expand Up @@ -2358,6 +2373,8 @@ def start_runtime(
arguments.extend(("--volume", f"{source}:{target}:ro"))
injected[name] = target
mounted_credentials.add(name)
# Only resolve an injected host path here. A credential the submitted service mounted itself
# already points at a container path, which would never validate as a host file.
google_path = injected.get("GOOGLE_APPLICATION_CREDENTIALS", "").strip()
if google_path and "GOOGLE_APPLICATION_CREDENTIALS" not in mounted_credentials:
google_source = Path(google_path).expanduser()
Expand Down
23 changes: 23 additions & 0 deletions src/fi/alk/harness/run/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,29 @@ def wire(
"TOOLS_API_URL": url,
"LIVEKIT_AGENT_NAME": agent_name,
}
# The generated runtime carries no .env.local, and the runtime configuration covers
# datastore settings only, so the provider credentials it needs come from here.
for _cred in (
"LIVEKIT_URL",
"LIVEKIT_API_KEY",
"LIVEKIT_API_SECRET",
"DEEPGRAM_API_KEY",
"CARTESIA_API_KEY",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_CLOUD_LOCATION",
):
_value = os.environ.get(_cred, "").strip()
if _value:
runtime_overrides.setdefault(_cred, _value)
# An agent's Compose often mounts its Vertex credential from this variable, and with it
# unset a placeholder is mounted and the agent's own LLM cannot authenticate.
_google_creds = runtime_overrides.get("GOOGLE_APPLICATION_CREDENTIALS", "").strip()
if _google_creds:
runtime_overrides.setdefault("VERTEX_CREDENTIALS", _google_creds)
# Audio enhancement is usually licensed, and unauthorized it raises on the first
# inbound frame and drops the call. Agents that do not read this flag ignore it.
runtime_overrides.setdefault("DISABLE_AI_COUSTICS", "1")
os.environ["LIVEKIT_TARGET_AGENT_NAME"] = agent_name
caller_phone = fixture_phone(scenario)
if caller_phone:
Expand Down
37 changes: 37 additions & 0 deletions src/fi/alk/harness/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def worker_environment(
# Runner-owned model configuration. Uploaded agent values with these names remain in the
# runtime namespace and cannot replace controller credentials.
"ALK_HARNESS_MODEL",
# Lets a local sandbox hand its own Google credential to the runtime it builds. A hosted
# runner leaves this unset so its platform key is never mounted into a submitted agent.
"ALK_ALLOW_HOST_GOOGLE_CREDENTIALS",
"ANTHROPIC_MODEL",
"ANTHROPIC_VERTEX_PROJECT_ID",
"CLAUDE_CODE_USE_VERTEX",
Expand All @@ -108,10 +111,44 @@ def worker_environment(
"FI_BASE_URL",
"FI_API_KEY",
"FI_SECRET_KEY",
# Runner-owned Docker runtime + voice configuration. The harness starts store and agent
# containers on the host daemon and must reach them: ALK_DOCKER_NETWORK lets the store be
# reached by container name on a shared network, ALK_DOCKER_PUBLISHED_HOST/BIND_HOST give
# the host published services are on. Without these the child defaults to 127.0.0.1 --
# its own loopback inside the sandbox -- and every store probe is refused.
"ALK_DOCKER_NETWORK",
"ALK_DOCKER_PUBLISHED_HOST",
"ALK_DOCKER_BIND_HOST",
"ALK_RUNNER_CONTAINER",
"ALK_HARNESS_MODEL",
"ALK_AGENT_MODEL",
"ALK_JUDGE_MODEL",
"ALK_USER_MODEL",
"CLOUD_ML_REGION",
"HARNESS_WEBHOOK_HOST",
"HARNESS_WEBHOOK_PORT",
"HARNESS_WEBHOOK_URL",
"HARNESS_RUNTIME_WEBHOOK_URL",
"HARNESS_VOICE_CASE",
"HARNESS_VOICE_INFRA_RETRIES",
"LIVEKIT_TARGET_AGENT_NAME",
# Local-dev convenience: let the developer's provider creds from their local environment
# reach the worker directly. A hosted provider supplies these through secret_refs instead.
"LIVEKIT_URL",
"LIVEKIT_API_KEY",
"LIVEKIT_API_SECRET",
"ACCEPTANCE_LIVEKIT_URL",
"DEEPGRAM_API_KEY",
"CARTESIA_API_KEY",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_CLOUD_LOCATION",
}
child = {name: value for name, value in host.items() if name in allowed}
reserved = {
"ALK_HARNESS_MODEL",
# A submitted job must not be able to turn on the host-credential fallback for itself.
"ALK_ALLOW_HOST_GOOGLE_CREDENTIALS",
"ANTHROPIC_MODEL",
"ANTHROPIC_VERTEX_PROJECT_ID",
"CLAUDE_CODE_USE_VERTEX",
Expand Down
2 changes: 1 addition & 1 deletion src/fi/alk/harness/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
# remain alive forever after a dropped upstream stream, though, which previously left a hosted
# job looking healthy while making no progress. Bound *inactivity*, not total stage duration:
# long scenario suites remain valid as long as they keep producing observable work.
STAGE_IDLE_TIMEOUT_SECONDS = float(os.getenv("ALK_STAGE_IDLE_TIMEOUT_SECONDS", "180"))
STAGE_IDLE_TIMEOUT_SECONDS = float(os.getenv("ALK_STAGE_IDLE_TIMEOUT_SECONDS", "600"))
STAGE_IDLE_RETRIES = int(os.getenv("ALK_STAGE_IDLE_RETRIES", "1"))


Expand Down
11 changes: 8 additions & 3 deletions src/fi/alk/harness/world/stores/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,14 @@ def save_to(self, path: str | Path) -> None:
def load_from(self, path: str | Path) -> None:
root = Path(path)
schema = root / SCHEMA
if not schema.exists():
raise StoreError(f"no saved Postgres schema at {schema}")
self.apply(schema.read_text(encoding="utf-8"))
# A standalone scenario store starts empty and needs the DDL; a compose-provisioned
# (Attached) store saves no schema.sql because store.json already carries the applied
# CREATE scripts, which Held.load_from replays. Apply schema.sql only when it exists and
# the tables are not already there, so restore works either way and never double-applies.
with self._connect() as connection:
has_schema = bool(self._tables(connection))
if not has_schema and schema.exists():
self.apply(schema.read_text(encoding="utf-8"))
Held.load_from(self, root)

# -- what a scenario changes -----------------------------------------------------
Expand Down
Loading