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.
Files changed (179) hide show
  1. openrua/__init__.py +31 -0
  2. openrua/__main__.py +7 -0
  3. openrua/agents/__init__.py +17 -0
  4. openrua/agents/__main__.py +28 -0
  5. openrua/agents/base.py +231 -0
  6. openrua/agents/credentials.py +45 -0
  7. openrua/agents/launcher.py +113 -0
  8. openrua/agents/prompts.py +26 -0
  9. openrua/agents/registry.py +194 -0
  10. openrua/cli/__init__.py +68 -0
  11. openrua/cli/commands/__init__.py +9 -0
  12. openrua/cli/commands/agent.py +36 -0
  13. openrua/cli/commands/bench.py +12 -0
  14. openrua/cli/commands/build.py +112 -0
  15. openrua/cli/commands/config.py +85 -0
  16. openrua/cli/commands/demo.py +76 -0
  17. openrua/cli/commands/doctor.py +27 -0
  18. openrua/cli/commands/down.py +24 -0
  19. openrua/cli/commands/install.py +60 -0
  20. openrua/cli/commands/list.py +151 -0
  21. openrua/cli/commands/probe.py +82 -0
  22. openrua/cli/commands/ps.py +48 -0
  23. openrua/cli/commands/run.py +51 -0
  24. openrua/cli/commands/up.py +196 -0
  25. openrua/cli/output.py +19 -0
  26. openrua/cli/state.py +38 -0
  27. openrua/config/__init__.py +18 -0
  28. openrua/config/loader.py +453 -0
  29. openrua/config/paths.py +164 -0
  30. openrua/config/schema.py +664 -0
  31. openrua/configs/agents/claude-code.yaml +50 -0
  32. openrua/configs/agents/codex.yaml +47 -0
  33. openrua/configs/benchmarks/calvin/merged_config.yaml +395 -0
  34. openrua/configs/benchmarks/calvin.yaml +38 -0
  35. openrua/configs/benchmarks/capbench.yaml +148 -0
  36. openrua/configs/benchmarks/libero.yaml +58 -0
  37. openrua/configs/benchmarks/libero_mem/py312.patch +68 -0
  38. openrua/configs/benchmarks/libero_mem/requirements.txt +119 -0
  39. openrua/configs/benchmarks/libero_mem.yaml +62 -0
  40. openrua/configs/benchmarks/libero_plus/py312.patch +22 -0
  41. openrua/configs/benchmarks/libero_plus/requirements.txt +124 -0
  42. openrua/configs/benchmarks/libero_plus.yaml +72 -0
  43. openrua/configs/benchmarks/libero_pro/libero-pro-suites.tar.gz +0 -0
  44. openrua/configs/benchmarks/libero_pro/requirements.txt +119 -0
  45. openrua/configs/benchmarks/libero_pro.yaml +95 -0
  46. openrua/configs/benchmarks/maniskill.yaml +39 -0
  47. openrua/configs/benchmarks/mikasa/requirements.txt +101 -0
  48. openrua/configs/benchmarks/mikasa.yaml +52 -0
  49. openrua/configs/benchmarks/robocasa/requirements.txt +78 -0
  50. openrua/configs/benchmarks/robocasa.yaml +87 -0
  51. openrua/configs/benchmarks/robocasa365/requirements.txt +138 -0
  52. openrua/configs/benchmarks/robocasa365.yaml +176 -0
  53. openrua/configs/benchmarks/robocerebra/requirements.txt +119 -0
  54. openrua/configs/benchmarks/robocerebra.yaml +64 -0
  55. openrua/configs/benchmarks/robotwin.yaml +40 -0
  56. openrua/configs/benchmarks/simpler.yaml +44 -0
  57. openrua/configs/benchmarks/vlabench.yaml +36 -0
  58. openrua/configs/config.yaml +6 -0
  59. openrua/configs/robots/aloha-agilex.yaml +41 -0
  60. openrua/configs/robots/controllers/pandaomron_joint_ctrl.json +29 -0
  61. openrua/configs/robots/panda-omron.yaml +35 -0
  62. openrua/configs/robots/panda.yaml +30 -0
  63. openrua/configs/robots/widowx.yaml +22 -0
  64. openrua/configs/simulators/calvin/log-without-git.patch +16 -0
  65. openrua/configs/simulators/calvin/requirements.txt +82 -0
  66. openrua/configs/simulators/calvin.yaml +35 -0
  67. openrua/configs/simulators/maniskill/bridge-joint-mode.patch +41 -0
  68. openrua/configs/simulators/maniskill/requirements.txt +100 -0
  69. openrua/configs/simulators/maniskill.yaml +54 -0
  70. openrua/configs/simulators/robosuite/capx-pyproject.patch +89 -0
  71. openrua/configs/simulators/robosuite/requirements.txt +143 -0
  72. openrua/configs/simulators/robosuite.yaml +50 -0
  73. openrua/configs/simulators/robotwin/eval-without-curobo.patch +68 -0
  74. openrua/configs/simulators/robotwin/requirements.txt +123 -0
  75. openrua/configs/simulators/robotwin.yaml +44 -0
  76. openrua/configs/simulators/vlabench/requirements.txt +120 -0
  77. openrua/configs/simulators/vlabench.yaml +43 -0
  78. openrua/demo/__init__.py +11 -0
  79. openrua/demo/compose.py +493 -0
  80. openrua/doctor/__init__.py +13 -0
  81. openrua/doctor/checks.py +317 -0
  82. openrua/doctor/report.py +57 -0
  83. openrua/errors.py +65 -0
  84. openrua/plugins/__init__.py +1 -0
  85. openrua/plugins/agents/__init__.py +2 -0
  86. openrua/plugins/agents/claude_code.py +454 -0
  87. openrua/plugins/agents/codex.py +340 -0
  88. openrua/proxy/__init__.py +35 -0
  89. openrua/proxy/__main__.py +45 -0
  90. openrua/proxy/build.py +65 -0
  91. openrua/proxy/down.py +32 -0
  92. openrua/proxy/proxy.Dockerfile +16 -0
  93. openrua/proxy/tinyproxy.conf +16 -0
  94. openrua/proxy/up.py +133 -0
  95. openrua/robot/__init__.py +78 -0
  96. openrua/robot/base.py +28 -0
  97. openrua/robot/real/__init__.py +10 -0
  98. openrua/robot/real/down.py +38 -0
  99. openrua/robot/real/probe.py +147 -0
  100. openrua/robot/real/up.py +69 -0
  101. openrua/robot/sim/Dockerfile.humble +18 -0
  102. openrua/robot/sim/Dockerfile.jazzy +26 -0
  103. openrua/robot/sim/__init__.py +11 -0
  104. openrua/robot/sim/bridge/__init__.py +23 -0
  105. openrua/robot/sim/bridge/catalog.py +43 -0
  106. openrua/robot/sim/bridge/engines/__init__.py +74 -0
  107. openrua/robot/sim/bridge/engines/calvin.py +212 -0
  108. openrua/robot/sim/bridge/engines/frames.py +25 -0
  109. openrua/robot/sim/bridge/engines/maniskill.py +211 -0
  110. openrua/robot/sim/bridge/engines/mujoco.py +123 -0
  111. openrua/robot/sim/bridge/engines/robosuite.py +209 -0
  112. openrua/robot/sim/bridge/engines/robotwin.py +187 -0
  113. openrua/robot/sim/bridge/engines/vlabench.py +92 -0
  114. openrua/robot/sim/bridge/environments/__init__.py +55 -0
  115. openrua/robot/sim/bridge/environments/calvin.py +176 -0
  116. openrua/robot/sim/bridge/environments/capbench.py +108 -0
  117. openrua/robot/sim/bridge/environments/libero.py +169 -0
  118. openrua/robot/sim/bridge/environments/maniskill.py +111 -0
  119. openrua/robot/sim/bridge/environments/mikasa.py +144 -0
  120. openrua/robot/sim/bridge/environments/robocasa.py +100 -0
  121. openrua/robot/sim/bridge/environments/robocasa365.py +130 -0
  122. openrua/robot/sim/bridge/environments/robocerebra.py +123 -0
  123. openrua/robot/sim/bridge/environments/robosuite.py +82 -0
  124. openrua/robot/sim/bridge/environments/robotwin.py +180 -0
  125. openrua/robot/sim/bridge/environments/simpler.py +62 -0
  126. openrua/robot/sim/bridge/environments/vlabench.py +76 -0
  127. openrua/robot/sim/bridge/environments/worker.py +70 -0
  128. openrua/robot/sim/bridge/main.py +181 -0
  129. openrua/robot/sim/bridge/plug.py +50 -0
  130. openrua/robot/sim/bridge/ros/__init__.py +13 -0
  131. openrua/robot/sim/bridge/ros/arms.py +18 -0
  132. openrua/robot/sim/bridge/ros/clock.py +22 -0
  133. openrua/robot/sim/bridge/ros/controllers.py +334 -0
  134. openrua/robot/sim/bridge/ros/joints.py +26 -0
  135. openrua/robot/sim/bridge/ros/launch/moveit.launch.py +100 -0
  136. openrua/robot/sim/bridge/ros/launch/panda.srdf +127 -0
  137. openrua/robot/sim/bridge/ros/node.py +39 -0
  138. openrua/robot/sim/bridge/ros/sensors.py +326 -0
  139. openrua/robot/sim/bridge/rpc.py +254 -0
  140. openrua/robot/sim/build.py +67 -0
  141. openrua/robot/sim/client.py +91 -0
  142. openrua/robot/sim/down.py +30 -0
  143. openrua/robot/sim/install.py +127 -0
  144. openrua/robot/sim/up.py +230 -0
  145. openrua/runner/__init__.py +20 -0
  146. openrua/runner/bringup.py +326 -0
  147. openrua/runner/lock.py +170 -0
  148. openrua/runner/main.py +200 -0
  149. openrua/runner/operators.py +213 -0
  150. openrua/runner/preflight.py +184 -0
  151. openrua/runner/record.py +419 -0
  152. openrua/runner/session.py +277 -0
  153. openrua/runner/trial.py +319 -0
  154. openrua/sandbox/__init__.py +37 -0
  155. openrua/sandbox/__main__.py +48 -0
  156. openrua/sandbox/build.py +94 -0
  157. openrua/sandbox/down.py +30 -0
  158. openrua/sandbox/sandbox.Dockerfile +77 -0
  159. openrua/sandbox/up.py +218 -0
  160. openrua/sandbox/workspace/.gitignore +2 -0
  161. openrua/sandbox/workspace/README.md +27 -0
  162. openrua/sandbox/workspace/docs/10-machine.md +80 -0
  163. openrua/sandbox/workspace/docs/20-perception.md +109 -0
  164. openrua/sandbox/workspace/docs/30-action.md +135 -0
  165. openrua/sandbox/workspace/docs/40-patterns.md +56 -0
  166. openrua/sandbox/workspace/tools/README.md +10 -0
  167. openrua/sandbox/workspace/tools/action/base_goto.py +114 -0
  168. openrua/sandbox/workspace/tools/action/fjt_send.py +57 -0
  169. openrua/sandbox/workspace/tools/action/gripper_cmd.py +47 -0
  170. openrua/sandbox/workspace/tools/action/ik_move.py +112 -0
  171. openrua/sandbox/workspace/tools/perception/cam_snap.py +46 -0
  172. openrua/sandbox/workspace/tools/perception/px2world.py +76 -0
  173. openrua/sandbox/workspace.py +216 -0
  174. openrua/testing.py +98 -0
  175. openrua-0.0.7.dist-info/METADATA +320 -0
  176. openrua-0.0.7.dist-info/RECORD +179 -0
  177. openrua-0.0.7.dist-info/WHEEL +4 -0
  178. openrua-0.0.7.dist-info/entry_points.txt +3 -0
  179. 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,7 @@
1
+ """``python -m openrua`` is the same command line as ``openrua``."""
2
+
3
+ import sys
4
+
5
+ from openrua.cli import main
6
+
7
+ sys.exit(main())
@@ -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."