996 lines
34 KiB
Python
996 lines
34 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any, Literal, cast
|
|
from urllib.parse import urljoin
|
|
|
|
from openai.types.responses import ResponseTextDeltaEvent
|
|
from pydantic import BaseModel
|
|
|
|
from agents import Agent, ModelSettings, Runner, function_tool
|
|
from agents.run import RunConfig
|
|
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
|
|
|
|
if __package__ is None or __package__ == "":
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
|
|
|
|
from examples.sandbox.misc.example_support import text_manifest, tool_call_name
|
|
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
|
|
|
|
try:
|
|
from agents.extensions.sandbox import (
|
|
DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT,
|
|
DEFAULT_RUNLOOP_WORKSPACE_ROOT,
|
|
RunloopAfterIdle,
|
|
RunloopGatewaySpec,
|
|
RunloopLaunchParameters,
|
|
RunloopMcpSpec,
|
|
RunloopSandboxClient,
|
|
RunloopSandboxClientOptions,
|
|
RunloopSandboxSessionState,
|
|
RunloopTunnelConfig,
|
|
RunloopUserParameters,
|
|
)
|
|
except Exception as exc: # pragma: no cover - import path depends on optional extras
|
|
raise SystemExit(
|
|
"Runloop sandbox examples require the optional repo extra.\n"
|
|
"Install it with: uv sync --extra runloop"
|
|
) from exc
|
|
|
|
|
|
DEFAULT_MODEL = "gpt-5.6-sol"
|
|
DEFAULT_HTTP_PORT = 8123
|
|
DEFAULT_AGENT_PROMPT = (
|
|
"Inspect this Runloop sandbox workspace, verify the configuration using the shell tool, "
|
|
"and summarize which Runloop-specific capabilities were exercised."
|
|
)
|
|
EXAMPLE_RESOURCE_SLUG = "runloop-capabilities-example"
|
|
PERSISTENT_SECRET_NAME = "RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN"
|
|
PERSISTENT_SECRET_VALUE = "runloop-capabilities-example-token"
|
|
PERSISTENT_NETWORK_POLICY_NAME = "runloop-capabilities-example-policy"
|
|
HTTP_LOG_PATH = Path(".runloop-http.log")
|
|
RUNTIME_CONTEXT_PATH = Path("runtime_context.json")
|
|
AGENT_PROOF_PATH = Path("verification/agent-proof.txt")
|
|
|
|
|
|
class RunloopResourceQueryResult(BaseModel):
|
|
resource_type: Literal["secret", "network_policy"]
|
|
name: str
|
|
found: bool
|
|
id: str | None = None
|
|
description: str | None = None
|
|
|
|
|
|
class RunloopResourceBootstrapResult(BaseModel):
|
|
resource_type: Literal["secret", "network_policy"]
|
|
name: str
|
|
action: Literal["created", "reused", "override"]
|
|
id: str | None = None
|
|
found_before_bootstrap: bool
|
|
|
|
|
|
def _phase(title: str) -> None:
|
|
print(f"\n=== {title} ===", flush=True)
|
|
|
|
|
|
def _require_env(name: str) -> None:
|
|
if os.environ.get(name):
|
|
return
|
|
raise SystemExit(f"{name} must be set before running this example.")
|
|
|
|
|
|
def _run_id() -> str:
|
|
return uuid.uuid4().hex[:8]
|
|
|
|
|
|
def _summarize_resource(item: object, fields: tuple[str, ...]) -> dict[str, object]:
|
|
summary: dict[str, object] = {}
|
|
for field in fields:
|
|
value = getattr(item, field, None)
|
|
if value is not None:
|
|
summary[field] = value
|
|
return summary
|
|
|
|
|
|
async def _collect_async_items(items: Any, *, limit: int) -> list[Any]:
|
|
collected: list[Any] = []
|
|
async for item in items:
|
|
collected.append(item)
|
|
if len(collected) >= limit:
|
|
break
|
|
return collected
|
|
|
|
|
|
def _status_code(exc: BaseException) -> int | None:
|
|
status_code = getattr(exc, "status_code", None)
|
|
if isinstance(status_code, int):
|
|
return status_code
|
|
response = getattr(exc, "response", None)
|
|
response_status = getattr(response, "status_code", None)
|
|
return response_status if isinstance(response_status, int) else None
|
|
|
|
|
|
def _is_not_found(exc: BaseException) -> bool:
|
|
return _status_code(exc) == 404
|
|
|
|
|
|
def _error_message(exc: BaseException) -> str | None:
|
|
message = getattr(exc, "message", None)
|
|
if isinstance(message, str):
|
|
return message
|
|
body = getattr(exc, "body", None)
|
|
if isinstance(body, dict):
|
|
body_message = body.get("message")
|
|
if isinstance(body_message, str):
|
|
return body_message
|
|
return None
|
|
|
|
|
|
def _is_conflict(exc: BaseException) -> bool:
|
|
status_code = _status_code(exc)
|
|
if status_code == 409:
|
|
return True
|
|
if status_code == 400:
|
|
message = _error_message(exc)
|
|
return isinstance(message, str) and "already exists" in message.lower()
|
|
return False
|
|
|
|
|
|
async def _collect_maybe_async_items(items: Any, *, limit: int) -> list[Any]:
|
|
if hasattr(items, "__aiter__"):
|
|
return await _collect_async_items(items, limit=limit)
|
|
return list(items)[:limit]
|
|
|
|
|
|
async def _read_text(session: Any, path: Path) -> str:
|
|
data = await session.read(path)
|
|
try:
|
|
payload = data.read()
|
|
finally:
|
|
data.close()
|
|
if isinstance(payload, bytes):
|
|
return payload.decode("utf-8")
|
|
return str(payload)
|
|
|
|
|
|
async def _write_json(session: Any, path: Path, payload: dict[str, object]) -> None:
|
|
await session.write(
|
|
path, io.BytesIO(json.dumps(payload, indent=2, sort_keys=True).encode("utf-8"))
|
|
)
|
|
|
|
|
|
def _build_manifest(*, workspace_root: str, context: dict[str, object]) -> Manifest:
|
|
manifest = text_manifest(
|
|
{
|
|
"README.md": (
|
|
"# Runloop Capabilities Example\n\n"
|
|
"This workspace is used to validate the Runloop-specific sandbox integration end "
|
|
"to end.\n"
|
|
),
|
|
"checklist.md": (
|
|
"# Checklist\n\n"
|
|
"1. Inspect the workspace.\n"
|
|
"2. Verify the resource discovery results in the context files.\n"
|
|
"3. Confirm the managed secret is available without printing its full value.\n"
|
|
"4. Confirm the HTTP preview server and verification file.\n"
|
|
"5. Summarize what Runloop-native features were exercised and whether persistent "
|
|
"resources were reused or created.\n"
|
|
),
|
|
"platform_context.json": json.dumps(context, indent=2, sort_keys=True) + "\n",
|
|
}
|
|
)
|
|
return Manifest(root=workspace_root, entries=manifest.entries)
|
|
|
|
|
|
def _build_sandbox_agent(
|
|
*, model: str, manifest: Manifest, managed_secret_name: str
|
|
) -> SandboxAgent:
|
|
return SandboxAgent(
|
|
name="Runloop Capabilities Guide",
|
|
model=model,
|
|
instructions=(
|
|
"Inspect the Runloop sandbox workspace carefully before answering. Use the shell tool "
|
|
"to verify what happened in the environment and keep the final response concise. "
|
|
"Follow this sequence:\n"
|
|
"1. Run `pwd` and `find . -maxdepth 3 -type f | sort`.\n"
|
|
"2. Read `README.md`, `checklist.md`, `platform_context.json`, and `runtime_context.json`.\n"
|
|
"3. Report whether the managed secret and network policy existed before bootstrap by "
|
|
"reading the query/bootstrap summaries from the context files.\n"
|
|
f"4. Confirm whether `${managed_secret_name}` is set, but never print the full value. "
|
|
"Only report whether it exists and its character length.\n"
|
|
f"5. Read `{HTTP_LOG_PATH.as_posix()}` and confirm the HTTP server started.\n"
|
|
f"6. Create `{AGENT_PROOF_PATH.as_posix()}` with these exact lines:\n"
|
|
" runloop_capabilities_verified=true\n"
|
|
" managed_secret_checked=true\n"
|
|
" tunnel_verified=true\n"
|
|
"7. Print that verification file from the shell.\n"
|
|
"8. Final answer: 2 short sentences naming the specific Runloop features exercised, "
|
|
"including whether the persistent secret and policy were reused or created.\n"
|
|
"Only mention facts you verified from files, environment inspection, or shell output."
|
|
),
|
|
default_manifest=manifest,
|
|
capabilities=[WorkspaceShellCapability()],
|
|
model_settings=ModelSettings(tool_choice="required"),
|
|
)
|
|
|
|
|
|
def _build_query_agent(
|
|
*,
|
|
model: str,
|
|
query_secret_tool: Any,
|
|
query_policy_tool: Any,
|
|
managed_secret_name: str,
|
|
network_policy_name: str,
|
|
) -> Agent:
|
|
return Agent(
|
|
name="Runloop Resource Discovery Guide",
|
|
model=model,
|
|
instructions=(
|
|
"Use the provided Runloop query tools to check whether the persistent example "
|
|
"resources already exist before any create step. Keep the final answer concise."
|
|
),
|
|
tools=[query_secret_tool, query_policy_tool],
|
|
model_settings=ModelSettings(tool_choice="required"),
|
|
).clone(
|
|
instructions=(
|
|
"Use the provided Runloop query tools to check whether the persistent example "
|
|
"resources already exist before any create step. Keep the final answer concise."
|
|
),
|
|
handoff_description=None,
|
|
output_type=None,
|
|
)
|
|
|
|
|
|
def _stream_event_banner(event_name: str) -> str | None:
|
|
if event_name == "tool_called":
|
|
return "[tool call]"
|
|
if event_name == "tool_output":
|
|
return "[tool output]"
|
|
return None
|
|
|
|
|
|
def _runloop_state(session: Any) -> RunloopSandboxSessionState:
|
|
return cast(RunloopSandboxSessionState, session.state)
|
|
|
|
|
|
async def _run_plain_agent(
|
|
*,
|
|
agent: Agent,
|
|
prompt: str,
|
|
workflow_name: str,
|
|
stream: bool,
|
|
) -> str:
|
|
if not stream:
|
|
result = await Runner.run(agent, prompt, run_config=RunConfig(workflow_name=workflow_name))
|
|
print(result.final_output)
|
|
return str(result.final_output)
|
|
|
|
stream_result = Runner.run_streamed(
|
|
agent,
|
|
prompt,
|
|
run_config=RunConfig(workflow_name=workflow_name),
|
|
)
|
|
saw_text_delta = False
|
|
saw_any_text = False
|
|
|
|
async for event in stream_result.stream_events():
|
|
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
|
if not saw_text_delta:
|
|
print("assistant> ", end="", flush=True)
|
|
saw_text_delta = True
|
|
print(event.data.delta, end="", flush=True)
|
|
saw_any_text = True
|
|
continue
|
|
|
|
if event.type != "run_item_stream_event":
|
|
continue
|
|
|
|
banner = _stream_event_banner(event.name)
|
|
if banner is None:
|
|
continue
|
|
if saw_text_delta:
|
|
print()
|
|
saw_text_delta = False
|
|
print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True)
|
|
|
|
if saw_text_delta:
|
|
print()
|
|
if not saw_any_text:
|
|
print(stream_result.final_output)
|
|
return str(stream_result.final_output)
|
|
|
|
|
|
async def _run_sandbox_agent(
|
|
*,
|
|
agent: SandboxAgent,
|
|
prompt: str,
|
|
session: Any,
|
|
workflow_name: str,
|
|
stream: bool,
|
|
) -> str:
|
|
if not stream:
|
|
result = await Runner.run(
|
|
agent,
|
|
prompt,
|
|
run_config=RunConfig(
|
|
sandbox=SandboxRunConfig(session=session),
|
|
workflow_name=workflow_name,
|
|
),
|
|
)
|
|
print(result.final_output)
|
|
return str(result.final_output)
|
|
|
|
stream_result = Runner.run_streamed(
|
|
agent,
|
|
prompt,
|
|
run_config=RunConfig(
|
|
sandbox=SandboxRunConfig(session=session),
|
|
workflow_name=workflow_name,
|
|
),
|
|
)
|
|
saw_text_delta = False
|
|
saw_any_text = False
|
|
|
|
async for event in stream_result.stream_events():
|
|
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
|
if not saw_text_delta:
|
|
print("assistant> ", end="", flush=True)
|
|
saw_text_delta = True
|
|
print(event.data.delta, end="", flush=True)
|
|
saw_any_text = True
|
|
continue
|
|
|
|
if event.type != "run_item_stream_event":
|
|
continue
|
|
|
|
banner = _stream_event_banner(event.name)
|
|
if banner is None:
|
|
continue
|
|
if saw_text_delta:
|
|
print()
|
|
saw_text_delta = False
|
|
print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True)
|
|
|
|
if saw_text_delta:
|
|
print()
|
|
if not saw_any_text:
|
|
print(stream_result.final_output)
|
|
return str(stream_result.final_output)
|
|
|
|
|
|
async def _start_http_server(session: Any, *, port: int, workspace_root: str) -> None:
|
|
command = (
|
|
"python -m http.server "
|
|
f"{port} --bind 0.0.0.0 --directory {workspace_root} "
|
|
f"> {HTTP_LOG_PATH.as_posix()} 2>&1 &"
|
|
)
|
|
result = await session.exec(command, shell=True, timeout=10)
|
|
if not result.ok():
|
|
raise RuntimeError(result.stderr.decode("utf-8", errors="replace"))
|
|
|
|
|
|
def _build_endpoint_url(endpoint: Any) -> str:
|
|
scheme = "https" if endpoint.tls else "http"
|
|
port = endpoint.port
|
|
host = endpoint.host
|
|
if (scheme == "https" and port == 443) or (scheme == "http" and port == 80):
|
|
return f"{scheme}://{host}/"
|
|
return f"{scheme}://{host}:{port}/"
|
|
|
|
|
|
async def _fetch_text(url: str, *, timeout_s: float) -> str:
|
|
def _fetch() -> str:
|
|
with urllib.request.urlopen(url, timeout=timeout_s) as response:
|
|
payload = response.read()
|
|
if isinstance(payload, bytes):
|
|
return payload.decode("utf-8", errors="replace")
|
|
return str(payload)
|
|
|
|
return await asyncio.to_thread(_fetch)
|
|
|
|
|
|
async def _poll_http_preview(url: str, *, expected_substring: str, timeout_s: float) -> str:
|
|
deadline = time.monotonic() + timeout_s
|
|
last_error: Exception | None = None
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
body = await _fetch_text(url, timeout_s=5.0)
|
|
if expected_substring in body:
|
|
return body
|
|
except (urllib.error.URLError, TimeoutError) as exc:
|
|
last_error = exc
|
|
await asyncio.sleep(2)
|
|
if last_error is not None:
|
|
raise RuntimeError(f"HTTP preview never became ready: {last_error}") from last_error
|
|
raise RuntimeError("HTTP preview never returned the expected content.")
|
|
|
|
|
|
async def _preflight_public_resources(client: RunloopSandboxClient) -> dict[str, object]:
|
|
blueprints = await _collect_async_items(
|
|
await client.platform.blueprints.list_public(limit=3),
|
|
limit=3,
|
|
)
|
|
benchmarks = await _collect_async_items(
|
|
await client.platform.benchmarks.list_public(limit=3),
|
|
limit=3,
|
|
)
|
|
|
|
blueprint_summaries = [
|
|
_summarize_resource(item, ("id", "name", "status")) for item in blueprints
|
|
]
|
|
benchmark_summaries = [
|
|
_summarize_resource(item, ("id", "name", "description")) for item in benchmarks
|
|
]
|
|
|
|
if blueprint_summaries:
|
|
print("public blueprints:")
|
|
for summary in blueprint_summaries:
|
|
print(f" - {summary}")
|
|
else:
|
|
print("public blueprints: none returned")
|
|
|
|
if benchmark_summaries:
|
|
print("public benchmarks:")
|
|
for summary in benchmark_summaries:
|
|
print(f" - {summary}")
|
|
else:
|
|
print("public benchmarks: none returned")
|
|
|
|
return {
|
|
"public_blueprints": blueprint_summaries,
|
|
"public_benchmarks": benchmark_summaries,
|
|
}
|
|
|
|
|
|
async def _query_runloop_secret(
|
|
client: RunloopSandboxClient,
|
|
*,
|
|
name: str,
|
|
) -> RunloopResourceQueryResult:
|
|
try:
|
|
secret = cast(Any, await client.platform.secrets.get(name))
|
|
except Exception as exc:
|
|
if _is_not_found(exc):
|
|
return RunloopResourceQueryResult(resource_type="secret", name=name, found=False)
|
|
raise
|
|
|
|
return RunloopResourceQueryResult(
|
|
resource_type="secret",
|
|
name=name,
|
|
found=True,
|
|
id=cast(str | None, getattr(secret, "id", None)),
|
|
)
|
|
|
|
|
|
async def _query_runloop_network_policy(
|
|
client: RunloopSandboxClient,
|
|
*,
|
|
name: str,
|
|
) -> RunloopResourceQueryResult:
|
|
policies = await _collect_maybe_async_items(
|
|
await client.platform.network_policies.list(name=name, limit=10),
|
|
limit=10,
|
|
)
|
|
for policy in policies:
|
|
if getattr(policy, "name", None) != name:
|
|
continue
|
|
info = cast(
|
|
Any, await client.platform.network_policies.get(cast(str, policy.id)).get_info()
|
|
)
|
|
return RunloopResourceQueryResult(
|
|
resource_type="network_policy",
|
|
name=name,
|
|
found=True,
|
|
id=cast(str | None, getattr(policy, "id", None)),
|
|
description=cast(str | None, getattr(info, "description", None)),
|
|
)
|
|
|
|
return RunloopResourceQueryResult(resource_type="network_policy", name=name, found=False)
|
|
|
|
|
|
def _build_resource_query_tools(
|
|
client: RunloopSandboxClient,
|
|
*,
|
|
managed_secret_name: str,
|
|
network_policy_name: str,
|
|
) -> tuple[list[Any], dict[str, RunloopResourceQueryResult]]:
|
|
query_results: dict[str, RunloopResourceQueryResult] = {}
|
|
|
|
@function_tool
|
|
async def query_runloop_secret(name: str) -> RunloopResourceQueryResult:
|
|
"""Query whether a Runloop secret exists by name and return non-sensitive metadata."""
|
|
|
|
result = await _query_runloop_secret(client, name=name)
|
|
query_results["secret"] = result
|
|
return result
|
|
|
|
@function_tool
|
|
async def query_runloop_network_policy(name: str) -> RunloopResourceQueryResult:
|
|
"""Query whether a Runloop network policy exists by name and return basic metadata."""
|
|
|
|
result = await _query_runloop_network_policy(client, name=name)
|
|
query_results["network_policy"] = result
|
|
return result
|
|
|
|
tools = [query_runloop_secret, query_runloop_network_policy]
|
|
_ = (managed_secret_name, network_policy_name)
|
|
return tools, query_results
|
|
|
|
|
|
async def _run_resource_query_phase(
|
|
client: RunloopSandboxClient,
|
|
*,
|
|
model: str,
|
|
stream: bool,
|
|
managed_secret_name: str,
|
|
network_policy_name: str,
|
|
) -> tuple[dict[str, RunloopResourceQueryResult], str]:
|
|
tools, query_results = _build_resource_query_tools(
|
|
client,
|
|
managed_secret_name=managed_secret_name,
|
|
network_policy_name=network_policy_name,
|
|
)
|
|
query_agent = Agent(
|
|
name="Runloop Resource Discovery Guide",
|
|
model=model,
|
|
instructions=(
|
|
"Use both query tools before answering. You are checking whether the persistent "
|
|
"Runloop example resources already exist before any create step.\n\n"
|
|
f"1. Call `query_runloop_secret` with `{managed_secret_name}`.\n"
|
|
f"2. Call `query_runloop_network_policy` with `{network_policy_name}`.\n"
|
|
"3. Final answer in 2 short sentences stating whether each resource already exists."
|
|
),
|
|
tools=tools,
|
|
model_settings=ModelSettings(tool_choice="required"),
|
|
)
|
|
prompt = (
|
|
"Check whether the persistent Runloop secret and network policy for this example already "
|
|
"exist before the script attempts any create or reuse step."
|
|
)
|
|
output = await _run_plain_agent(
|
|
agent=query_agent,
|
|
prompt=prompt,
|
|
workflow_name="Runloop resource query example",
|
|
stream=stream,
|
|
)
|
|
if "secret" not in query_results or "network_policy" not in query_results:
|
|
raise RuntimeError("The query agent did not call both Runloop resource query tools.")
|
|
return query_results, output
|
|
|
|
|
|
async def _bootstrap_persistent_resources(
|
|
client: RunloopSandboxClient,
|
|
*,
|
|
managed_secret_name: str,
|
|
managed_secret_value: str,
|
|
network_policy_name: str,
|
|
network_policy_id_override: str | None,
|
|
query_results: dict[str, RunloopResourceQueryResult],
|
|
axon_name: str | None,
|
|
) -> dict[str, object]:
|
|
secret_query = query_results["secret"]
|
|
policy_query = query_results["network_policy"]
|
|
|
|
bootstrap: dict[str, object] = {
|
|
"managed_secret_value": managed_secret_value,
|
|
"secret": RunloopResourceBootstrapResult(
|
|
resource_type="secret",
|
|
name=managed_secret_name,
|
|
action="reused" if secret_query.found else "created",
|
|
id=secret_query.id,
|
|
found_before_bootstrap=secret_query.found,
|
|
),
|
|
"network_policy": RunloopResourceBootstrapResult(
|
|
resource_type="network_policy",
|
|
name=network_policy_name,
|
|
action="override"
|
|
if network_policy_id_override
|
|
else ("reused" if policy_query.found else "created"),
|
|
id=network_policy_id_override or policy_query.id,
|
|
found_before_bootstrap=policy_query.found,
|
|
),
|
|
"axon_id": None,
|
|
"axon_name": axon_name,
|
|
}
|
|
|
|
secret_result = cast(RunloopResourceBootstrapResult, bootstrap["secret"])
|
|
if not secret_query.found:
|
|
created_secret = cast(
|
|
Any,
|
|
await client.platform.secrets.create(
|
|
name=managed_secret_name, value=managed_secret_value
|
|
),
|
|
)
|
|
secret_result.id = cast(str | None, getattr(created_secret, "id", None))
|
|
print(
|
|
"persistent secret bootstrap:",
|
|
secret_result.model_dump(mode="json"),
|
|
)
|
|
|
|
policy_result = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"])
|
|
if network_policy_id_override is None and not policy_query.found:
|
|
try:
|
|
created_policy = cast(
|
|
Any,
|
|
await client.platform.network_policies.create(
|
|
name=network_policy_name,
|
|
allow_all=True,
|
|
description="Persistent network policy for the Runloop capabilities example.",
|
|
),
|
|
)
|
|
except Exception as exc:
|
|
if not _is_conflict(exc):
|
|
raise
|
|
policy_result.action = "reused"
|
|
policy_result.found_before_bootstrap = True
|
|
refreshed_policy = await _query_runloop_network_policy(client, name=network_policy_name)
|
|
policy_result.id = refreshed_policy.id
|
|
else:
|
|
policy_result.id = cast(str | None, getattr(created_policy, "id", None))
|
|
print(
|
|
"persistent network policy bootstrap:",
|
|
policy_result.model_dump(mode="json"),
|
|
)
|
|
|
|
if axon_name is not None:
|
|
axon = cast(Any, await client.platform.axons.create(name=axon_name))
|
|
await client.platform.axons.query_sql(
|
|
cast(str, axon.id),
|
|
sql="CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL)",
|
|
)
|
|
await client.platform.axons.batch_sql(
|
|
cast(str, axon.id),
|
|
statements=[
|
|
{"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["capabilities"]},
|
|
{"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["agent_guided"]},
|
|
],
|
|
)
|
|
query_result = cast(
|
|
Any,
|
|
await client.platform.axons.query_sql(
|
|
cast(str, axon.id),
|
|
sql="SELECT COUNT(*) AS total_events FROM events",
|
|
),
|
|
)
|
|
publish_result = cast(
|
|
Any,
|
|
await client.platform.axons.publish(
|
|
cast(str, axon.id),
|
|
event_type="capabilities_example",
|
|
origin="AGENT_EVENT",
|
|
payload=json.dumps({"axon_name": axon_name}),
|
|
source="openai-agents-python",
|
|
),
|
|
)
|
|
bootstrap["axon_id"] = cast(str, axon.id)
|
|
print(
|
|
"axon demo created:",
|
|
{
|
|
"id": cast(str, axon.id),
|
|
"name": axon_name,
|
|
"rows": query_result.rows,
|
|
"published": getattr(publish_result, "published", None),
|
|
},
|
|
)
|
|
|
|
return bootstrap
|
|
|
|
|
|
def _optional_gateways(args: argparse.Namespace) -> dict[str, RunloopGatewaySpec]:
|
|
if not (args.gateway_env_var and args.gateway_name and args.gateway_secret_name):
|
|
return {}
|
|
return {
|
|
args.gateway_env_var: RunloopGatewaySpec(
|
|
gateway=args.gateway_name,
|
|
secret=args.gateway_secret_name,
|
|
)
|
|
}
|
|
|
|
|
|
def _optional_mcp(args: argparse.Namespace) -> dict[str, RunloopMcpSpec]:
|
|
if not (args.mcp_env_var and args.mcp_config and args.mcp_secret_name):
|
|
return {}
|
|
return {
|
|
args.mcp_env_var: RunloopMcpSpec(
|
|
mcp_config=args.mcp_config,
|
|
secret=args.mcp_secret_name,
|
|
)
|
|
}
|
|
|
|
|
|
async def main(args: argparse.Namespace) -> None:
|
|
_require_env("OPENAI_API_KEY")
|
|
_require_env("RUNLOOP_API_KEY")
|
|
|
|
workspace_root = (
|
|
DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if args.root else DEFAULT_RUNLOOP_WORKSPACE_ROOT
|
|
)
|
|
run_id = _run_id()
|
|
metadata = {
|
|
"example": "runloop-capabilities",
|
|
"run_id": run_id,
|
|
}
|
|
|
|
client = RunloopSandboxClient()
|
|
session = None
|
|
resumed = None
|
|
session_closed = False
|
|
resumed_closed = False
|
|
|
|
try:
|
|
_phase("Public Resource Discovery")
|
|
public_context = await _preflight_public_resources(client)
|
|
|
|
_phase("Agent Resource Discovery")
|
|
query_results, query_agent_output = await _run_resource_query_phase(
|
|
client,
|
|
model=args.model,
|
|
stream=args.stream,
|
|
managed_secret_name=PERSISTENT_SECRET_NAME,
|
|
network_policy_name=PERSISTENT_NETWORK_POLICY_NAME,
|
|
)
|
|
print(
|
|
"resource query results:",
|
|
{key: value.model_dump(mode="json") for key, value in query_results.items()},
|
|
)
|
|
|
|
_phase("Persistent Resource Bootstrap")
|
|
axon_name = f"{EXAMPLE_RESOURCE_SLUG}-axon-{run_id}" if args.with_axon_demo else None
|
|
bootstrap = await _bootstrap_persistent_resources(
|
|
client,
|
|
managed_secret_name=PERSISTENT_SECRET_NAME,
|
|
managed_secret_value=PERSISTENT_SECRET_VALUE,
|
|
network_policy_name=PERSISTENT_NETWORK_POLICY_NAME,
|
|
network_policy_id_override=args.network_policy_id,
|
|
query_results=query_results,
|
|
axon_name=axon_name,
|
|
)
|
|
secret_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["secret"])
|
|
network_policy_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"])
|
|
network_policy_id = network_policy_bootstrap.id
|
|
|
|
context = {
|
|
"example_slug": EXAMPLE_RESOURCE_SLUG,
|
|
"workspace_root": workspace_root,
|
|
"requested_blueprint_name": args.blueprint_name,
|
|
"public_resources": public_context,
|
|
"resource_query_agent_output": query_agent_output,
|
|
"resource_queries": {
|
|
key: value.model_dump(mode="json") for key, value in query_results.items()
|
|
},
|
|
"resource_bootstrap": {
|
|
"secret": secret_bootstrap.model_dump(mode="json"),
|
|
"network_policy": network_policy_bootstrap.model_dump(mode="json"),
|
|
"axon_id": bootstrap["axon_id"],
|
|
"axon_name": bootstrap["axon_name"],
|
|
},
|
|
"managed_secret_env_var": PERSISTENT_SECRET_NAME,
|
|
"network_policy_id": network_policy_id,
|
|
"metadata": metadata,
|
|
"gateway_bindings": sorted(_optional_gateways(args)),
|
|
"mcp_bindings": sorted(_optional_mcp(args)),
|
|
}
|
|
|
|
manifest = _build_manifest(workspace_root=workspace_root, context=context)
|
|
agent = _build_sandbox_agent(
|
|
model=args.model,
|
|
manifest=manifest,
|
|
managed_secret_name=PERSISTENT_SECRET_NAME,
|
|
)
|
|
options = RunloopSandboxClientOptions(
|
|
blueprint_name=args.blueprint_name,
|
|
pause_on_exit=True,
|
|
exposed_ports=(args.http_port,),
|
|
user_parameters=(RunloopUserParameters(username="root", uid=0) if args.root else None),
|
|
launch_parameters=RunloopLaunchParameters(
|
|
network_policy_id=network_policy_id,
|
|
resource_size_request=args.resource_size,
|
|
after_idle=RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend"),
|
|
launch_commands=["echo runloop-capabilities-example"],
|
|
),
|
|
tunnel=RunloopTunnelConfig(
|
|
auth_mode="open",
|
|
http_keep_alive=True,
|
|
wake_on_http=True,
|
|
),
|
|
gateways=_optional_gateways(args),
|
|
mcp=_optional_mcp(args),
|
|
metadata=metadata,
|
|
managed_secrets={PERSISTENT_SECRET_NAME: PERSISTENT_SECRET_VALUE},
|
|
)
|
|
|
|
_phase("Sandbox Create")
|
|
session = await client.create(manifest=manifest, options=options)
|
|
await session.start()
|
|
session_state = _runloop_state(session)
|
|
print(
|
|
"session started:",
|
|
{
|
|
"devbox_id": session_state.devbox_id,
|
|
"secret_refs": session_state.secret_refs,
|
|
"metadata": session_state.metadata,
|
|
},
|
|
)
|
|
|
|
_phase("Tunnel Check")
|
|
await _write_json(
|
|
session,
|
|
RUNTIME_CONTEXT_PATH,
|
|
{
|
|
**context,
|
|
"devbox_id": session_state.devbox_id,
|
|
"secret_refs": session_state.secret_refs,
|
|
"runtime_phase": "before_tunnel_check",
|
|
},
|
|
)
|
|
await _start_http_server(session, port=args.http_port, workspace_root=workspace_root)
|
|
endpoint = await session.resolve_exposed_port(args.http_port)
|
|
preview_url = urljoin(_build_endpoint_url(endpoint), "README.md")
|
|
preview_body = await _poll_http_preview(
|
|
preview_url,
|
|
expected_substring="Runloop Capabilities Example",
|
|
timeout_s=45.0,
|
|
)
|
|
print("resolved tunnel:", preview_url)
|
|
await _write_json(
|
|
session,
|
|
RUNTIME_CONTEXT_PATH,
|
|
{
|
|
**context,
|
|
"devbox_id": session_state.devbox_id,
|
|
"secret_refs": session_state.secret_refs,
|
|
"tunnel_url": preview_url,
|
|
"http_preview_contains_readme": "Runloop Capabilities Example" in preview_body,
|
|
"runtime_phase": "before_agent_run",
|
|
},
|
|
)
|
|
|
|
_phase("Agent Verification")
|
|
await _run_sandbox_agent(
|
|
agent=agent,
|
|
prompt=args.prompt,
|
|
session=session,
|
|
workflow_name="Runloop capabilities example",
|
|
stream=args.stream,
|
|
)
|
|
proof_text = await _read_text(session, AGENT_PROOF_PATH)
|
|
print("agent proof:")
|
|
print(proof_text.rstrip())
|
|
|
|
_phase("Suspend")
|
|
await session.aclose()
|
|
session_closed = True
|
|
print("session persisted and suspended")
|
|
|
|
_phase("Resume Check")
|
|
resumed = await client.resume(session.state)
|
|
await resumed.start()
|
|
resumed_state = _runloop_state(resumed)
|
|
resumed_runtime_context = await _read_text(resumed, RUNTIME_CONTEXT_PATH)
|
|
resumed_proof_text = await _read_text(resumed, AGENT_PROOF_PATH)
|
|
print("resumed runtime context bytes:", len(resumed_runtime_context.encode("utf-8")))
|
|
print("resumed proof:")
|
|
print(resumed_proof_text.rstrip())
|
|
resumed_state.pause_on_exit = False
|
|
await resumed.aclose()
|
|
resumed_closed = True
|
|
print("resumed session cleaned up with delete semantics")
|
|
|
|
_phase("Persistent Resource Summary")
|
|
print(
|
|
"persistent resources retained:",
|
|
{
|
|
"secret": secret_bootstrap.model_dump(mode="json"),
|
|
"network_policy": network_policy_bootstrap.model_dump(mode="json"),
|
|
},
|
|
)
|
|
if bootstrap["axon_id"] is not None:
|
|
print(
|
|
"axon retained for manual cleanup:",
|
|
{
|
|
"axon_id": bootstrap["axon_id"],
|
|
"axon_name": bootstrap["axon_name"],
|
|
},
|
|
)
|
|
finally:
|
|
if resumed is not None and not resumed_closed:
|
|
try:
|
|
_runloop_state(resumed).pause_on_exit = False
|
|
await resumed.aclose()
|
|
except Exception as exc:
|
|
print(f"warning: failed to close resumed session cleanly: {exc}")
|
|
elif session is not None and not session_closed:
|
|
try:
|
|
_runloop_state(session).pause_on_exit = False
|
|
await session.aclose()
|
|
except Exception as exc:
|
|
print(f"warning: failed to close initial session cleanly: {exc}")
|
|
elif session is not None and session_closed and resumed is None:
|
|
try:
|
|
cleanup_session = await client.resume(session.state)
|
|
_runloop_state(cleanup_session).pause_on_exit = False
|
|
await cleanup_session.aclose()
|
|
except Exception as exc:
|
|
print(f"warning: failed to resume suspended session for cleanup: {exc}")
|
|
|
|
await client.close()
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
|
|
parser.add_argument(
|
|
"--prompt", default=DEFAULT_AGENT_PROMPT, help="Prompt to send to the agent."
|
|
)
|
|
parser.add_argument("--blueprint-name", default=None, help="Optional Runloop blueprint name.")
|
|
parser.add_argument(
|
|
"--resource-size",
|
|
default="MEDIUM",
|
|
choices=["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"],
|
|
help="Runloop resource size request for the devbox.",
|
|
)
|
|
parser.add_argument(
|
|
"--network-policy-id",
|
|
default=None,
|
|
help="Optional Runloop network policy id override. Without this flag, the example reuses or creates the persistent example policy by name.",
|
|
)
|
|
parser.add_argument(
|
|
"--http-port",
|
|
type=int,
|
|
default=DEFAULT_HTTP_PORT,
|
|
help="Port used by the preview HTTP server.",
|
|
)
|
|
parser.add_argument(
|
|
"--root",
|
|
action="store_true",
|
|
default=False,
|
|
help="Launch the Runloop devbox as root. The workspace root becomes /root.",
|
|
)
|
|
parser.add_argument(
|
|
"--stream",
|
|
action="store_true",
|
|
default=False,
|
|
help="Stream the agent response and tool activity.",
|
|
)
|
|
parser.add_argument(
|
|
"--with-axon-demo",
|
|
action="store_true",
|
|
default=False,
|
|
help="Also create and use a temporary Axon. This leaves the Axon behind for manual cleanup.",
|
|
)
|
|
parser.add_argument(
|
|
"--gateway-env-var", default=None, help="Env var name for a gateway binding."
|
|
)
|
|
parser.add_argument(
|
|
"--gateway-name", default=None, help="Runloop gateway name for the binding."
|
|
)
|
|
parser.add_argument(
|
|
"--gateway-secret-name",
|
|
default=None,
|
|
help="Runloop secret name used by the gateway binding.",
|
|
)
|
|
parser.add_argument("--mcp-env-var", default=None, help="Env var name for an MCP binding.")
|
|
parser.add_argument(
|
|
"--mcp-config", default=None, help="Runloop MCP config name for the binding."
|
|
)
|
|
parser.add_argument(
|
|
"--mcp-secret-name",
|
|
default=None,
|
|
help="Runloop secret name used by the MCP binding.",
|
|
)
|
|
return parser
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main(_build_parser().parse_args()))
|