openshell-agent-runner 0.0.1__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.
@@ -0,0 +1,162 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Build, execute, and inspect native OpenShell commands."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ import resource
10
+ import shlex
11
+ import subprocess
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass
14
+ from functools import partial
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from openshell_agent_runner.artifacts import ARTIFACT_PATH
19
+ from openshell_agent_runner.errors import ExecutionError
20
+
21
+ if TYPE_CHECKING:
22
+ from openshell_agent_runner.harnesses.resources import PreparedResources
23
+ from openshell_agent_runner.runner import ResolvedRun, RunRequest
24
+
25
+ MINIMUM_OPEN_SHELL_VERSION = (0, 0, 111)
26
+ RESERVED_LABEL = "oar-run-id"
27
+ VERSION_PATTERN = re.compile(r"\b(\d+)\.(\d+)\.(\d+)\b")
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class NativeTarget:
32
+ executable: str = "openshell"
33
+ gateway: str | None = None
34
+ workspace: str = "default"
35
+
36
+ def global_args(self) -> list[str]:
37
+ values: list[str] = []
38
+ if self.gateway:
39
+ values.extend(["--gateway", self.gateway])
40
+ values.extend(["--workspace", self.workspace])
41
+ return values
42
+
43
+
44
+ def sandbox_create(
45
+ resolved: ResolvedRun,
46
+ resources: PreparedResources,
47
+ name: str,
48
+ token: str,
49
+ ) -> list[str]:
50
+ command = [*resolved.create_command, "--name", name]
51
+ for upload in resources.uploads:
52
+ command.extend(["--upload", upload])
53
+ command.extend(["--label", f"{RESERVED_LABEL}={token}"])
54
+ command.extend(["--", "bash", "/opt/oar/pi/exec.sh", *resources.arguments])
55
+ return command
56
+
57
+
58
+ def sandbox_download(resolved: ResolvedRun, name: str, destination: Path) -> list[str]:
59
+ return [
60
+ resolved.request.openshell_bin,
61
+ "sandbox",
62
+ "download",
63
+ name,
64
+ ARTIFACT_PATH,
65
+ str(destination),
66
+ *_native_target_args(resolved.request),
67
+ ]
68
+
69
+
70
+ def sandbox_get(request: RunRequest, name: str) -> list[str]:
71
+ return [
72
+ request.openshell_bin,
73
+ "sandbox",
74
+ "get",
75
+ name,
76
+ *_native_target_args(request),
77
+ "--output",
78
+ "json",
79
+ ]
80
+
81
+
82
+ def sandbox_delete(request: RunRequest, name: str) -> list[str]:
83
+ return [
84
+ request.openshell_bin,
85
+ "sandbox",
86
+ "delete",
87
+ name,
88
+ *_native_target_args(request),
89
+ ]
90
+
91
+
92
+ def run(
93
+ command: list[str],
94
+ timeout: int,
95
+ *,
96
+ capture: bool = False,
97
+ max_file_bytes: int | None = None,
98
+ ) -> subprocess.CompletedProcess[str]:
99
+ try:
100
+ return subprocess.run(
101
+ command,
102
+ check=True,
103
+ text=True,
104
+ capture_output=capture,
105
+ timeout=timeout,
106
+ preexec_fn=(
107
+ partial(_set_file_size_limit, max_file_bytes)
108
+ if max_file_bytes is not None
109
+ else None
110
+ ),
111
+ )
112
+ except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
113
+ raise ExecutionError(
114
+ f"command failed: {shlex.join(command)}: {error}"
115
+ ) from error
116
+
117
+
118
+ def _set_file_size_limit(max_file_bytes: int) -> None:
119
+ resource.setrlimit(resource.RLIMIT_FSIZE, (max_file_bytes, max_file_bytes))
120
+
121
+
122
+ def doctor(target: NativeTarget) -> list[tuple[str, str]]:
123
+ checks = []
124
+ for name, arguments in (
125
+ ("version", ["--version"]),
126
+ ("status", ["status"]),
127
+ ("inference", ["inference", "get"]),
128
+ ):
129
+ completed = _run_read_only(target, arguments)
130
+ result = completed.stdout.strip()
131
+ if name == "version":
132
+ match = VERSION_PATTERN.search(result)
133
+ if match is None:
134
+ raise ExecutionError(f"cannot parse OpenShell version: {result!r}")
135
+ version = tuple(int(part) for part in match.groups())
136
+ if version < MINIMUM_OPEN_SHELL_VERSION:
137
+ minimum = ".".join(str(part) for part in MINIMUM_OPEN_SHELL_VERSION)
138
+ raise ExecutionError(
139
+ f"OpenShell {minimum} or newer is required; found {match.group(0)}"
140
+ )
141
+ checks.append((name, result))
142
+ return checks
143
+
144
+
145
+ def _native_target_args(request: RunRequest) -> list[str]:
146
+ result: list[str] = []
147
+ if request.gateway:
148
+ result.extend(["--gateway", request.gateway])
149
+ result.extend(["--workspace", request.workspace])
150
+ return result
151
+
152
+
153
+ def _run_read_only(
154
+ target: NativeTarget, arguments: Sequence[str]
155
+ ) -> subprocess.CompletedProcess[str]:
156
+ command = [target.executable, *arguments, *target.global_args()]
157
+ try:
158
+ return subprocess.run(command, check=True, text=True, capture_output=True)
159
+ except (OSError, subprocess.CalledProcessError) as error:
160
+ raise ExecutionError(
161
+ f"OpenShell check failed: {shlex.join(command)}: {error}"
162
+ ) from error
@@ -0,0 +1,104 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Create editable profiles from resources packaged with OAR."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import shutil
11
+ import tempfile
12
+ from collections.abc import Sequence
13
+ from enum import StrEnum
14
+ from importlib.resources import as_file, files
15
+ from pathlib import Path
16
+
17
+ from openshell_agent_runner.config import MODEL_IDENTIFIER_PATTERN, load_profile
18
+ from openshell_agent_runner.errors import ConfigurationError
19
+
20
+ PACKAGED_PROFILES = ("reviewer",)
21
+
22
+
23
+ class ThinkingLevel(StrEnum):
24
+ OFF = "off"
25
+ MINIMAL = "minimal"
26
+ LOW = "low"
27
+ MEDIUM = "medium"
28
+ HIGH = "high"
29
+ XHIGH = "xhigh"
30
+ MAX = "max"
31
+
32
+
33
+ def initialize_profiles(
34
+ destination: Path,
35
+ profile_names: Sequence[str],
36
+ model_id: str,
37
+ thinking: ThinkingLevel,
38
+ ) -> tuple[Path, ...]:
39
+ """Create selected packaged profiles under destination."""
40
+ if not re.fullmatch(MODEL_IDENTIFIER_PATTERN, model_id):
41
+ raise ConfigurationError("--model must be a valid model identifier")
42
+
43
+ selected = tuple(profile_names) or PACKAGED_PROFILES
44
+ if len(selected) != len(set(selected)):
45
+ raise ConfigurationError("--profile values must be unique")
46
+ unknown = sorted(set(selected) - set(PACKAGED_PROFILES))
47
+ if unknown:
48
+ available = ", ".join(PACKAGED_PROFILES)
49
+ raise ConfigurationError(
50
+ f"unknown packaged profile {unknown[0]!r}; available profiles: {available}"
51
+ )
52
+
53
+ try:
54
+ destination.mkdir(parents=True, exist_ok=True)
55
+ except OSError as error:
56
+ raise ConfigurationError(
57
+ f"cannot create profile directory {destination}: {error}"
58
+ ) from error
59
+ if not destination.is_dir():
60
+ raise ConfigurationError(
61
+ f"profile destination is not a directory: {destination}"
62
+ )
63
+
64
+ targets = tuple(destination / name for name in selected)
65
+ collisions = [path for path in targets if path.exists() or path.is_symlink()]
66
+ if collisions:
67
+ raise ConfigurationError(f"profile destination already exists: {collisions[0]}")
68
+
69
+ try:
70
+ with tempfile.TemporaryDirectory(
71
+ prefix=".oar-init-", dir=destination
72
+ ) as staging:
73
+ staging_root = Path(staging)
74
+ with as_file(files("openshell_agent_runner.profiles")) as source_root:
75
+ staged_profiles = []
76
+ for name in selected:
77
+ staged = staging_root / name
78
+ shutil.copytree(source_root / name, staged)
79
+ _configure_runtime(staged, model_id, thinking)
80
+ load_profile(staged)
81
+ staged_profiles.append(staged)
82
+ for staged, target in zip(staged_profiles, targets, strict=True):
83
+ staged.rename(target)
84
+ except OSError as error:
85
+ raise ConfigurationError(f"cannot initialize profiles: {error}") from error
86
+
87
+ return targets
88
+
89
+
90
+ def _configure_runtime(
91
+ profile_directory: Path, model_id: str, thinking: ThinkingLevel
92
+ ) -> None:
93
+ models_path = profile_directory / "models.json"
94
+ models = json.loads(models_path.read_text(encoding="utf-8"))
95
+ model = models["providers"]["openshell"]["models"][0]
96
+ model["id"] = model_id
97
+ model["reasoning"] = thinking is not ThinkingLevel.OFF
98
+ models_path.write_text(json.dumps(models, indent=2) + "\n", encoding="utf-8")
99
+
100
+ settings_path = profile_directory / "settings.json"
101
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
102
+ settings["defaultModel"] = model_id
103
+ settings["defaultThinkingLevel"] = thinking.value
104
+ settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Profiles packaged with OpenShell Agent Runner."""
@@ -0,0 +1,19 @@
1
+ {
2
+ "providers": {
3
+ "openshell": {
4
+ "baseUrl": "https://inference.local/v1",
5
+ "api": "openai-completions",
6
+ "apiKey": "unused",
7
+ "authHeader": true,
8
+ "compat": {
9
+ "supportsDeveloperRole": false
10
+ },
11
+ "models": [
12
+ {
13
+ "id": "MODEL_ID",
14
+ "reasoning": true
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ version: 1
2
+
3
+ filesystem_policy:
4
+ include_workdir: false
5
+ read_only: [/usr, /lib, /proc, /dev/urandom, /etc, /opt/oar]
6
+ read_write: [/workspace, /sandbox, /tmp, /dev/null]
7
+
8
+ landlock:
9
+ compatibility: hard_requirement
10
+
11
+ process:
12
+ run_as_user: "1000"
13
+ run_as_group: "1000"
14
+
15
+ network_policies: {}
@@ -0,0 +1,12 @@
1
+ id: reviewer
2
+ description: Review a required input document and publish the result.
3
+
4
+ sandbox:
5
+ policy: policy.yaml
6
+
7
+ tasks:
8
+ review:
9
+ description: Review an input document and return a useful written result.
10
+ required_input: document
11
+ prompt: prompt.md
12
+ tools: [read, grep, find, ls, bash]
@@ -0,0 +1,5 @@
1
+ # Review the input document
2
+
3
+ Act as a coding agent. Inspect `/workspace/input/document.md`, using the declared
4
+ tools as needed. Return a concise Markdown review that identifies the document's
5
+ strengths and the most useful improvements to its clarity and completeness.
@@ -0,0 +1,5 @@
1
+ {
2
+ "defaultProvider": "openshell",
3
+ "defaultModel": "MODEL_ID",
4
+ "defaultThinkingLevel": "high"
5
+ }
@@ -0,0 +1,277 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Resolve and run one configured task in an OpenShell sandbox."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import secrets
10
+ import shlex
11
+ import sys
12
+ import tempfile
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ import openshell_agent_runner.openshell as openshell
18
+ from openshell_agent_runner.artifacts import (
19
+ MAX_ARTIFACT_BYTES,
20
+ atomic_publish,
21
+ validate_artifact,
22
+ )
23
+ from openshell_agent_runner.config import (
24
+ ResolvedProfile,
25
+ resolve_task,
26
+ validate_environment_assignments,
27
+ validate_upload_mappings,
28
+ )
29
+ from openshell_agent_runner.errors import ConfigurationError, ExecutionError
30
+ from openshell_agent_runner.harnesses.pi.resources import (
31
+ image_directory,
32
+ prepare_resources,
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class RunRequest:
38
+ profile_directory: Path
39
+ task_id: str
40
+ output: Path
41
+ input_document: Path | None = None
42
+ uploads: Sequence[str] = ()
43
+ environments: Sequence[str] = ()
44
+ gateway: str | None = None
45
+ workspace: str = "default"
46
+ timeout_seconds: int = 1200
47
+ keep_sandbox: bool = False
48
+ openshell_bin: str = "openshell"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ResolvedRun:
53
+ request: RunRequest
54
+ profile: ResolvedProfile
55
+ uploads: tuple[str, ...]
56
+ environments: tuple[str, ...]
57
+ create_command: tuple[str, ...]
58
+
59
+
60
+ def resolve_run(request: RunRequest) -> ResolvedRun:
61
+ profile = resolve_task(request.profile_directory, request.task_id)
62
+ task = profile.profile.tasks[request.task_id]
63
+ document_upload = _resolve_document_upload(request, task.required_input)
64
+ uploads = _validate_uploads(
65
+ [
66
+ *profile.profile.sandbox.upload,
67
+ *([document_upload] if document_upload else []),
68
+ *request.uploads,
69
+ ]
70
+ )
71
+ environments = _validate_environments(
72
+ [
73
+ *([_DOCUMENT_INPUT_ENVIRONMENT] if document_upload else []),
74
+ *profile.profile.sandbox.env,
75
+ *request.environments,
76
+ ]
77
+ )
78
+ sandbox = profile.profile.sandbox
79
+ command = [request.openshell_bin, "sandbox", "create"]
80
+ if request.gateway:
81
+ command.extend(["--gateway", request.gateway])
82
+ command.extend(
83
+ [
84
+ "--workspace",
85
+ request.workspace,
86
+ "--from",
87
+ str(image_directory()),
88
+ "--policy",
89
+ str(profile.profile_dir / sandbox.policy),
90
+ ]
91
+ )
92
+ for upload in uploads:
93
+ command.extend(["--upload", upload])
94
+ for environment in environments:
95
+ command.extend(["--env", environment])
96
+ command.extend(["--no-auto-providers", "--no-tty", "--approval-mode", "auto"])
97
+ return ResolvedRun(
98
+ request=request,
99
+ profile=profile,
100
+ uploads=uploads,
101
+ environments=environments,
102
+ create_command=tuple(command),
103
+ )
104
+
105
+
106
+ def render_dry_run(request: RunRequest) -> str:
107
+ """Render the exact nominal command sequence without executing subprocesses."""
108
+ resolved = resolve_run(request)
109
+ name, token = _identity()
110
+ resources = prepare_resources(resolved.profile, request.task_id)
111
+ try:
112
+ with tempfile.TemporaryDirectory(prefix="oar-output-") as directory:
113
+ downloaded = Path(directory) / "output.download"
114
+ commands = [
115
+ (
116
+ "create",
117
+ openshell.sandbox_create(resolved, resources, name, token),
118
+ ),
119
+ (
120
+ "download",
121
+ openshell.sandbox_download(resolved, name, downloaded),
122
+ ),
123
+ ]
124
+ if not request.keep_sandbox:
125
+ commands.extend(
126
+ [
127
+ (
128
+ "verify ownership",
129
+ openshell.sandbox_get(request, name),
130
+ ),
131
+ (
132
+ "delete",
133
+ openshell.sandbox_delete(request, name),
134
+ ),
135
+ ]
136
+ )
137
+ lines = [
138
+ "Dry run: no commands were executed.",
139
+ f"Profile: {resolved.profile.profile.id}",
140
+ f"Task: {request.task_id}",
141
+ f"Sandbox: {name}",
142
+ "OpenShell commands:",
143
+ *(f"[{label}] {shlex.join(command)}" for label, command in commands),
144
+ "Host actions:",
145
+ _validation_preview(resolved, downloaded),
146
+ f"[publish] atomically replace {request.output}",
147
+ ]
148
+ if request.keep_sandbox:
149
+ lines.append("[cleanup] skipped because --keep-sandbox is set")
150
+ else:
151
+ lines.append(
152
+ "[cleanup] ownership verification and deletion also run after "
153
+ "failures when the sandbox can be inspected"
154
+ )
155
+ return "\n".join(lines) + "\n"
156
+ finally:
157
+ resources.close()
158
+
159
+
160
+ def run_agent(request: RunRequest) -> str:
161
+ resolved = resolve_run(request)
162
+ name, token = _identity()
163
+ resources = prepare_resources(resolved.profile, request.task_id)
164
+ create = openshell.sandbox_create(resolved, resources, name, token)
165
+ primary_error: BaseException | None = None
166
+ try:
167
+ openshell.run(create, request.timeout_seconds)
168
+ task = resolved.profile.profile.tasks[request.task_id]
169
+ with tempfile.TemporaryDirectory(prefix="oar-output-") as directory:
170
+ downloaded = Path(directory) / "output.download"
171
+ downloaded.touch(mode=0o600)
172
+ openshell.run(
173
+ openshell.sandbox_download(resolved, name, downloaded),
174
+ 120,
175
+ max_file_bytes=MAX_ARTIFACT_BYTES,
176
+ )
177
+ schema_path = (
178
+ resolved.profile.profile_dir / task.output_schema
179
+ if task.output_schema is not None
180
+ else None
181
+ )
182
+ validate_artifact(downloaded, schema_path)
183
+ atomic_publish(downloaded, request.output)
184
+ return name
185
+ except BaseException as error:
186
+ primary_error = error
187
+ raise
188
+ finally:
189
+ resources.close()
190
+ if request.keep_sandbox:
191
+ print(f"oar: sandbox name (--keep-sandbox): {name}", file=sys.stderr)
192
+ else:
193
+ try:
194
+ _verify_ownership(request, name, token)
195
+ openshell.run(openshell.sandbox_delete(request, name), 60)
196
+ except ExecutionError as cleanup_error:
197
+ if primary_error is None:
198
+ raise
199
+ print(
200
+ f"oar: cleanup failed after primary error: {cleanup_error}",
201
+ file=sys.stderr,
202
+ )
203
+
204
+
205
+ def _validate_uploads(values: Sequence[str]) -> tuple[str, ...]:
206
+ try:
207
+ return validate_upload_mappings(values)
208
+ except ValueError as error:
209
+ raise ConfigurationError(str(error)) from error
210
+
211
+
212
+ def _validate_environments(values: Sequence[str]) -> tuple[str, ...]:
213
+ try:
214
+ return validate_environment_assignments(values)
215
+ except ValueError as error:
216
+ raise ConfigurationError(str(error)) from error
217
+
218
+
219
+ def _resolve_document_upload(
220
+ request: RunRequest, required_input: str | None
221
+ ) -> str | None:
222
+ if required_input is None:
223
+ if request.input_document is not None:
224
+ raise ConfigurationError(
225
+ f"task {request.task_id!r} does not accept --input"
226
+ )
227
+ return None
228
+ if request.input_document is None:
229
+ raise ConfigurationError(f"task {request.task_id!r} requires --input DOCUMENT")
230
+ try:
231
+ document = request.input_document.resolve(strict=True)
232
+ except OSError as error:
233
+ raise ConfigurationError(
234
+ f"input document does not exist: {request.input_document}"
235
+ ) from error
236
+ if not document.is_file():
237
+ raise ConfigurationError(f"input document must be a file: {document}")
238
+ return f"{document}:{_DOCUMENT_INPUT_PATH}"
239
+
240
+
241
+ def _validation_preview(resolved: ResolvedRun, downloaded: Path) -> str:
242
+ task = resolved.profile.profile.tasks[resolved.request.task_id]
243
+ if task.output_schema is None:
244
+ return f"[validate] {downloaded} is present, non-empty, and bounded"
245
+ schema = resolved.profile.profile_dir / task.output_schema
246
+ return f"[validate] {downloaded} as JSON against {schema}"
247
+
248
+
249
+ def _identity() -> tuple[str, str]:
250
+ token = secrets.token_hex(8)[:15]
251
+ return f"oar-{token}", token
252
+
253
+
254
+ def _verify_ownership(request: RunRequest, name: str, token: str) -> None:
255
+ command = openshell.sandbox_get(request, name)
256
+ result = openshell.run(command, 30, capture=True)
257
+ try:
258
+ document = json.loads(result.stdout)
259
+ except json.JSONDecodeError as error:
260
+ raise ExecutionError(
261
+ f"cleanup ownership response was invalid for {name}"
262
+ ) from error
263
+ labels = document.get("labels") if isinstance(document, dict) else None
264
+ owned = (
265
+ isinstance(document, dict)
266
+ and document.get("name") == name
267
+ and isinstance(labels, dict)
268
+ and labels.get(openshell.RESERVED_LABEL) == token
269
+ )
270
+ if not owned:
271
+ raise ExecutionError(
272
+ f"refusing to delete sandbox with mismatched ownership: {name}"
273
+ )
274
+
275
+
276
+ _DOCUMENT_INPUT_PATH = "/workspace/input/document.md"
277
+ _DOCUMENT_INPUT_ENVIRONMENT = "REPOSITORY_ROOT=/workspace/input"