openshell-agent-runner 0.0.1__tar.gz → 0.0.3__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 (43) hide show
  1. openshell_agent_runner-0.0.3/PKG-INFO +138 -0
  2. openshell_agent_runner-0.0.3/README.md +115 -0
  3. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/pyproject.toml +2 -1
  4. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/cli.py +46 -8
  5. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/config.py +107 -24
  6. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/pi/resources.py +32 -4
  7. openshell_agent_runner-0.0.3/src/openshell_agent_runner/harnesses/pi/runtime/extensions/validate-tools.ts +42 -0
  8. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/pi/runtime/image/exec.sh +1 -0
  9. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/openshell.py +36 -4
  10. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/profile_init.py +1 -1
  11. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer/profile.yaml +21 -0
  12. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer/prompt-repository.md +15 -0
  13. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer/schemas/review.json +159 -0
  14. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer/skills/review-code/SKILL.md +111 -0
  15. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer/skills/review-code/agents/openai.yaml +4 -0
  16. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/models.json +19 -0
  17. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/policy.yaml +15 -0
  18. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/profile.yaml +21 -0
  19. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/prompt-document.md +15 -0
  20. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/schemas/review.json +160 -0
  21. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/settings.json +5 -0
  22. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/skills/review-technical-writing/SKILL.md +120 -0
  23. openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/technical-writing-reviewer/skills/review-technical-writing/agents/openai.yaml +4 -0
  24. openshell_agent_runner-0.0.3/src/openshell_agent_runner/prompt_templates.py +48 -0
  25. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/runner.py +144 -26
  26. openshell_agent_runner-0.0.1/PKG-INFO +0 -404
  27. openshell_agent_runner-0.0.1/README.md +0 -381
  28. openshell_agent_runner-0.0.1/src/openshell_agent_runner/profiles/reviewer/profile.yaml +0 -12
  29. openshell_agent_runner-0.0.1/src/openshell_agent_runner/profiles/reviewer/prompt.md +0 -5
  30. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/.gitignore +0 -0
  31. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/LICENSE +0 -0
  32. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/__init__.py +0 -0
  33. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/artifacts.py +0 -0
  34. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/errors.py +0 -0
  35. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/__init__.py +0 -0
  36. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/pi/__init__.py +0 -0
  37. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/pi/runtime/extensions/submit-result.ts +0 -0
  38. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/pi/runtime/image/Dockerfile +0 -0
  39. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/harnesses/resources.py +0 -0
  40. {openshell_agent_runner-0.0.1 → openshell_agent_runner-0.0.3}/src/openshell_agent_runner/profiles/__init__.py +0 -0
  41. {openshell_agent_runner-0.0.1/src/openshell_agent_runner/profiles/reviewer → openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer}/models.json +0 -0
  42. {openshell_agent_runner-0.0.1/src/openshell_agent_runner/profiles/reviewer → openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer}/policy.yaml +0 -0
  43. {openshell_agent_runner-0.0.1/src/openshell_agent_runner/profiles/reviewer → openshell_agent_runner-0.0.3/src/openshell_agent_runner/profiles/code-reviewer}/settings.json +0 -0
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: openshell-agent-runner
3
+ Version: 0.0.3
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) runs an agent task in an isolated OpenShell sandbox
27
+ and saves the result to a file. Use it to review code, review technical writing,
28
+ or run your own tasks from a terminal or CI job.
29
+
30
+ ```text
31
+ Profile + input → OAR → Agent in a temporary sandbox → Result file
32
+ sandbox removed when the run ends
33
+ ```
34
+
35
+ A **profile** packages prompts, reusable skills, and settings. A **task** is one
36
+ job in that profile; it selects the prompt and skills to use. OAR includes two
37
+ ready-to-use reviewers:
38
+
39
+ | Profile | Task | Input |
40
+ | --- | --- | --- |
41
+ | `code-reviewer` | `review-repository` | A local project directory |
42
+ | `technical-writing-reviewer` | `review-document` | A text file, such as Markdown or `.txt` |
43
+
44
+ ## Get started
45
+
46
+ You need [uv](https://docs.astral.sh/uv/getting-started/installation/) and
47
+ OpenShell 0.0.111 or newer, with a running gateway and configured inference.
48
+ If OpenShell is not ready, follow its
49
+ [quickstart](https://docs.nvidia.com/openshell/latest/get-started/quickstart).
50
+ OAR uses that setup to create sandboxes and reach your model.
51
+
52
+ **1. Install OAR and check your connection.**
53
+
54
+ ```bash
55
+ uv tool install openshell-agent-runner
56
+ oar doctor
57
+ ```
58
+
59
+ Alternatively, use `uvx --from openshell-agent-runner oar` in place of `oar`.
60
+
61
+ These commands use your selected OpenShell gateway and its `default` workspace.
62
+ For another target, add `--gateway NAME --workspace NAME` to `doctor` and `run`.
63
+
64
+ **2. Create your profiles.** Replace `YOUR_MODEL_ID` with the model ID shown in
65
+ the inference output from `oar doctor`.
66
+
67
+ ```bash
68
+ oar init ./profiles --model YOUR_MODEL_ID
69
+ ```
70
+
71
+ This creates editable copies of both reviewers. For a model without reasoning
72
+ support, add `--thinking off`.
73
+
74
+ **3. Review a document.** Replace `./README.md` with an existing text file.
75
+
76
+ ```bash
77
+ oar run ./profiles/technical-writing-reviewer \
78
+ --task review-document \
79
+ --input ./README.md \
80
+ --output ./review.json
81
+ ```
82
+
83
+ Open `review.json` for the verdict, a score from 0 to 100, and specific findings.
84
+ OAR saves the validated result and removes the sandbox. Add `--dry-run` to
85
+ preview the operation without launching an agent.
86
+
87
+ ## Review code
88
+
89
+ Pass a local project directory. Optional `focus` and `context` values help the
90
+ reviewer understand what matters for this run:
91
+
92
+ ```bash
93
+ oar run ./profiles/code-reviewer \
94
+ --task review-repository \
95
+ --input ./my-project \
96
+ --prompt-var focus="src/auth and tests/auth" \
97
+ --prompt-var context="A small internal tool; keep recommendations proportionate." \
98
+ --output ./code-review.json
99
+ ```
100
+
101
+ The agent works on an uploaded copy. Changes in the sandbox stay there; only the
102
+ result is downloaded. A `focus` value guides attention but does not limit which
103
+ files are uploaded.
104
+
105
+ ## Learn more
106
+
107
+ - [Get started](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/): setup and your first review.
108
+ - [Run reviews](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/reviews/): inputs, focus, context, and scores.
109
+ - [Customize profiles](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/profiles/): prompts, variables, skills, and output formats.
110
+ - [Command reference](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/reference/): options, CI behavior, and troubleshooting.
111
+
112
+ For help with a specific task:
113
+
114
+ ```bash
115
+ oar run ./profiles/code-reviewer --task review-repository --help
116
+ ```
117
+
118
+ ## Develop OAR
119
+
120
+ From `projects/openshell-agent-runner`, run:
121
+
122
+ ```bash
123
+ make check
124
+ make build
125
+ ```
126
+
127
+ `make check` includes CLI workflows against a simulated OpenShell. With Docker,
128
+ run `make test-runtime` to exercise the real Pi harness without external inference.
129
+ For runtime changes, one live CI task checks input transfer, prompt variables,
130
+ structured output, and sandbox cleanup. It does not grade reviewer opinions.
131
+
132
+ Use `uv run --frozen oar` in this directory to run the checked-out code instead
133
+ of the installed release. Run a focused test with
134
+ `make test PYTEST_ARGS="tests/test_config.py"`. See
135
+ [RELEASING.md](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/openshell-agent-runner/RELEASING.md)
136
+ for publishing, and the
137
+ [repository CI guide](https://github.com/NVIDIA/OpenShell-Research/blob/main/docs/development/ci.md)
138
+ for automated project reviews and the live OAR smoke test.
@@ -0,0 +1,115 @@
1
+ # OpenShell Agent Runner
2
+
3
+ OpenShell Agent Runner (OAR) runs an agent task in an isolated OpenShell sandbox
4
+ and saves the result to a file. Use it to review code, review technical writing,
5
+ or run your own tasks from a terminal or CI job.
6
+
7
+ ```text
8
+ Profile + input → OAR → Agent in a temporary sandbox → Result file
9
+ sandbox removed when the run ends
10
+ ```
11
+
12
+ A **profile** packages prompts, reusable skills, and settings. A **task** is one
13
+ job in that profile; it selects the prompt and skills to use. OAR includes two
14
+ ready-to-use reviewers:
15
+
16
+ | Profile | Task | Input |
17
+ | --- | --- | --- |
18
+ | `code-reviewer` | `review-repository` | A local project directory |
19
+ | `technical-writing-reviewer` | `review-document` | A text file, such as Markdown or `.txt` |
20
+
21
+ ## Get started
22
+
23
+ You need [uv](https://docs.astral.sh/uv/getting-started/installation/) and
24
+ OpenShell 0.0.111 or newer, with a running gateway and configured inference.
25
+ If OpenShell is not ready, follow its
26
+ [quickstart](https://docs.nvidia.com/openshell/latest/get-started/quickstart).
27
+ OAR uses that setup to create sandboxes and reach your model.
28
+
29
+ **1. Install OAR and check your connection.**
30
+
31
+ ```bash
32
+ uv tool install openshell-agent-runner
33
+ oar doctor
34
+ ```
35
+
36
+ Alternatively, use `uvx --from openshell-agent-runner oar` in place of `oar`.
37
+
38
+ These commands use your selected OpenShell gateway and its `default` workspace.
39
+ For another target, add `--gateway NAME --workspace NAME` to `doctor` and `run`.
40
+
41
+ **2. Create your profiles.** Replace `YOUR_MODEL_ID` with the model ID shown in
42
+ the inference output from `oar doctor`.
43
+
44
+ ```bash
45
+ oar init ./profiles --model YOUR_MODEL_ID
46
+ ```
47
+
48
+ This creates editable copies of both reviewers. For a model without reasoning
49
+ support, add `--thinking off`.
50
+
51
+ **3. Review a document.** Replace `./README.md` with an existing text file.
52
+
53
+ ```bash
54
+ oar run ./profiles/technical-writing-reviewer \
55
+ --task review-document \
56
+ --input ./README.md \
57
+ --output ./review.json
58
+ ```
59
+
60
+ Open `review.json` for the verdict, a score from 0 to 100, and specific findings.
61
+ OAR saves the validated result and removes the sandbox. Add `--dry-run` to
62
+ preview the operation without launching an agent.
63
+
64
+ ## Review code
65
+
66
+ Pass a local project directory. Optional `focus` and `context` values help the
67
+ reviewer understand what matters for this run:
68
+
69
+ ```bash
70
+ oar run ./profiles/code-reviewer \
71
+ --task review-repository \
72
+ --input ./my-project \
73
+ --prompt-var focus="src/auth and tests/auth" \
74
+ --prompt-var context="A small internal tool; keep recommendations proportionate." \
75
+ --output ./code-review.json
76
+ ```
77
+
78
+ The agent works on an uploaded copy. Changes in the sandbox stay there; only the
79
+ result is downloaded. A `focus` value guides attention but does not limit which
80
+ files are uploaded.
81
+
82
+ ## Learn more
83
+
84
+ - [Get started](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/): setup and your first review.
85
+ - [Run reviews](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/reviews/): inputs, focus, context, and scores.
86
+ - [Customize profiles](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/profiles/): prompts, variables, skills, and output formats.
87
+ - [Command reference](https://nvidia.github.io/OpenShell-Research/documentation/openshell-agent-runner/reference/): options, CI behavior, and troubleshooting.
88
+
89
+ For help with a specific task:
90
+
91
+ ```bash
92
+ oar run ./profiles/code-reviewer --task review-repository --help
93
+ ```
94
+
95
+ ## Develop OAR
96
+
97
+ From `projects/openshell-agent-runner`, run:
98
+
99
+ ```bash
100
+ make check
101
+ make build
102
+ ```
103
+
104
+ `make check` includes CLI workflows against a simulated OpenShell. With Docker,
105
+ run `make test-runtime` to exercise the real Pi harness without external inference.
106
+ For runtime changes, one live CI task checks input transfer, prompt variables,
107
+ structured output, and sandbox cleanup. It does not grade reviewer opinions.
108
+
109
+ Use `uv run --frozen oar` in this directory to run the checked-out code instead
110
+ of the installed release. Run a focused test with
111
+ `make test PYTEST_ARGS="tests/test_config.py"`. See
112
+ [RELEASING.md](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/openshell-agent-runner/RELEASING.md)
113
+ for publishing, and the
114
+ [repository CI guide](https://github.com/NVIDIA/OpenShell-Research/blob/main/docs/development/ci.md)
115
+ for automated project reviews and the live OAR smoke test.
@@ -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]
@@ -120,9 +120,16 @@ def run(
120
120
  output: Annotated[
121
121
  Path, typer.Option("--output", help="Host path for the agent result.")
122
122
  ],
123
- input_document: Annotated[
123
+ input_path: Annotated[
124
124
  Path | None,
125
- typer.Option("--input", help="Host document required by document tasks."),
125
+ typer.Option("--input", help="Host input required by the selected task."),
126
+ ] = None,
127
+ prompt_variable: Annotated[
128
+ list[str] | None,
129
+ typer.Option(
130
+ "--prompt-var",
131
+ help="Non-secret NAME=VALUE prompt variable. Repeat for several.",
132
+ ),
126
133
  ] = None,
127
134
  upload: Annotated[
128
135
  list[str] | None,
@@ -159,7 +166,8 @@ def run(
159
166
  profile_directory=profile,
160
167
  task_id=task,
161
168
  output=output,
162
- input_document=input_document,
169
+ input_path=input_path,
170
+ prompt_variables=prompt_variable or (),
163
171
  uploads=upload or (),
164
172
  environments=environment or (),
165
173
  gateway=gateway,
@@ -185,12 +193,17 @@ def doctor(
185
193
  str, typer.Option("--workspace", help="OpenShell workspace name.")
186
194
  ] = "default",
187
195
  ) -> None:
188
- """Check OpenShell readiness without changing its state."""
196
+ """Inspect OpenShell connection and configuration without changing its state."""
189
197
  try:
190
198
  checks = run_doctor(NativeTarget(gateway=gateway, workspace=workspace))
191
199
  except OarError as error:
192
200
  _fail(error)
193
201
  typer.echo("\n\n".join(result for _, result in checks))
202
+ typer.echo(
203
+ "\nThese are configuration checks, not an inference request. "
204
+ "Before running a task, confirm that an inference provider and model "
205
+ "are listed above."
206
+ )
194
207
 
195
208
 
196
209
  def _fail(error: OarError) -> NoReturn:
@@ -227,8 +240,10 @@ def _render_task_help(
227
240
  _help_command(f" oar run {shlex.quote(str(profile_directory))} \\"),
228
241
  _help_command(f" --task {shlex.quote(task_id)} \\"),
229
242
  ]
230
- if task.required_input == "document":
231
- usage_lines.append(_help_command(" --input DOCUMENT \\"))
243
+ if task.required_input is not None:
244
+ usage_lines.append(
245
+ _help_command(f" --input {task.required_input.upper()} \\")
246
+ )
232
247
  usage_lines.append(_help_command(" --output OUTPUT"))
233
248
 
234
249
  upload_lines = [_help_heading("Additional configured uploads:")]
@@ -244,6 +259,22 @@ def _render_task_help(
244
259
  environment_lines.append(" None. Add values with --env KEY=VALUE.")
245
260
 
246
261
  input_lines = _required_input_help(task.required_input)
262
+ prompt_variable_lines = [_help_heading("Prompt variables:")]
263
+ if task.prompt_variables:
264
+ for name, variable in task.prompt_variables.items():
265
+ requirement = (
266
+ f"Default: {variable.default}"
267
+ if variable.default is not None
268
+ else "Required."
269
+ )
270
+ prompt_variable_lines.extend(
271
+ [
272
+ _help_command(f" --prompt-var {name}=VALUE"),
273
+ f" {variable.description} {requirement}",
274
+ ]
275
+ )
276
+ else:
277
+ prompt_variable_lines.append(" None.")
247
278
 
248
279
  output_description = (
249
280
  f"JSON validated against {task.output_schema}."
@@ -260,6 +291,8 @@ def _render_task_help(
260
291
  "",
261
292
  *input_lines,
262
293
  "",
294
+ *prompt_variable_lines,
295
+ "",
263
296
  *upload_lines,
264
297
  "",
265
298
  *environment_lines,
@@ -274,10 +307,15 @@ def _render_task_help(
274
307
  def _required_input_help(required_input: str | None) -> list[str]:
275
308
  if required_input is None:
276
309
  return [_help_heading("Required input:"), " None."]
310
+ description = (
311
+ "Host document to review."
312
+ if required_input == "document"
313
+ else "Host code repository to review."
314
+ )
277
315
  return [
278
316
  _help_heading("Required argument:"),
279
- _help_command(" --input DOCUMENT"),
280
- " Host document to review.",
317
+ _help_command(f" --input {required_input.upper()}"),
318
+ f" {description}",
281
319
  ]
282
320
 
283
321
 
@@ -19,9 +19,15 @@ 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
26
+ from openshell_agent_runner.prompt_templates import (
27
+ BUILTIN_PROMPT_VARIABLES,
28
+ PROMPT_VARIABLE_NAME_PATTERN,
29
+ validate_prompt_template,
30
+ )
25
31
 
26
32
  IDENTIFIER_PATTERN = r"^[a-z][a-z0-9-]{0,62}$"
27
33
  RESOURCE_IDENTIFIER_PATTERN = r"^[a-z][a-z0-9_-]{0,62}$"
@@ -29,6 +35,8 @@ MODEL_IDENTIFIER_PATTERN = r"^[A-Za-z0-9._:/-]{1,256}$"
29
35
  MODELS_FILENAME = "models.json"
30
36
  PROFILE_FILENAME = "profile.yaml"
31
37
  SETTINGS_FILENAME = "settings.json"
38
+ BUILTIN_PI_TOOLS = frozenset({"bash", "edit", "find", "grep", "ls", "read", "write"})
39
+ SUBMIT_RESULT_TOOL = "submit_result"
32
40
  _PI_RUNTIME_SETTING_KEYS = {
33
41
  "defaultProvider",
34
42
  "defaultModel",
@@ -58,24 +66,86 @@ class SandboxConfig(StrictModel):
58
66
  return values
59
67
 
60
68
 
69
+ ToolName = Annotated[str, Field(pattern=RESOURCE_IDENTIFIER_PATTERN)]
70
+ PromptVariableName = Annotated[str, Field(pattern=PROMPT_VARIABLE_NAME_PATTERN)]
71
+
72
+
73
+ class ExtensionConfig(StrictModel):
74
+ path: Path
75
+ tools: list[ToolName] = Field(default_factory=list)
76
+
77
+ @field_validator("tools")
78
+ @classmethod
79
+ def require_unique_tools(cls, values: list[str]) -> list[str]:
80
+ if len(values) != len(set(values)):
81
+ raise ValueError("extension tool entries must be unique")
82
+ return values
83
+
84
+
85
+ class PromptVariableConfig(StrictModel):
86
+ description: str = Field(min_length=1, max_length=1000)
87
+ default: str | None = Field(default=None, min_length=1)
88
+
89
+
61
90
  class TaskConfig(StrictModel):
62
91
  description: str | None = Field(default=None, min_length=1, max_length=1000)
63
- required_input: Literal["document"] | None = None
92
+ required_input: Literal["document", "repository"] | None = None
64
93
  prompt: Path
65
- output_schema: Path | None = None
66
- tools: list[Annotated[str, Field(pattern=RESOURCE_IDENTIFIER_PATTERN)]] = Field(
67
- default_factory=list
94
+ prompt_variables: dict[PromptVariableName, PromptVariableConfig] = Field(
95
+ default_factory=dict
68
96
  )
97
+ output_schema: Path | None = None
98
+ tools: list[ToolName] = Field(default_factory=list)
69
99
  skills: list[Path] = Field(default_factory=list)
70
- extensions: list[Path] = Field(default_factory=list)
100
+ extensions: list[ExtensionConfig] = Field(default_factory=list)
71
101
 
72
- @field_validator("tools", "skills", "extensions")
102
+ @field_validator("tools", "skills")
73
103
  @classmethod
74
104
  def require_unique_resources(cls, values: list[object]) -> list[object]:
75
105
  if len(values) != len(set(values)):
76
106
  raise ValueError("resource entries must be unique")
77
107
  return values
78
108
 
109
+ @field_validator("extensions")
110
+ @classmethod
111
+ def require_unique_extensions(
112
+ cls, values: list[ExtensionConfig]
113
+ ) -> list[ExtensionConfig]:
114
+ paths = [extension.path for extension in values]
115
+ if len(paths) != len(set(paths)):
116
+ raise ValueError("extension paths must be unique")
117
+ return values
118
+
119
+ @model_validator(mode="after")
120
+ def require_known_tools(self) -> TaskConfig:
121
+ declared_custom_tools: set[str] = set()
122
+ for extension in self.extensions:
123
+ for tool in extension.tools:
124
+ if tool in BUILTIN_PI_TOOLS or tool == SUBMIT_RESULT_TOOL:
125
+ raise ValueError(
126
+ f"extension tool {tool!r} conflicts with a reserved tool"
127
+ )
128
+ if tool in declared_custom_tools:
129
+ raise ValueError(
130
+ f"custom tool {tool!r} is declared by multiple extensions"
131
+ )
132
+ declared_custom_tools.add(tool)
133
+
134
+ available_tools = BUILTIN_PI_TOOLS | declared_custom_tools
135
+ unknown_tools = sorted(set(self.tools) - available_tools)
136
+ if unknown_tools:
137
+ raise ValueError(
138
+ f"unknown tools {unknown_tools}; Pi built-ins are "
139
+ f"{sorted(BUILTIN_PI_TOOLS)}; declare each custom tool under a "
140
+ "referenced extension"
141
+ )
142
+ if self.skills and "read" not in self.tools:
143
+ raise ValueError(
144
+ "tasks with skills must include 'read' in tools so Pi can "
145
+ "discover and load the skills"
146
+ )
147
+ return self
148
+
79
149
 
80
150
  class ProfileConfig(StrictModel):
81
151
  id: Annotated[str, Field(pattern=IDENTIFIER_PATTERN)]
@@ -155,7 +225,6 @@ def resolve_task(profile_directory: Path, task_id: str) -> ResolvedProfile:
155
225
  def validate_upload_mappings(values: Sequence[str]) -> tuple[str, ...]:
156
226
  if len(values) != len(set(values)):
157
227
  raise ValueError("duplicate upload mapping")
158
- destinations: dict[str, str] = {}
159
228
  for value in values:
160
229
  source, separator, destination = value.rpartition(":")
161
230
  if not separator or not source or not destination.startswith("/"):
@@ -169,16 +238,11 @@ def validate_upload_mappings(values: Sequence[str]) -> tuple[str, ...]:
169
238
  PurePosixPath("/sandbox/artifacts"),
170
239
  PurePosixPath("/sandbox/oar-runtime"),
171
240
  ):
172
- if path == reserved or path.is_relative_to(reserved):
241
+ if path.is_relative_to(reserved) or reserved.is_relative_to(path):
173
242
  raise ValueError(
174
243
  "upload destination is reserved for runner resources: "
175
244
  f"{destination}"
176
245
  )
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
246
  return tuple(values)
183
247
 
184
248
 
@@ -300,7 +364,23 @@ def _validate_profile_resources(resolved: ResolvedProfile) -> None:
300
364
  directory = resolved.profile_dir
301
365
  _inside(directory, directory / resolved.profile.sandbox.policy, "sandbox policy")
302
366
  for task_id, task in resolved.profile.tasks.items():
303
- _inside(directory, directory / task.prompt, f"prompt for task {task_id}")
367
+ prompt = _inside(
368
+ directory, directory / task.prompt, f"prompt for task {task_id}"
369
+ )
370
+ available_builtins = (
371
+ BUILTIN_PROMPT_VARIABLES if task.required_input is not None else frozenset()
372
+ )
373
+ try:
374
+ template = prompt.read_text(encoding="utf-8")
375
+ validate_prompt_template(
376
+ template,
377
+ task.prompt_variables.keys(),
378
+ available_builtins,
379
+ )
380
+ except (OSError, UnicodeError, ValueError) as error:
381
+ raise ConfigurationError(
382
+ f"invalid prompt template for task {task_id}: {error}"
383
+ ) from error
304
384
  if task.output_schema is not None:
305
385
  schema = _inside(
306
386
  directory,
@@ -326,7 +406,11 @@ def _validate_profile_resources(resolved: ResolvedProfile) -> None:
326
406
  f"skill for task {task_id} contains a symlink: {descendant}"
327
407
  )
328
408
  for extension in task.extensions:
329
- _inside(directory, directory / extension, f"extension for task {task_id}")
409
+ _inside(
410
+ directory,
411
+ directory / extension.path,
412
+ f"extension for task {task_id}",
413
+ )
330
414
 
331
415
 
332
416
  def _validate_output_schema(path: Path) -> None:
@@ -335,10 +419,10 @@ def _validate_output_schema(path: Path) -> None:
335
419
  Draft202012Validator.check_schema(document)
336
420
  except (OSError, UnicodeError, json.JSONDecodeError, SchemaError) as error:
337
421
  raise ConfigurationError(f"invalid output schema {path}: {error}") from error
338
- _validate_schema_references(document, path)
422
+ _validate_schema_references(document)
339
423
 
340
424
 
341
- def _validate_schema_references(document: Any, path: Path) -> None:
425
+ def _validate_schema_references(document: Any) -> None:
342
426
  if not isinstance(document, dict):
343
427
  return
344
428
 
@@ -349,23 +433,22 @@ def _validate_schema_references(document: Any, path: Path) -> None:
349
433
  f"({key}) because host and sandbox engines use different dialects"
350
434
  )
351
435
  for key in {"$ref", "$dynamicRef", "$recursiveRef"}:
352
- if key in document and (
353
- not isinstance(document[key], str) or not document[key].startswith("#")
354
- ):
436
+ if key in document:
355
437
  raise ConfigurationError(
356
- f"output schema references must stay inside {path}: {document[key]!r}"
438
+ "output schemas do not support reference keywords "
439
+ f"({key}) because the submission tool nests the schema"
357
440
  )
358
441
 
359
442
  for key in {"$defs", "definitions", "properties", "dependentSchemas"}:
360
443
  value = document.get(key)
361
444
  if isinstance(value, dict):
362
445
  for schema in value.values():
363
- _validate_schema_references(schema, path)
446
+ _validate_schema_references(schema)
364
447
  for key in {"allOf", "anyOf", "oneOf", "prefixItems"}:
365
448
  value = document.get(key)
366
449
  if isinstance(value, list):
367
450
  for schema in value:
368
- _validate_schema_references(schema, path)
451
+ _validate_schema_references(schema)
369
452
  for key in {
370
453
  "additionalProperties",
371
454
  "contains",
@@ -379,4 +462,4 @@ def _validate_schema_references(document: Any, path: Path) -> None:
379
462
  "unevaluatedItems",
380
463
  "unevaluatedProperties",
381
464
  }:
382
- _validate_schema_references(document.get(key), path)
465
+ _validate_schema_references(document.get(key))
@@ -3,8 +3,10 @@
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
9
+ from collections.abc import Mapping
8
10
  from importlib.resources import files
9
11
  from pathlib import Path
10
12
 
@@ -14,6 +16,7 @@ from openshell_agent_runner.config import (
14
16
  ResolvedProfile,
15
17
  )
16
18
  from openshell_agent_runner.harnesses.resources import PreparedResources
19
+ from openshell_agent_runner.prompt_templates import render_prompt_template
17
20
 
18
21
  SANDBOX_RUNTIME_ROOT = "/sandbox/oar-runtime"
19
22
 
@@ -22,13 +25,19 @@ def image_directory() -> Path:
22
25
  return Path(str(files("openshell_agent_runner.harnesses.pi") / "runtime" / "image"))
23
26
 
24
27
 
25
- def prepare_resources(resolved: ResolvedProfile, task_id: str) -> PreparedResources:
28
+ def prepare_resources(
29
+ resolved: ResolvedProfile,
30
+ task_id: str,
31
+ prompt_variables: Mapping[str, str] | None = None,
32
+ ) -> PreparedResources:
26
33
  temporary = tempfile.TemporaryDirectory(prefix="oar-pi-")
27
34
  runtime = Path(temporary.name) / "runtime"
28
35
  (runtime / "skills").mkdir(parents=True, exist_ok=True)
29
36
  (runtime / "extensions").mkdir(parents=True, exist_ok=True)
30
37
  task = resolved.profile.tasks[task_id]
31
- shutil.copy2(resolved.profile_dir / task.prompt, runtime / "prompt.md")
38
+ template = (resolved.profile_dir / task.prompt).read_text(encoding="utf-8")
39
+ rendered_prompt = render_prompt_template(template, prompt_variables or {})
40
+ (runtime / "prompt.md").write_text(rendered_prompt, encoding="utf-8")
32
41
  shutil.copy2(resolved.profile_dir / MODELS_FILENAME, runtime / MODELS_FILENAME)
33
42
  shutil.copy2(resolved.profile_dir / SETTINGS_FILENAME, runtime / SETTINGS_FILENAME)
34
43
  arguments = [
@@ -66,15 +75,34 @@ def prepare_resources(resolved: ResolvedProfile, task_id: str) -> PreparedResour
66
75
  shutil.copytree(resolved.profile_dir / skill, target)
67
76
  arguments.extend(["--skill", f"{SANDBOX_RUNTIME_ROOT}/skills/{target.name}"])
68
77
  for index, extension in enumerate(task.extensions):
69
- target = runtime / "extensions" / f"{index:02d}-{extension.name}"
70
- shutil.copy2(resolved.profile_dir / extension, target)
78
+ target = runtime / "extensions" / f"{index:02d}-{extension.path.name}"
79
+ shutil.copy2(resolved.profile_dir / extension.path, target)
71
80
  arguments.extend(
72
81
  ["--extension", f"{SANDBOX_RUNTIME_ROOT}/extensions/{target.name}"]
73
82
  )
83
+ expected_tools = runtime / "tools.json"
84
+ expected_tools.write_text(f"{json.dumps(tools)}\n", encoding="utf-8")
85
+ validate_tools = Path(
86
+ str(
87
+ files("openshell_agent_runner.harnesses.pi")
88
+ / "runtime"
89
+ / "extensions"
90
+ / "validate-tools.ts"
91
+ )
92
+ )
93
+ validator_target = runtime / "extensions" / "oar-validate-tools.ts"
94
+ shutil.copy2(validate_tools, validator_target)
95
+ arguments.extend(
96
+ [
97
+ "--extension",
98
+ f"{SANDBOX_RUNTIME_ROOT}/extensions/{validator_target.name}",
99
+ ]
100
+ )
74
101
  uploads = [
75
102
  f"{runtime / 'prompt.md'}:{SANDBOX_RUNTIME_ROOT}/prompt.md",
76
103
  f"{runtime / 'models.json'}:{SANDBOX_RUNTIME_ROOT}/models.json",
77
104
  f"{runtime / 'settings.json'}:{SANDBOX_RUNTIME_ROOT}/settings.json",
105
+ f"{expected_tools}:{SANDBOX_RUNTIME_ROOT}/tools.json",
78
106
  ]
79
107
  if task.output_schema is not None:
80
108
  uploads.append(