openshell-agent-runner 0.0.1__tar.gz → 0.0.2__tar.gz

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 (29) hide show
  1. openshell_agent_runner-0.0.2/PKG-INFO +164 -0
  2. openshell_agent_runner-0.0.2/README.md +141 -0
  3. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/pyproject.toml +2 -1
  4. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/config.py +70 -22
  5. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/resources.py +22 -2
  6. openshell_agent_runner-0.0.2/src/openshell_agent_runner/harnesses/pi/runtime/extensions/validate-tools.ts +42 -0
  7. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/openshell.py +34 -4
  8. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/runner.py +21 -8
  9. openshell_agent_runner-0.0.1/PKG-INFO +0 -404
  10. openshell_agent_runner-0.0.1/README.md +0 -381
  11. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/.gitignore +0 -0
  12. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/LICENSE +0 -0
  13. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/__init__.py +0 -0
  14. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/artifacts.py +0 -0
  15. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/cli.py +0 -0
  16. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/errors.py +0 -0
  17. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/__init__.py +0 -0
  18. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/__init__.py +0 -0
  19. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/runtime/extensions/submit-result.ts +0 -0
  20. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/runtime/image/Dockerfile +0 -0
  21. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/runtime/image/exec.sh +0 -0
  22. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/resources.py +0 -0
  23. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profile_init.py +0 -0
  24. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/__init__.py +0 -0
  25. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/models.json +0 -0
  26. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/policy.yaml +0 -0
  27. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/profile.yaml +0 -0
  28. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/prompt.md +0 -0
  29. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/settings.json +0 -0
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.5
2
+ Name: openshell-agent-runner
3
+ Version: 0.0.2
4
+ Summary: Launch ephemeral agents for single tasks in OpenShell sandboxes.
5
+ Project-URL: Repository, https://github.com/NVIDIA/OpenShell-Research
6
+ Project-URL: Documentation, https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/
7
+ Author: NVIDIA CORPORATION & AFFILIATES
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: jsonschema<5,>=4.25
19
+ Requires-Dist: pydantic<3,>=2.11
20
+ Requires-Dist: pyyaml<7,>=6
21
+ Requires-Dist: typer<1,>=0.16
22
+ Description-Content-Type: text/markdown
23
+
24
+ # OpenShell Agent Runner
25
+
26
+ OpenShell Agent Runner (OAR) launches one ephemeral agent for one configured
27
+ task. Each `oar run` creates an isolated OpenShell sandbox, runs Pi with the
28
+ selected profile, publishes one result, and removes the sandbox. This bounded
29
+ lifecycle works well in CI jobs and other automated workflows.
30
+
31
+ OAR uses an existing OpenShell gateway, workspace, and inference route. It does
32
+ not create or change providers, credentials, gateways, workspaces, or routes.
33
+
34
+ ## Requirements
35
+
36
+ - [`uv`](https://docs.astral.sh/uv/)
37
+ - OpenShell 0.0.111 or newer
38
+ - A running OpenShell gateway
39
+ - An inference route and its model ID
40
+
41
+ ## Quick start
42
+
43
+ Create the profiles packaged with OAR. `MODEL_ID` is an ordinary shell variable;
44
+ replace its value with the model ID configured on your inference route.
45
+
46
+ ```bash
47
+ export MODEL_ID="provider/model"
48
+
49
+ uvx --from openshell-agent-runner oar init ./profiles \
50
+ --model "$MODEL_ID"
51
+ uvx --from openshell-agent-runner oar doctor --gateway openshell
52
+ ```
53
+
54
+ Validate the included reviewer profile and preview its task:
55
+
56
+ ```bash
57
+ printf '# Review me\n\nA short document.\n' > document.md
58
+ uvx --from openshell-agent-runner oar validate ./profiles/reviewer
59
+
60
+ uvx --from openshell-agent-runner oar run ./profiles/reviewer \
61
+ --task review \
62
+ --gateway openshell \
63
+ --input document.md \
64
+ --output /tmp/oar-review.md \
65
+ --dry-run
66
+ ```
67
+
68
+ Replace `openshell` with your gateway name. Remove `--dry-run` to launch the
69
+ agent and write its result to `/tmp/oar-review.md`.
70
+
71
+ `oar init` copies the packaged profiles into an ordinary directory so you can
72
+ inspect, edit, and commit them. Omit `--profile` to create all packaged profiles,
73
+ or repeat `--profile NAME` to select a subset.
74
+
75
+ ## Profiles
76
+
77
+ A profile contains `profile.yaml`, Pi's `models.json` and `settings.json`, an
78
+ OpenShell policy, and the prompts or other files referenced by its tasks. The
79
+ profile owns stable behavior and permissions; the CLI supplies values that vary
80
+ for each run, such as the task, inputs, output path, gateway, and workspace.
81
+
82
+ ```yaml
83
+ id: reviewer
84
+ description: Review an uploaded document.
85
+
86
+ sandbox:
87
+ policy: policy.yaml
88
+ upload: []
89
+ env: []
90
+
91
+ tasks:
92
+ review:
93
+ required_input: document
94
+ prompt: prompt.md
95
+ tools: [read, grep, find, ls, bash]
96
+ skills: []
97
+ extensions: []
98
+ ```
99
+
100
+ `tools` is a strict allowlist. OAR recognizes Pi's built-in `bash`, `edit`,
101
+ `find`, `grep`, `ls`, `read`, and `write` tools. Custom tools must be declared by
102
+ an extension used by the same task:
103
+
104
+ ```yaml
105
+ tasks:
106
+ check:
107
+ prompt: prompts/check.md
108
+ tools: [read, custom_check]
109
+ extensions:
110
+ - path: extensions/custom-check.ts
111
+ tools: [custom_check]
112
+ ```
113
+
114
+ `oar validate` rejects unknown fields, missing or escaping resources, invalid
115
+ schemas, and tools that are not built in or declared by a referenced extension.
116
+ The runtime also verifies that Pi actually registered every selected tool before
117
+ the first model request.
118
+
119
+ Add `output_schema` to a task when its result must be JSON. OAR exposes the
120
+ built-in Pi `submit_result` extension for that task, lets Pi correct invalid
121
+ submissions during the session, and validates the downloaded result against the
122
+ same Draft 2020-12 schema before publishing it.
123
+
124
+ ## Commands
125
+
126
+ ```text
127
+ oar init PROFILE_ROOT --model MODEL_ID [OPTIONS]
128
+ oar validate PROFILE_DIRECTORY
129
+ oar run PROFILE_DIRECTORY --task TASK --output PATH [OPTIONS]
130
+ oar doctor [OPTIONS]
131
+ ```
132
+
133
+ - `init` creates editable copies of profiles packaged with OAR.
134
+ - `validate` checks a profile and all of its local resources without running it.
135
+ - `doctor` performs read-only OpenShell gateway and inference checks.
136
+ - `run` launches a task, or prints its resolved operations with `--dry-run`.
137
+
138
+ Run `oar COMMAND --help` for command options. For task-specific help, select the
139
+ profile and task before `--help`:
140
+
141
+ ```bash
142
+ uvx --from openshell-agent-runner oar run \
143
+ ./profiles/reviewer --task review --help
144
+ ```
145
+
146
+ ## Documentation
147
+
148
+ The [OAR guide](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/)
149
+ explains profile inputs, tools and extensions, uploads, the run lifecycle,
150
+ structured results, security boundaries, and exit codes.
151
+
152
+ ## Development
153
+
154
+ From `projects/openshell-agent-runner`:
155
+
156
+ ```bash
157
+ make check
158
+ make build
159
+ ```
160
+
161
+ Run a focused test with `make test PYTEST_ARGS="tests/test_config.py"`. Use
162
+ `make clean` to remove generated build and cache files. See
163
+ [RELEASING.md](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/openshell-agent-runner/RELEASING.md)
164
+ for the local PyPI release process.
@@ -0,0 +1,141 @@
1
+ # OpenShell Agent Runner
2
+
3
+ OpenShell Agent Runner (OAR) launches one ephemeral agent for one configured
4
+ task. Each `oar run` creates an isolated OpenShell sandbox, runs Pi with the
5
+ selected profile, publishes one result, and removes the sandbox. This bounded
6
+ lifecycle works well in CI jobs and other automated workflows.
7
+
8
+ OAR uses an existing OpenShell gateway, workspace, and inference route. It does
9
+ not create or change providers, credentials, gateways, workspaces, or routes.
10
+
11
+ ## Requirements
12
+
13
+ - [`uv`](https://docs.astral.sh/uv/)
14
+ - OpenShell 0.0.111 or newer
15
+ - A running OpenShell gateway
16
+ - An inference route and its model ID
17
+
18
+ ## Quick start
19
+
20
+ Create the profiles packaged with OAR. `MODEL_ID` is an ordinary shell variable;
21
+ replace its value with the model ID configured on your inference route.
22
+
23
+ ```bash
24
+ export MODEL_ID="provider/model"
25
+
26
+ uvx --from openshell-agent-runner oar init ./profiles \
27
+ --model "$MODEL_ID"
28
+ uvx --from openshell-agent-runner oar doctor --gateway openshell
29
+ ```
30
+
31
+ Validate the included reviewer profile and preview its task:
32
+
33
+ ```bash
34
+ printf '# Review me\n\nA short document.\n' > document.md
35
+ uvx --from openshell-agent-runner oar validate ./profiles/reviewer
36
+
37
+ uvx --from openshell-agent-runner oar run ./profiles/reviewer \
38
+ --task review \
39
+ --gateway openshell \
40
+ --input document.md \
41
+ --output /tmp/oar-review.md \
42
+ --dry-run
43
+ ```
44
+
45
+ Replace `openshell` with your gateway name. Remove `--dry-run` to launch the
46
+ agent and write its result to `/tmp/oar-review.md`.
47
+
48
+ `oar init` copies the packaged profiles into an ordinary directory so you can
49
+ inspect, edit, and commit them. Omit `--profile` to create all packaged profiles,
50
+ or repeat `--profile NAME` to select a subset.
51
+
52
+ ## Profiles
53
+
54
+ A profile contains `profile.yaml`, Pi's `models.json` and `settings.json`, an
55
+ OpenShell policy, and the prompts or other files referenced by its tasks. The
56
+ profile owns stable behavior and permissions; the CLI supplies values that vary
57
+ for each run, such as the task, inputs, output path, gateway, and workspace.
58
+
59
+ ```yaml
60
+ id: reviewer
61
+ description: Review an uploaded document.
62
+
63
+ sandbox:
64
+ policy: policy.yaml
65
+ upload: []
66
+ env: []
67
+
68
+ tasks:
69
+ review:
70
+ required_input: document
71
+ prompt: prompt.md
72
+ tools: [read, grep, find, ls, bash]
73
+ skills: []
74
+ extensions: []
75
+ ```
76
+
77
+ `tools` is a strict allowlist. OAR recognizes Pi's built-in `bash`, `edit`,
78
+ `find`, `grep`, `ls`, `read`, and `write` tools. Custom tools must be declared by
79
+ an extension used by the same task:
80
+
81
+ ```yaml
82
+ tasks:
83
+ check:
84
+ prompt: prompts/check.md
85
+ tools: [read, custom_check]
86
+ extensions:
87
+ - path: extensions/custom-check.ts
88
+ tools: [custom_check]
89
+ ```
90
+
91
+ `oar validate` rejects unknown fields, missing or escaping resources, invalid
92
+ schemas, and tools that are not built in or declared by a referenced extension.
93
+ The runtime also verifies that Pi actually registered every selected tool before
94
+ the first model request.
95
+
96
+ Add `output_schema` to a task when its result must be JSON. OAR exposes the
97
+ built-in Pi `submit_result` extension for that task, lets Pi correct invalid
98
+ submissions during the session, and validates the downloaded result against the
99
+ same Draft 2020-12 schema before publishing it.
100
+
101
+ ## Commands
102
+
103
+ ```text
104
+ oar init PROFILE_ROOT --model MODEL_ID [OPTIONS]
105
+ oar validate PROFILE_DIRECTORY
106
+ oar run PROFILE_DIRECTORY --task TASK --output PATH [OPTIONS]
107
+ oar doctor [OPTIONS]
108
+ ```
109
+
110
+ - `init` creates editable copies of profiles packaged with OAR.
111
+ - `validate` checks a profile and all of its local resources without running it.
112
+ - `doctor` performs read-only OpenShell gateway and inference checks.
113
+ - `run` launches a task, or prints its resolved operations with `--dry-run`.
114
+
115
+ Run `oar COMMAND --help` for command options. For task-specific help, select the
116
+ profile and task before `--help`:
117
+
118
+ ```bash
119
+ uvx --from openshell-agent-runner oar run \
120
+ ./profiles/reviewer --task review --help
121
+ ```
122
+
123
+ ## Documentation
124
+
125
+ The [OAR guide](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/)
126
+ explains profile inputs, tools and extensions, uploads, the run lifecycle,
127
+ structured results, security boundaries, and exit codes.
128
+
129
+ ## Development
130
+
131
+ From `projects/openshell-agent-runner`:
132
+
133
+ ```bash
134
+ make check
135
+ make build
136
+ ```
137
+
138
+ Run a focused test with `make test PYTEST_ARGS="tests/test_config.py"`. Use
139
+ `make clean` to remove generated build and cache files. See
140
+ [RELEASING.md](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/openshell-agent-runner/RELEASING.md)
141
+ for the local PyPI release process.
@@ -31,7 +31,7 @@ openshell-agent-runner = "openshell_agent_runner.cli:app"
31
31
 
32
32
  [project.urls]
33
33
  Repository = "https://github.com/NVIDIA/OpenShell-Research"
34
- Documentation = "https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/openshell-agent-runner/docs/index.md"
34
+ Documentation = "https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/"
35
35
 
36
36
  [dependency-groups]
37
37
  dev = [
@@ -51,6 +51,7 @@ source = "uv-dynamic-versioning"
51
51
  [tool.uv-dynamic-versioning]
52
52
  vcs = "git"
53
53
  style = "pep440"
54
+ pattern-prefix = "oar-"
54
55
  bump = true
55
56
 
56
57
  [tool.hatch.build.targets.wheel]
@@ -19,6 +19,7 @@ from pydantic import (
19
19
  Field,
20
20
  ValidationError,
21
21
  field_validator,
22
+ model_validator,
22
23
  )
23
24
 
24
25
  from openshell_agent_runner.errors import ConfigurationError
@@ -29,6 +30,8 @@ MODEL_IDENTIFIER_PATTERN = r"^[A-Za-z0-9._:/-]{1,256}$"
29
30
  MODELS_FILENAME = "models.json"
30
31
  PROFILE_FILENAME = "profile.yaml"
31
32
  SETTINGS_FILENAME = "settings.json"
33
+ BUILTIN_PI_TOOLS = frozenset({"bash", "edit", "find", "grep", "ls", "read", "write"})
34
+ SUBMIT_RESULT_TOOL = "submit_result"
32
35
  _PI_RUNTIME_SETTING_KEYS = {
33
36
  "defaultProvider",
34
37
  "defaultModel",
@@ -58,24 +61,72 @@ class SandboxConfig(StrictModel):
58
61
  return values
59
62
 
60
63
 
64
+ ToolName = Annotated[str, Field(pattern=RESOURCE_IDENTIFIER_PATTERN)]
65
+
66
+
67
+ class ExtensionConfig(StrictModel):
68
+ path: Path
69
+ tools: list[ToolName] = Field(default_factory=list)
70
+
71
+ @field_validator("tools")
72
+ @classmethod
73
+ def require_unique_tools(cls, values: list[str]) -> list[str]:
74
+ if len(values) != len(set(values)):
75
+ raise ValueError("extension tool entries must be unique")
76
+ return values
77
+
78
+
61
79
  class TaskConfig(StrictModel):
62
80
  description: str | None = Field(default=None, min_length=1, max_length=1000)
63
81
  required_input: Literal["document"] | None = None
64
82
  prompt: Path
65
83
  output_schema: Path | None = None
66
- tools: list[Annotated[str, Field(pattern=RESOURCE_IDENTIFIER_PATTERN)]] = Field(
67
- default_factory=list
68
- )
84
+ tools: list[ToolName] = Field(default_factory=list)
69
85
  skills: list[Path] = Field(default_factory=list)
70
- extensions: list[Path] = Field(default_factory=list)
86
+ extensions: list[ExtensionConfig] = Field(default_factory=list)
71
87
 
72
- @field_validator("tools", "skills", "extensions")
88
+ @field_validator("tools", "skills")
73
89
  @classmethod
74
90
  def require_unique_resources(cls, values: list[object]) -> list[object]:
75
91
  if len(values) != len(set(values)):
76
92
  raise ValueError("resource entries must be unique")
77
93
  return values
78
94
 
95
+ @field_validator("extensions")
96
+ @classmethod
97
+ def require_unique_extensions(
98
+ cls, values: list[ExtensionConfig]
99
+ ) -> list[ExtensionConfig]:
100
+ paths = [extension.path for extension in values]
101
+ if len(paths) != len(set(paths)):
102
+ raise ValueError("extension paths must be unique")
103
+ return values
104
+
105
+ @model_validator(mode="after")
106
+ def require_known_tools(self) -> TaskConfig:
107
+ declared_custom_tools: set[str] = set()
108
+ for extension in self.extensions:
109
+ for tool in extension.tools:
110
+ if tool in BUILTIN_PI_TOOLS or tool == SUBMIT_RESULT_TOOL:
111
+ raise ValueError(
112
+ f"extension tool {tool!r} conflicts with a reserved tool"
113
+ )
114
+ if tool in declared_custom_tools:
115
+ raise ValueError(
116
+ f"custom tool {tool!r} is declared by multiple extensions"
117
+ )
118
+ declared_custom_tools.add(tool)
119
+
120
+ available_tools = BUILTIN_PI_TOOLS | declared_custom_tools
121
+ unknown_tools = sorted(set(self.tools) - available_tools)
122
+ if unknown_tools:
123
+ raise ValueError(
124
+ f"unknown tools {unknown_tools}; Pi built-ins are "
125
+ f"{sorted(BUILTIN_PI_TOOLS)}; declare each custom tool under a "
126
+ "referenced extension"
127
+ )
128
+ return self
129
+
79
130
 
80
131
  class ProfileConfig(StrictModel):
81
132
  id: Annotated[str, Field(pattern=IDENTIFIER_PATTERN)]
@@ -155,7 +206,6 @@ def resolve_task(profile_directory: Path, task_id: str) -> ResolvedProfile:
155
206
  def validate_upload_mappings(values: Sequence[str]) -> tuple[str, ...]:
156
207
  if len(values) != len(set(values)):
157
208
  raise ValueError("duplicate upload mapping")
158
- destinations: dict[str, str] = {}
159
209
  for value in values:
160
210
  source, separator, destination = value.rpartition(":")
161
211
  if not separator or not source or not destination.startswith("/"):
@@ -169,16 +219,11 @@ def validate_upload_mappings(values: Sequence[str]) -> tuple[str, ...]:
169
219
  PurePosixPath("/sandbox/artifacts"),
170
220
  PurePosixPath("/sandbox/oar-runtime"),
171
221
  ):
172
- if path == reserved or path.is_relative_to(reserved):
222
+ if path.is_relative_to(reserved) or reserved.is_relative_to(path):
173
223
  raise ValueError(
174
224
  "upload destination is reserved for runner resources: "
175
225
  f"{destination}"
176
226
  )
177
- normalized = str(path)
178
- previous = destinations.get(normalized)
179
- if previous is not None and previous != source:
180
- raise ValueError(f"conflicting upload destination: {destination}")
181
- destinations[normalized] = source
182
227
  return tuple(values)
183
228
 
184
229
 
@@ -326,7 +371,11 @@ def _validate_profile_resources(resolved: ResolvedProfile) -> None:
326
371
  f"skill for task {task_id} contains a symlink: {descendant}"
327
372
  )
328
373
  for extension in task.extensions:
329
- _inside(directory, directory / extension, f"extension for task {task_id}")
374
+ _inside(
375
+ directory,
376
+ directory / extension.path,
377
+ f"extension for task {task_id}",
378
+ )
330
379
 
331
380
 
332
381
  def _validate_output_schema(path: Path) -> None:
@@ -335,10 +384,10 @@ def _validate_output_schema(path: Path) -> None:
335
384
  Draft202012Validator.check_schema(document)
336
385
  except (OSError, UnicodeError, json.JSONDecodeError, SchemaError) as error:
337
386
  raise ConfigurationError(f"invalid output schema {path}: {error}") from error
338
- _validate_schema_references(document, path)
387
+ _validate_schema_references(document)
339
388
 
340
389
 
341
- def _validate_schema_references(document: Any, path: Path) -> None:
390
+ def _validate_schema_references(document: Any) -> None:
342
391
  if not isinstance(document, dict):
343
392
  return
344
393
 
@@ -349,23 +398,22 @@ def _validate_schema_references(document: Any, path: Path) -> None:
349
398
  f"({key}) because host and sandbox engines use different dialects"
350
399
  )
351
400
  for key in {"$ref", "$dynamicRef", "$recursiveRef"}:
352
- if key in document and (
353
- not isinstance(document[key], str) or not document[key].startswith("#")
354
- ):
401
+ if key in document:
355
402
  raise ConfigurationError(
356
- f"output schema references must stay inside {path}: {document[key]!r}"
403
+ "output schemas do not support reference keywords "
404
+ f"({key}) because the submission tool nests the schema"
357
405
  )
358
406
 
359
407
  for key in {"$defs", "definitions", "properties", "dependentSchemas"}:
360
408
  value = document.get(key)
361
409
  if isinstance(value, dict):
362
410
  for schema in value.values():
363
- _validate_schema_references(schema, path)
411
+ _validate_schema_references(schema)
364
412
  for key in {"allOf", "anyOf", "oneOf", "prefixItems"}:
365
413
  value = document.get(key)
366
414
  if isinstance(value, list):
367
415
  for schema in value:
368
- _validate_schema_references(schema, path)
416
+ _validate_schema_references(schema)
369
417
  for key in {
370
418
  "additionalProperties",
371
419
  "contains",
@@ -379,4 +427,4 @@ def _validate_schema_references(document: Any, path: Path) -> None:
379
427
  "unevaluatedItems",
380
428
  "unevaluatedProperties",
381
429
  }:
382
- _validate_schema_references(document.get(key), path)
430
+ _validate_schema_references(document.get(key))
@@ -3,6 +3,7 @@
3
3
 
4
4
  """Materialize the explicit native-upload runtime bundle for Pi."""
5
5
 
6
+ import json
6
7
  import shutil
7
8
  import tempfile
8
9
  from importlib.resources import files
@@ -66,15 +67,34 @@ def prepare_resources(resolved: ResolvedProfile, task_id: str) -> PreparedResour
66
67
  shutil.copytree(resolved.profile_dir / skill, target)
67
68
  arguments.extend(["--skill", f"{SANDBOX_RUNTIME_ROOT}/skills/{target.name}"])
68
69
  for index, extension in enumerate(task.extensions):
69
- target = runtime / "extensions" / f"{index:02d}-{extension.name}"
70
- shutil.copy2(resolved.profile_dir / extension, target)
70
+ target = runtime / "extensions" / f"{index:02d}-{extension.path.name}"
71
+ shutil.copy2(resolved.profile_dir / extension.path, target)
71
72
  arguments.extend(
72
73
  ["--extension", f"{SANDBOX_RUNTIME_ROOT}/extensions/{target.name}"]
73
74
  )
75
+ expected_tools = runtime / "tools.json"
76
+ expected_tools.write_text(f"{json.dumps(tools)}\n", encoding="utf-8")
77
+ validate_tools = Path(
78
+ str(
79
+ files("openshell_agent_runner.harnesses.pi")
80
+ / "runtime"
81
+ / "extensions"
82
+ / "validate-tools.ts"
83
+ )
84
+ )
85
+ validator_target = runtime / "extensions" / "oar-validate-tools.ts"
86
+ shutil.copy2(validate_tools, validator_target)
87
+ arguments.extend(
88
+ [
89
+ "--extension",
90
+ f"{SANDBOX_RUNTIME_ROOT}/extensions/{validator_target.name}",
91
+ ]
92
+ )
74
93
  uploads = [
75
94
  f"{runtime / 'prompt.md'}:{SANDBOX_RUNTIME_ROOT}/prompt.md",
76
95
  f"{runtime / 'models.json'}:{SANDBOX_RUNTIME_ROOT}/models.json",
77
96
  f"{runtime / 'settings.json'}:{SANDBOX_RUNTIME_ROOT}/settings.json",
97
+ f"{expected_tools}:{SANDBOX_RUNTIME_ROOT}/tools.json",
78
98
  ]
79
99
  if task.output_schema is not None:
80
100
  uploads.append(
@@ -0,0 +1,42 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { readFileSync } from "node:fs";
5
+
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+
8
+ const runtimeRoot = process.env.OAR_RUNTIME_ROOT || "/sandbox/oar-runtime";
9
+ const requestedTools = JSON.parse(
10
+ readFileSync(`${runtimeRoot}/tools.json`, "utf8"),
11
+ ) as string[];
12
+
13
+ export function findMissingTools(
14
+ requested: string[],
15
+ available: Array<{ name: string }>,
16
+ ): string[] {
17
+ const availableNames = new Set(available.map((tool) => tool.name));
18
+ return requested.filter((name) => !availableNames.has(name));
19
+ }
20
+
21
+ export default function (pi: ExtensionAPI) {
22
+ pi.on("before_agent_start", (_event, context) => {
23
+ const availableTools = context.getAllTools();
24
+ const missingTools = findMissingTools(requestedTools, availableTools);
25
+ const activeTools = context.getActiveTools();
26
+ const activeNames = new Set(activeTools);
27
+ const inactiveTools = requestedTools.filter((name) => !activeNames.has(name));
28
+ const unavailableTools = [...new Set([...missingTools, ...inactiveTools])];
29
+ if (unavailableTools.length === 0) return;
30
+
31
+ const availableNames = availableTools
32
+ .map((tool) => tool.name)
33
+ .sort()
34
+ .join(", ");
35
+ process.stderr.write(
36
+ `OAR tool validation failed: unavailable tools: ${unavailableTools.join(", ")}. ` +
37
+ `Registered tools: ${availableNames || "none"}. ` +
38
+ `Active tools: ${activeTools.sort().join(", ") || "none"}.\n`,
39
+ );
40
+ process.exit(2);
41
+ });
42
+ }
@@ -43,18 +43,48 @@ class NativeTarget:
43
43
 
44
44
  def sandbox_create(
45
45
  resolved: ResolvedRun,
46
- resources: PreparedResources,
47
46
  name: str,
48
47
  token: str,
49
48
  ) -> list[str]:
50
49
  command = [*resolved.create_command, "--name", name]
51
- for upload in resources.uploads:
52
- command.extend(["--upload", upload])
53
50
  command.extend(["--label", f"{RESERVED_LABEL}={token}"])
54
- command.extend(["--", "bash", "/opt/oar/pi/exec.sh", *resources.arguments])
51
+ command.extend(["--detach", "--", "sleep", "infinity"])
55
52
  return command
56
53
 
57
54
 
55
+ def sandbox_upload(request: RunRequest, name: str, mapping: str) -> list[str]:
56
+ source, _, destination = mapping.rpartition(":")
57
+ return [
58
+ request.openshell_bin,
59
+ "sandbox",
60
+ "upload",
61
+ name,
62
+ source,
63
+ destination,
64
+ *_native_target_args(request),
65
+ ]
66
+
67
+
68
+ def sandbox_exec(
69
+ resolved: ResolvedRun,
70
+ resources: PreparedResources,
71
+ name: str,
72
+ ) -> list[str]:
73
+ return [
74
+ resolved.request.openshell_bin,
75
+ "sandbox",
76
+ "exec",
77
+ "--name",
78
+ name,
79
+ "--no-tty",
80
+ *_native_target_args(resolved.request),
81
+ "--",
82
+ "bash",
83
+ "/opt/oar/pi/exec.sh",
84
+ *resources.arguments,
85
+ ]
86
+
87
+
58
88
  def sandbox_download(resolved: ResolvedRun, name: str, destination: Path) -> list[str]:
59
89
  return [
60
90
  resolved.request.openshell_bin,