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.
- openshell_agent_runner-0.0.2/PKG-INFO +164 -0
- openshell_agent_runner-0.0.2/README.md +141 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/pyproject.toml +2 -1
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/config.py +70 -22
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/resources.py +22 -2
- openshell_agent_runner-0.0.2/src/openshell_agent_runner/harnesses/pi/runtime/extensions/validate-tools.ts +42 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/openshell.py +34 -4
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/runner.py +21 -8
- openshell_agent_runner-0.0.1/PKG-INFO +0 -404
- openshell_agent_runner-0.0.1/README.md +0 -381
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/.gitignore +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/LICENSE +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/__init__.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/artifacts.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/cli.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/errors.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/__init__.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/__init__.py +0 -0
- {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
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/runtime/image/Dockerfile +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/pi/runtime/image/exec.sh +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/harnesses/resources.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profile_init.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/__init__.py +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/models.json +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/policy.yaml +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/profile.yaml +0 -0
- {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/profiles/reviewer/prompt.md +0 -0
- {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.
|
|
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]
|
{openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.2}/src/openshell_agent_runner/config.py
RENAMED
|
@@ -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[
|
|
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[
|
|
86
|
+
extensions: list[ExtensionConfig] = Field(default_factory=list)
|
|
71
87
|
|
|
72
|
-
@field_validator("tools", "skills"
|
|
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
|
|
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(
|
|
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
|
|
387
|
+
_validate_schema_references(document)
|
|
339
388
|
|
|
340
389
|
|
|
341
|
-
def _validate_schema_references(document: Any
|
|
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
|
|
353
|
-
not isinstance(document[key], str) or not document[key].startswith("#")
|
|
354
|
-
):
|
|
401
|
+
if key in document:
|
|
355
402
|
raise ConfigurationError(
|
|
356
|
-
|
|
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
|
|
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
|
|
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)
|
|
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(["--", "
|
|
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,
|