openrua 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openrua/__init__.py +31 -0
- openrua/__main__.py +7 -0
- openrua/agents/__init__.py +17 -0
- openrua/agents/__main__.py +28 -0
- openrua/agents/base.py +231 -0
- openrua/agents/credentials.py +45 -0
- openrua/agents/launcher.py +113 -0
- openrua/agents/prompts.py +26 -0
- openrua/agents/registry.py +194 -0
- openrua/cli/__init__.py +68 -0
- openrua/cli/commands/__init__.py +9 -0
- openrua/cli/commands/agent.py +36 -0
- openrua/cli/commands/bench.py +12 -0
- openrua/cli/commands/build.py +112 -0
- openrua/cli/commands/config.py +85 -0
- openrua/cli/commands/demo.py +76 -0
- openrua/cli/commands/doctor.py +27 -0
- openrua/cli/commands/down.py +24 -0
- openrua/cli/commands/install.py +60 -0
- openrua/cli/commands/list.py +151 -0
- openrua/cli/commands/probe.py +82 -0
- openrua/cli/commands/ps.py +48 -0
- openrua/cli/commands/run.py +51 -0
- openrua/cli/commands/up.py +196 -0
- openrua/cli/output.py +19 -0
- openrua/cli/state.py +38 -0
- openrua/config/__init__.py +18 -0
- openrua/config/loader.py +453 -0
- openrua/config/paths.py +164 -0
- openrua/config/schema.py +664 -0
- openrua/configs/agents/claude-code.yaml +50 -0
- openrua/configs/agents/codex.yaml +47 -0
- openrua/configs/benchmarks/calvin/merged_config.yaml +395 -0
- openrua/configs/benchmarks/calvin.yaml +38 -0
- openrua/configs/benchmarks/capbench.yaml +148 -0
- openrua/configs/benchmarks/libero.yaml +58 -0
- openrua/configs/benchmarks/libero_mem/py312.patch +68 -0
- openrua/configs/benchmarks/libero_mem/requirements.txt +119 -0
- openrua/configs/benchmarks/libero_mem.yaml +62 -0
- openrua/configs/benchmarks/libero_plus/py312.patch +22 -0
- openrua/configs/benchmarks/libero_plus/requirements.txt +124 -0
- openrua/configs/benchmarks/libero_plus.yaml +72 -0
- openrua/configs/benchmarks/libero_pro/libero-pro-suites.tar.gz +0 -0
- openrua/configs/benchmarks/libero_pro/requirements.txt +119 -0
- openrua/configs/benchmarks/libero_pro.yaml +95 -0
- openrua/configs/benchmarks/maniskill.yaml +39 -0
- openrua/configs/benchmarks/mikasa/requirements.txt +101 -0
- openrua/configs/benchmarks/mikasa.yaml +52 -0
- openrua/configs/benchmarks/robocasa/requirements.txt +78 -0
- openrua/configs/benchmarks/robocasa.yaml +87 -0
- openrua/configs/benchmarks/robocasa365/requirements.txt +138 -0
- openrua/configs/benchmarks/robocasa365.yaml +176 -0
- openrua/configs/benchmarks/robocerebra/requirements.txt +119 -0
- openrua/configs/benchmarks/robocerebra.yaml +64 -0
- openrua/configs/benchmarks/robotwin.yaml +40 -0
- openrua/configs/benchmarks/simpler.yaml +44 -0
- openrua/configs/benchmarks/vlabench.yaml +36 -0
- openrua/configs/config.yaml +6 -0
- openrua/configs/robots/aloha-agilex.yaml +41 -0
- openrua/configs/robots/controllers/pandaomron_joint_ctrl.json +29 -0
- openrua/configs/robots/panda-omron.yaml +35 -0
- openrua/configs/robots/panda.yaml +30 -0
- openrua/configs/robots/widowx.yaml +22 -0
- openrua/configs/simulators/calvin/log-without-git.patch +16 -0
- openrua/configs/simulators/calvin/requirements.txt +82 -0
- openrua/configs/simulators/calvin.yaml +35 -0
- openrua/configs/simulators/maniskill/bridge-joint-mode.patch +41 -0
- openrua/configs/simulators/maniskill/requirements.txt +100 -0
- openrua/configs/simulators/maniskill.yaml +54 -0
- openrua/configs/simulators/robosuite/capx-pyproject.patch +89 -0
- openrua/configs/simulators/robosuite/requirements.txt +143 -0
- openrua/configs/simulators/robosuite.yaml +50 -0
- openrua/configs/simulators/robotwin/eval-without-curobo.patch +68 -0
- openrua/configs/simulators/robotwin/requirements.txt +123 -0
- openrua/configs/simulators/robotwin.yaml +44 -0
- openrua/configs/simulators/vlabench/requirements.txt +120 -0
- openrua/configs/simulators/vlabench.yaml +43 -0
- openrua/demo/__init__.py +11 -0
- openrua/demo/compose.py +493 -0
- openrua/doctor/__init__.py +13 -0
- openrua/doctor/checks.py +317 -0
- openrua/doctor/report.py +57 -0
- openrua/errors.py +65 -0
- openrua/plugins/__init__.py +1 -0
- openrua/plugins/agents/__init__.py +2 -0
- openrua/plugins/agents/claude_code.py +454 -0
- openrua/plugins/agents/codex.py +340 -0
- openrua/proxy/__init__.py +35 -0
- openrua/proxy/__main__.py +45 -0
- openrua/proxy/build.py +65 -0
- openrua/proxy/down.py +32 -0
- openrua/proxy/proxy.Dockerfile +16 -0
- openrua/proxy/tinyproxy.conf +16 -0
- openrua/proxy/up.py +133 -0
- openrua/robot/__init__.py +78 -0
- openrua/robot/base.py +28 -0
- openrua/robot/real/__init__.py +10 -0
- openrua/robot/real/down.py +38 -0
- openrua/robot/real/probe.py +147 -0
- openrua/robot/real/up.py +69 -0
- openrua/robot/sim/Dockerfile.humble +18 -0
- openrua/robot/sim/Dockerfile.jazzy +26 -0
- openrua/robot/sim/__init__.py +11 -0
- openrua/robot/sim/bridge/__init__.py +23 -0
- openrua/robot/sim/bridge/catalog.py +43 -0
- openrua/robot/sim/bridge/engines/__init__.py +74 -0
- openrua/robot/sim/bridge/engines/calvin.py +212 -0
- openrua/robot/sim/bridge/engines/frames.py +25 -0
- openrua/robot/sim/bridge/engines/maniskill.py +211 -0
- openrua/robot/sim/bridge/engines/mujoco.py +123 -0
- openrua/robot/sim/bridge/engines/robosuite.py +209 -0
- openrua/robot/sim/bridge/engines/robotwin.py +187 -0
- openrua/robot/sim/bridge/engines/vlabench.py +92 -0
- openrua/robot/sim/bridge/environments/__init__.py +55 -0
- openrua/robot/sim/bridge/environments/calvin.py +176 -0
- openrua/robot/sim/bridge/environments/capbench.py +108 -0
- openrua/robot/sim/bridge/environments/libero.py +169 -0
- openrua/robot/sim/bridge/environments/maniskill.py +111 -0
- openrua/robot/sim/bridge/environments/mikasa.py +144 -0
- openrua/robot/sim/bridge/environments/robocasa.py +100 -0
- openrua/robot/sim/bridge/environments/robocasa365.py +130 -0
- openrua/robot/sim/bridge/environments/robocerebra.py +123 -0
- openrua/robot/sim/bridge/environments/robosuite.py +82 -0
- openrua/robot/sim/bridge/environments/robotwin.py +180 -0
- openrua/robot/sim/bridge/environments/simpler.py +62 -0
- openrua/robot/sim/bridge/environments/vlabench.py +76 -0
- openrua/robot/sim/bridge/environments/worker.py +70 -0
- openrua/robot/sim/bridge/main.py +181 -0
- openrua/robot/sim/bridge/plug.py +50 -0
- openrua/robot/sim/bridge/ros/__init__.py +13 -0
- openrua/robot/sim/bridge/ros/arms.py +18 -0
- openrua/robot/sim/bridge/ros/clock.py +22 -0
- openrua/robot/sim/bridge/ros/controllers.py +334 -0
- openrua/robot/sim/bridge/ros/joints.py +26 -0
- openrua/robot/sim/bridge/ros/launch/moveit.launch.py +100 -0
- openrua/robot/sim/bridge/ros/launch/panda.srdf +127 -0
- openrua/robot/sim/bridge/ros/node.py +39 -0
- openrua/robot/sim/bridge/ros/sensors.py +326 -0
- openrua/robot/sim/bridge/rpc.py +254 -0
- openrua/robot/sim/build.py +67 -0
- openrua/robot/sim/client.py +91 -0
- openrua/robot/sim/down.py +30 -0
- openrua/robot/sim/install.py +127 -0
- openrua/robot/sim/up.py +230 -0
- openrua/runner/__init__.py +20 -0
- openrua/runner/bringup.py +326 -0
- openrua/runner/lock.py +170 -0
- openrua/runner/main.py +200 -0
- openrua/runner/operators.py +213 -0
- openrua/runner/preflight.py +184 -0
- openrua/runner/record.py +419 -0
- openrua/runner/session.py +277 -0
- openrua/runner/trial.py +319 -0
- openrua/sandbox/__init__.py +37 -0
- openrua/sandbox/__main__.py +48 -0
- openrua/sandbox/build.py +94 -0
- openrua/sandbox/down.py +30 -0
- openrua/sandbox/sandbox.Dockerfile +77 -0
- openrua/sandbox/up.py +218 -0
- openrua/sandbox/workspace/.gitignore +2 -0
- openrua/sandbox/workspace/README.md +27 -0
- openrua/sandbox/workspace/docs/10-machine.md +80 -0
- openrua/sandbox/workspace/docs/20-perception.md +109 -0
- openrua/sandbox/workspace/docs/30-action.md +135 -0
- openrua/sandbox/workspace/docs/40-patterns.md +56 -0
- openrua/sandbox/workspace/tools/README.md +10 -0
- openrua/sandbox/workspace/tools/action/base_goto.py +114 -0
- openrua/sandbox/workspace/tools/action/fjt_send.py +57 -0
- openrua/sandbox/workspace/tools/action/gripper_cmd.py +47 -0
- openrua/sandbox/workspace/tools/action/ik_move.py +112 -0
- openrua/sandbox/workspace/tools/perception/cam_snap.py +46 -0
- openrua/sandbox/workspace/tools/perception/px2world.py +76 -0
- openrua/sandbox/workspace.py +216 -0
- openrua/testing.py +98 -0
- openrua-0.0.7.dist-info/METADATA +320 -0
- openrua-0.0.7.dist-info/RECORD +179 -0
- openrua-0.0.7.dist-info/WHEEL +4 -0
- openrua-0.0.7.dist-info/entry_points.txt +3 -0
- openrua-0.0.7.dist-info/licenses/LICENSE +201 -0
openrua/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""OpenRUA: operating robots through their native command-line interface.
|
|
2
|
+
|
|
3
|
+
Folders are units; top-level files compose or are shared (architecture
|
|
4
|
+
contract in pyproject.toml, enforced by tests/test_layering.py):
|
|
5
|
+
|
|
6
|
+
- ``robot`` the machine the agent operates, provided by a backend:
|
|
7
|
+
sim/ (host side + bridge/, the simulated robot's own
|
|
8
|
+
software, container) or real/ (a launch command and a
|
|
9
|
+
handle that waits for the graph)
|
|
10
|
+
- ``sandbox`` the agent's terminal: reachable machine + config ->
|
|
11
|
+
live container (native CLI/rclpy inside)
|
|
12
|
+
- ``proxy`` one shared whitelist proxy to the internet
|
|
13
|
+
- ``agents`` the agent contract, the registry (manifests + hooks),
|
|
14
|
+
the launcher, the prompts
|
|
15
|
+
- ``runner`` running trials: bring-up, preflight (every promise the
|
|
16
|
+
workspace docs make, verified before the agent starts;
|
|
17
|
+
red = trial refused), the operator, the verdict, the
|
|
18
|
+
record (sole runs/ writer), the lock
|
|
19
|
+
- ``cli``, ``doctor`` the command line and the install check
|
|
20
|
+
- ``config``, ``errors``, ``testing`` shared leaves
|
|
21
|
+
|
|
22
|
+
openrua.robot.sim.bridge is container-side (rclpy); host code never
|
|
23
|
+
imports it and reaches it only over DDS (the agent) or its stdio line
|
|
24
|
+
(the runner).
|
|
25
|
+
"""
|
|
26
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = _version("openrua")
|
|
30
|
+
except PackageNotFoundError: # a checkout that was never installed
|
|
31
|
+
__version__ = "0+unknown"
|
openrua/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Agents: the contract, the registry, the launcher, the prompts.
|
|
2
|
+
|
|
3
|
+
Every fact about a particular coding agent lives in its manifest
|
|
4
|
+
(``configs/agents/<name>.yaml``) and its hooks module
|
|
5
|
+
(``plugins/agents/<hooks>.py``); every consumer reaches an agent through
|
|
6
|
+
``get(name)``. Configs carry ``agent.name`` (the package default is in
|
|
7
|
+
``configs/config.yaml``), recorded per trial in ``operator_meta.agent``
|
|
8
|
+
so post-hoc tools resolve the agent a trial actually ran.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from openrua.agents.base import HOOK_NAMES, Agent, Credentials # noqa: F401
|
|
12
|
+
from openrua.agents.credentials import prepare_profile # noqa: F401
|
|
13
|
+
from openrua.agents.prompts import PROMPT, RESUME_PROMPT # noqa: F401
|
|
14
|
+
from openrua.agents.registry import ( # noqa: F401
|
|
15
|
+
Listed, Manifest, available, fact_sha256, get, manifest, manifests, preinstall,
|
|
16
|
+
split_pin, whitelist,
|
|
17
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""``python -m openrua.agents launch ...``: run an agent headless in a live sandbox.
|
|
2
|
+
|
|
3
|
+
The one verb this package exposes from the command line; image builds
|
|
4
|
+
take their facts from the manifests through ``openrua build``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from openrua.agents import launcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> int:
|
|
15
|
+
args = sys.argv[1:]
|
|
16
|
+
if not args or args[0] in ("-h", "--help"):
|
|
17
|
+
print("usage: python -m openrua.agents launch [options]\n"
|
|
18
|
+
"options: python -m openrua.agents launch --help")
|
|
19
|
+
return 0
|
|
20
|
+
if args[0] != "launch":
|
|
21
|
+
print(f"unknown verb {args[0]!r}; the only verb is launch", file=sys.stderr)
|
|
22
|
+
return 2
|
|
23
|
+
sys.argv = ["python -m openrua.agents launch", *args[1:]]
|
|
24
|
+
return launcher.main()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
sys.exit(main())
|
openrua/agents/base.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""The agent contract: what OpenRUA needs to know about one coding agent.
|
|
2
|
+
|
|
3
|
+
An ``Agent`` is composed by the registry from two halves:
|
|
4
|
+
|
|
5
|
+
- the manifest (``configs/agents/<name>.yaml``): the facts, set here as
|
|
6
|
+
plain attributes: name, default model, the shell that installs the
|
|
7
|
+
CLI into the sandbox image, the hosts it talks to, how it logs in;
|
|
8
|
+
- the hooks module (``plugins/agents/<hooks>.py``, exposing ``HOOKS``,
|
|
9
|
+
a subclass of this class): the behaviour. One method is required,
|
|
10
|
+
``launch_argv``, the docker-exec command that runs the agent headless
|
|
11
|
+
on a task. Every other hook has a documented default; a consumer that
|
|
12
|
+
finds the default does without (no interactive mode, no quota
|
|
13
|
+
bookkeeping, no replay). ``capabilities`` says which hooks a class
|
|
14
|
+
implements, so ``openrua agents`` and ``doctor`` can list them.
|
|
15
|
+
|
|
16
|
+
Hooks take ``**_`` so the harness can pass new keyword arguments without
|
|
17
|
+
breaking older hooks modules. ``openrua.testing.check_agent`` is the
|
|
18
|
+
conformance test.
|
|
19
|
+
|
|
20
|
+
Leaf module: imports nothing from openrua.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Credentials:
|
|
32
|
+
"""How a CLI keeps its login, when it keeps one in a profile directory.
|
|
33
|
+
|
|
34
|
+
``dirname``: the profile's directory name under ``~/.openrua/credentials/``.
|
|
35
|
+
``filename``: the file inside it that holds the rotating secret. It is
|
|
36
|
+
bind-mounted as ONE shared file into every sandbox (OAuth refresh
|
|
37
|
+
tokens rotate; copies kill each other), the rest of the profile is a
|
|
38
|
+
fresh per-sandbox copy.
|
|
39
|
+
``config_env``: the environment variable the CLI reads its profile
|
|
40
|
+
directory from.
|
|
41
|
+
``mount_point``: where the profile lands inside the sandbox.
|
|
42
|
+
"""
|
|
43
|
+
dirname: str
|
|
44
|
+
filename: str
|
|
45
|
+
config_env: str
|
|
46
|
+
mount_point: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# The optional hooks, in the order they are documented below. An agent
|
|
50
|
+
# "has" a capability when its hooks class overrides the hook.
|
|
51
|
+
HOOK_NAMES = (
|
|
52
|
+
"interactive_argv", "sandbox_cli_check", "login_hint", "token_hint",
|
|
53
|
+
"quota_probe_argv", "quota_window_open", "matches_quota_anomaly",
|
|
54
|
+
"read_rate_limits", "quota_since", "read_final", "scan_transcript",
|
|
55
|
+
"assistant_turns_before", "replay_ops", "collect",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Agent:
|
|
60
|
+
"""The contract. The manifest's fields arrive as keyword arguments;
|
|
61
|
+
a hooks module subclasses this and implements ``launch_argv``."""
|
|
62
|
+
|
|
63
|
+
# ---- required ---------------------------------------------------
|
|
64
|
+
name: str = "" # agent name (``agent.name`` in configs)
|
|
65
|
+
default_model: str = "" # model when the config names none
|
|
66
|
+
|
|
67
|
+
# ---- manifest facts, all with defaults --------------------------
|
|
68
|
+
binary: str = "" # the CLI executable inside the sandbox
|
|
69
|
+
version: str | None = None # the CLI version the install line pins
|
|
70
|
+
install: str = "" # one-line root shell chain that installs the
|
|
71
|
+
# CLI into the sandbox image ("" = nothing)
|
|
72
|
+
whitelist: tuple[str, ...] = () # hostname regexes the CLI must reach through
|
|
73
|
+
# the proxy (tinyproxy filter syntax)
|
|
74
|
+
credentials: Credentials | None = None # profile-directory login, or None
|
|
75
|
+
token_env: str | None = None # env var carrying a long-lived token, when
|
|
76
|
+
# the CLI accepts one (passed by file)
|
|
77
|
+
version_argv: tuple[str, ...] | None = None # prints the CLI version (provenance)
|
|
78
|
+
instruction_file: str | None = None # the instructions file this CLI reads on
|
|
79
|
+
# its own at start, if any; declared only,
|
|
80
|
+
# nothing is seeded from it yet
|
|
81
|
+
default_options: dict[str, Any] = {} # knobs a config may override under
|
|
82
|
+
# ``agent.options``
|
|
83
|
+
|
|
84
|
+
def __init__(self, **fields: Any) -> None:
|
|
85
|
+
for k, v in fields.items():
|
|
86
|
+
if not hasattr(type(self), k):
|
|
87
|
+
raise TypeError(f"{type(self).__name__} has no attribute {k!r}")
|
|
88
|
+
setattr(self, k, v)
|
|
89
|
+
if not self.name or not self.default_model:
|
|
90
|
+
raise TypeError(f"{type(self).__name__}: name and default_model are required")
|
|
91
|
+
|
|
92
|
+
# ---- the one required method ------------------------------------
|
|
93
|
+
def launch_argv(self, sandbox: str, prompt: str, model: str, max_turns: int,
|
|
94
|
+
proxy: str, options: dict[str, Any] | None = None,
|
|
95
|
+
session_id: str | None = None, resume: bool = False,
|
|
96
|
+
token_file: str | None = None, **_: Any) -> list[str]:
|
|
97
|
+
"""The docker-exec command that runs the agent headless in
|
|
98
|
+
``sandbox`` on ``prompt``, as the ``robot`` user in /workspace,
|
|
99
|
+
with ``proxy`` as its only route out, writing its transcript to
|
|
100
|
+
stdout. ``options`` are the merged adapter knobs. ``session_id``
|
|
101
|
+
names the session so ``resume=True`` can continue it later.
|
|
102
|
+
``token_file`` is a host file holding ``<token_env>=<token>`` for
|
|
103
|
+
docker's ``--env-file``."""
|
|
104
|
+
raise NotImplementedError(f"{self.name}: launch_argv is required")
|
|
105
|
+
|
|
106
|
+
# ---- generic behaviour derived from the declarations ------------
|
|
107
|
+
@staticmethod
|
|
108
|
+
def exec_argv(sandbox: str, env: list[str] = (), token_file: str | None = None,
|
|
109
|
+
interactive: bool = False) -> list[str]:
|
|
110
|
+
"""The ``docker exec`` prefix every hook shares: the ``robot`` user
|
|
111
|
+
in /workspace, ``env`` as ``-e`` pairs already rendered, the
|
|
112
|
+
token file handed to the process by ``--env-file``, a terminal
|
|
113
|
+
when ``interactive``. Append the agent's own command."""
|
|
114
|
+
return [
|
|
115
|
+
"docker", "exec", *(["-it"] if interactive else []),
|
|
116
|
+
"-u", "robot", "-w", "/workspace",
|
|
117
|
+
*(["--env-file", token_file] if token_file else []),
|
|
118
|
+
*env,
|
|
119
|
+
sandbox,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def sandbox_mounts(self, config_dir: Path,
|
|
123
|
+
credentials_file: Path | None = None) -> tuple[str, ...]:
|
|
124
|
+
"""SRC:DST mounts for the sandbox: the per-sandbox profile copy
|
|
125
|
+
at ``credentials.mount_point`` and, when one is given, the shared
|
|
126
|
+
credentials file inside it. Empty for an adapter without a
|
|
127
|
+
profile-directory login."""
|
|
128
|
+
if self.credentials is None:
|
|
129
|
+
return ()
|
|
130
|
+
c = self.credentials
|
|
131
|
+
mounts = (f"{config_dir}:{c.mount_point}",)
|
|
132
|
+
if credentials_file is None:
|
|
133
|
+
return mounts
|
|
134
|
+
return mounts + (f"{credentials_file}:{c.mount_point}/{c.filename}",)
|
|
135
|
+
|
|
136
|
+
def credentials_check(self) -> tuple[str, str] | None:
|
|
137
|
+
"""(name, bash) preflight check that the mounted credentials file is
|
|
138
|
+
readable by the sandbox user. None without a profile login."""
|
|
139
|
+
if self.credentials is None:
|
|
140
|
+
return None
|
|
141
|
+
f = f"{self.credentials.mount_point}/{self.credentials.filename}"
|
|
142
|
+
return ("credentials_readable", f"bash -c '[ ! -e {f} ] || test -r {f}'")
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def capabilities(self) -> frozenset[str]:
|
|
146
|
+
"""The hooks this agent's class implements (overrides)."""
|
|
147
|
+
return frozenset(h for h in HOOK_NAMES
|
|
148
|
+
if getattr(type(self), h) is not getattr(Agent, h))
|
|
149
|
+
|
|
150
|
+
# ---- optional hooks; each docstring states the default ----------
|
|
151
|
+
def interactive_argv(self, sandbox: str, model: str, proxy: str,
|
|
152
|
+
options: dict[str, Any] | None = None,
|
|
153
|
+
prompt: str | None = None, **_: Any) -> list[str] | None:
|
|
154
|
+
"""docker-exec command that opens the agent interactively in the
|
|
155
|
+
sandbox (a person at the keyboard). Default: None, meaning
|
|
156
|
+
``openrua agent`` refuses with "no interactive mode"."""
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def sandbox_cli_check(self) -> tuple[str, str] | None:
|
|
160
|
+
"""(name, bash) preflight check run inside the sandbox before the
|
|
161
|
+
agent starts (a version pin, say). Default: None, no check."""
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def login_hint(self, creds_home: Path) -> str:
|
|
165
|
+
"""One line telling the operator how to log this CLI in with
|
|
166
|
+
``creds_home`` as its profile directory. Default: a generic line."""
|
|
167
|
+
return (f"log the {self.name} CLI in with its profile directory "
|
|
168
|
+
f"set to {creds_home}")
|
|
169
|
+
|
|
170
|
+
def token_hint(self, token_file: Path | str) -> str | None:
|
|
171
|
+
"""How to mint the token file ``launch_argv`` expects. Default:
|
|
172
|
+
None (this CLI takes no token file)."""
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def quota_probe_argv(self, model: str) -> list[str] | None:
|
|
176
|
+
"""A minimal in-sandbox request whose outcome says whether the
|
|
177
|
+
account's quota window is open (see ``quota_window_open``).
|
|
178
|
+
Default: None, no probe; callers treat the window as open."""
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def quota_window_open(self, returncode: int, text: str) -> bool:
|
|
182
|
+
"""Judge a probe's result. Default: True."""
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
def matches_quota_anomaly(self, text: str) -> bool:
|
|
186
|
+
"""Does a runner anomaly string look like a quota wall? Default: False."""
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def read_rate_limits(self, transcript: Path) -> list[dict]:
|
|
190
|
+
"""Every quota reading the CLI wrote into a transcript, normalized
|
|
191
|
+
to ``{window, utilization, resets_at, status, at}``. Default: []."""
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
def quota_since(self, transcript: Path, line: int = 0) -> dict | None:
|
|
195
|
+
"""Quota-wall evidence written after ``line`` (``{evidence,
|
|
196
|
+
resets_at, ...}``), or None. Default: None, never a wall; the
|
|
197
|
+
runner then never suspends a trial."""
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def read_final(self, transcript: Path) -> dict:
|
|
201
|
+
"""The trial's totals from the transcript: ``{num_turns,
|
|
202
|
+
hit_max_turns, usage, cost_usd, duration_ms, segments}``. Default:
|
|
203
|
+
{}, nothing known; the runner then counts turns as unknown."""
|
|
204
|
+
return {}
|
|
205
|
+
|
|
206
|
+
def scan_transcript(self, transcript: Path) -> dict | None:
|
|
207
|
+
"""Classification evidence (``quota``, ``has_final_result``,
|
|
208
|
+
``final_is_error``, ``api_transport_error``, ...). Default: None."""
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def assistant_turns_before(self, transcript: Path, wall_unix: float) -> int | None:
|
|
212
|
+
"""Agent turns completed at or before an instant (post-hoc turn
|
|
213
|
+
budgets). Default: None, unknown."""
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
def replay_ops(self, transcript: Path) -> list[dict]:
|
|
217
|
+
"""The agent's world-facing operations in order, each one of
|
|
218
|
+
``{kind: "shell", command}``, ``{kind: "write", path, content}`` or
|
|
219
|
+
``{kind: "edit", path, old, new, replace_all}``, plus ``output``,
|
|
220
|
+
``duration_s`` and the wall times ``t0``/``t1`` (unix seconds,
|
|
221
|
+
None when the transcript has none). Default: [] (no replay)."""
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
def collect(self, profile_dir: Path, trial_dir: Path) -> list[Path]:
|
|
225
|
+
"""After the run, before the sandbox's profile directory is
|
|
226
|
+
discarded: keep what this agent wants from it in the trial
|
|
227
|
+
directory (a CLI that writes its own session log there, say).
|
|
228
|
+
Returns the files written; they get the same secret scrub as the
|
|
229
|
+
transcript. The file names are this agent's alone: its reading
|
|
230
|
+
hooks find them beside the transcript. Default: nothing kept."""
|
|
231
|
+
return []
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Staging an agent's login for one sandbox.
|
|
2
|
+
|
|
3
|
+
The profile directory (settings and other non-rotating state) is always
|
|
4
|
+
a fresh per-sandbox copy. The credentials file is never copied: OAuth
|
|
5
|
+
refresh tokens rotate, and two copies of the file invalidate each other,
|
|
6
|
+
so the one file is bind-mounted into every sandbox that needs it. A
|
|
7
|
+
sandbox that authenticates with a token of its own needs no credentials
|
|
8
|
+
file at all.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from openrua.agents.base import Agent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def prepare_profile(creds_home: Path, agent: Agent, require_credentials: bool = True
|
|
22
|
+
) -> tuple[Path, Path | None]:
|
|
23
|
+
"""Returns (profile_copy_dir, shared_credentials_file_or_None); the
|
|
24
|
+
caller owns deleting the copy dir.
|
|
25
|
+
|
|
26
|
+
With ``require_credentials`` the credentials file must exist and is
|
|
27
|
+
returned as a path for bind-mounting. Without it (a token
|
|
28
|
+
authenticates the sandbox) the second element is None. An agent
|
|
29
|
+
without a profile-directory login gets an empty copy dir and None.
|
|
30
|
+
"""
|
|
31
|
+
cfg_dir = Path(tempfile.mkdtemp(prefix="openrua-agentcfg-"))
|
|
32
|
+
creds = agent.credentials
|
|
33
|
+
if creds is None:
|
|
34
|
+
return cfg_dir, None
|
|
35
|
+
creds_file: Path | None = creds_home / creds.filename
|
|
36
|
+
if not require_credentials:
|
|
37
|
+
creds_file = None
|
|
38
|
+
elif not creds_file.exists():
|
|
39
|
+
raise RuntimeError(f"credentials missing: {agent.login_hint(creds_home)}")
|
|
40
|
+
for pattern in ("*.json", ".*.json"):
|
|
41
|
+
for f in creds_home.glob(pattern):
|
|
42
|
+
if f.name != creds.filename:
|
|
43
|
+
shutil.copy2(f, cfg_dir / f.name)
|
|
44
|
+
subprocess.run(["chmod", "-R", "777", str(cfg_dir)])
|
|
45
|
+
return cfg_dir, creds_file
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Headless agent launcher: run an agent in a live sandbox.
|
|
2
|
+
|
|
3
|
+
The agent lives inside the sandbox: this module docker-execs the agent
|
|
4
|
+
CLI in the sandbox container as the ``robot`` user in /workspace; all
|
|
5
|
+
native tools operate in-sandbox and containment is the container
|
|
6
|
+
itself. Invoked by the runner as a subprocess (never imported).
|
|
7
|
+
Everything agent-specific (binary, flags, auth env, transcript format)
|
|
8
|
+
comes from the agent selected by ``--agent``.
|
|
9
|
+
|
|
10
|
+
``python -m openrua.agents.launcher --sandbox <container> --task
|
|
11
|
+
"<sentence>" --transcript <path> [--prompt-file <path>] [--agent <name>]
|
|
12
|
+
[--model ...] [--option k=v ...] [--max-turns N] [--proxy http://host:port]
|
|
13
|
+
[--session-id <uuid>] [--resume] [--token-file <path>]``
|
|
14
|
+
|
|
15
|
+
A trial suspended at a quota wall resumes by re-running this launcher with
|
|
16
|
+
``--resume`` and the same ``--session-id``: the agent is shown the
|
|
17
|
+
content-free resume prompt instead of the task (the session already holds
|
|
18
|
+
it), and the new segment is APPENDED to the same transcript so one file
|
|
19
|
+
still holds the whole trial.
|
|
20
|
+
|
|
21
|
+
Network: the sandbox's only way out is the model-API proxy; the agent's
|
|
22
|
+
hooks disable any server-side search the CLI offers (it cannot be
|
|
23
|
+
proxied), verifiable in the transcript.
|
|
24
|
+
|
|
25
|
+
The launcher is pure process construction: no prompt tricks, no aids.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from openrua import agents
|
|
36
|
+
|
|
37
|
+
# Hand-run convenience only: the evaluator always passes the real URL it
|
|
38
|
+
# got from proxy ensure. Must stay consistent with the proxy package's
|
|
39
|
+
# defaults (leaves cannot import each other; tests/test_agents.py guards
|
|
40
|
+
# the pair against drift).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> int:
|
|
44
|
+
ap = argparse.ArgumentParser()
|
|
45
|
+
ap.add_argument("--sandbox", required=True)
|
|
46
|
+
ap.add_argument("--task", required=True)
|
|
47
|
+
ap.add_argument("--prompt-file", default=None,
|
|
48
|
+
help="override prompt template with a {task} "
|
|
49
|
+
"placeholder (default: agents.PROMPT)")
|
|
50
|
+
ap.add_argument("--transcript", required=True)
|
|
51
|
+
ap.add_argument("--agent", default=None,
|
|
52
|
+
help="agent adapter name (default: openrua.agents default)")
|
|
53
|
+
ap.add_argument("--model", default=None,
|
|
54
|
+
help="model id (default: the adapter's default_model)")
|
|
55
|
+
ap.add_argument("--option", action="append", default=[], metavar="KEY=VALUE",
|
|
56
|
+
help="adapter knob (repeatable; the adapter's "
|
|
57
|
+
"default_options apply underneath)")
|
|
58
|
+
ap.add_argument("--home", default=None,
|
|
59
|
+
help="user directory holding agents/ (default: ~/.openrua)")
|
|
60
|
+
ap.add_argument("--max-turns", type=int, default=100)
|
|
61
|
+
ap.add_argument("--proxy", required=True, help="the proxy URL the sandbox reaches its model API through")
|
|
62
|
+
ap.add_argument("--session-id", default=None,
|
|
63
|
+
help="name the session up front so it can be resumed")
|
|
64
|
+
ap.add_argument("--token-file", default=None,
|
|
65
|
+
help="file holding the sandbox CLI's auth token as "
|
|
66
|
+
"KEY=value; docker hands it to the CLI process only")
|
|
67
|
+
ap.add_argument("--resume", action="store_true",
|
|
68
|
+
help="continue --session-id instead of starting it; the "
|
|
69
|
+
"agent is shown agents.RESUME_PROMPT, not the task, and "
|
|
70
|
+
"the transcript is appended to")
|
|
71
|
+
args = ap.parse_args()
|
|
72
|
+
|
|
73
|
+
if args.resume and not args.session_id:
|
|
74
|
+
ap.error("--resume needs the --session-id of the session to continue")
|
|
75
|
+
|
|
76
|
+
agent = agents.get(args.agent, args.home)
|
|
77
|
+
options = {}
|
|
78
|
+
for item in args.option:
|
|
79
|
+
if "=" not in item:
|
|
80
|
+
ap.error(f"--option expects KEY=VALUE, got {item!r}")
|
|
81
|
+
k, v = item.split("=", 1)
|
|
82
|
+
options[k] = v
|
|
83
|
+
if args.resume:
|
|
84
|
+
prompt = agents.RESUME_PROMPT
|
|
85
|
+
else:
|
|
86
|
+
template = Path(args.prompt_file).read_text() if args.prompt_file \
|
|
87
|
+
else agents.PROMPT
|
|
88
|
+
prompt = template.format(task=args.task)
|
|
89
|
+
cmd = agent.launch_argv(
|
|
90
|
+
sandbox=args.sandbox,
|
|
91
|
+
prompt=prompt,
|
|
92
|
+
model=args.model or agent.default_model,
|
|
93
|
+
max_turns=args.max_turns,
|
|
94
|
+
proxy=args.proxy,
|
|
95
|
+
options=options,
|
|
96
|
+
session_id=args.session_id,
|
|
97
|
+
resume=args.resume,
|
|
98
|
+
token_file=args.token_file,
|
|
99
|
+
)
|
|
100
|
+
# stderr goes to a sidecar, not DEVNULL: a launch-dead CLI (docker
|
|
101
|
+
# exec miss, bad flag) exits loud but used to leave zero evidence.
|
|
102
|
+
# Append on resume so the segments of one trial stay in one transcript;
|
|
103
|
+
# truncate otherwise, so a retried attempt never inherits a dead one's
|
|
104
|
+
# records.
|
|
105
|
+
mode = "a" if args.resume else "w"
|
|
106
|
+
with open(args.transcript, mode) as out, \
|
|
107
|
+
open(f"{args.transcript}.stderr", mode) as err:
|
|
108
|
+
proc = subprocess.run(cmd, stdout=out, stderr=err)
|
|
109
|
+
return proc.returncode
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
sys.exit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The two strings an agent is ever shown by the harness.
|
|
2
|
+
|
|
3
|
+
``PROMPT`` opens a trial; ``{task}`` is its only placeholder. The
|
|
4
|
+
launcher formats it and the trial record hashes the same string, so the
|
|
5
|
+
hash pins the text the agent saw. ``RESUME_PROMPT`` is what a trial
|
|
6
|
+
suspended at a quota wall receives when it continues: content-free on
|
|
7
|
+
purpose, since the session already holds the task and the workspace,
|
|
8
|
+
and restating either would hand a resumed trial context an
|
|
9
|
+
uninterrupted one never had. A resumed trial still receives one more
|
|
10
|
+
user turn than an uninterrupted one; that difference is recorded, not
|
|
11
|
+
hidden.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
PROMPT = """\
|
|
15
|
+
You are working on a robot's onboard computer.
|
|
16
|
+
|
|
17
|
+
Your task: {task}
|
|
18
|
+
|
|
19
|
+
Survey the machine yourself to find out what robot this is and what it
|
|
20
|
+
can do; work until the task is physically done, verify it your own way,
|
|
21
|
+
then finish.
|
|
22
|
+
|
|
23
|
+
The workspace contains starter docs and tools you can use.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
RESUME_PROMPT = "Continue where you left off."
|