openshell-agent-runner 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,382 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Define, load, validate, and resolve agent profile configuration."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ from collections.abc import Sequence
11
+ from pathlib import Path, PurePosixPath
12
+ from typing import Annotated, Any, Literal
13
+
14
+ import yaml
15
+ from jsonschema import Draft202012Validator, SchemaError
16
+ from pydantic import (
17
+ BaseModel,
18
+ ConfigDict,
19
+ Field,
20
+ ValidationError,
21
+ field_validator,
22
+ )
23
+
24
+ from openshell_agent_runner.errors import ConfigurationError
25
+
26
+ IDENTIFIER_PATTERN = r"^[a-z][a-z0-9-]{0,62}$"
27
+ RESOURCE_IDENTIFIER_PATTERN = r"^[a-z][a-z0-9_-]{0,62}$"
28
+ MODEL_IDENTIFIER_PATTERN = r"^[A-Za-z0-9._:/-]{1,256}$"
29
+ MODELS_FILENAME = "models.json"
30
+ PROFILE_FILENAME = "profile.yaml"
31
+ SETTINGS_FILENAME = "settings.json"
32
+ _PI_RUNTIME_SETTING_KEYS = {
33
+ "defaultProvider",
34
+ "defaultModel",
35
+ "defaultThinkingLevel",
36
+ }
37
+
38
+
39
+ class StrictModel(BaseModel):
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+
43
+ class SandboxConfig(StrictModel):
44
+ policy: Path
45
+ upload: list[str] = Field(default_factory=list)
46
+ env: list[str] = Field(default_factory=list)
47
+
48
+ @field_validator("upload")
49
+ @classmethod
50
+ def validate_uploads(cls, values: list[str]) -> list[str]:
51
+ validate_upload_mappings(values)
52
+ return values
53
+
54
+ @field_validator("env")
55
+ @classmethod
56
+ def validate_environment(cls, values: list[str]) -> list[str]:
57
+ validate_environment_assignments(values)
58
+ return values
59
+
60
+
61
+ class TaskConfig(StrictModel):
62
+ description: str | None = Field(default=None, min_length=1, max_length=1000)
63
+ required_input: Literal["document"] | None = None
64
+ prompt: Path
65
+ output_schema: Path | None = None
66
+ tools: list[Annotated[str, Field(pattern=RESOURCE_IDENTIFIER_PATTERN)]] = Field(
67
+ default_factory=list
68
+ )
69
+ skills: list[Path] = Field(default_factory=list)
70
+ extensions: list[Path] = Field(default_factory=list)
71
+
72
+ @field_validator("tools", "skills", "extensions")
73
+ @classmethod
74
+ def require_unique_resources(cls, values: list[object]) -> list[object]:
75
+ if len(values) != len(set(values)):
76
+ raise ValueError("resource entries must be unique")
77
+ return values
78
+
79
+
80
+ class ProfileConfig(StrictModel):
81
+ id: Annotated[str, Field(pattern=IDENTIFIER_PATTERN)]
82
+ description: str = Field(min_length=1, max_length=1000)
83
+ sandbox: SandboxConfig
84
+ tasks: dict[Annotated[str, Field(pattern=IDENTIFIER_PATTERN)], TaskConfig]
85
+
86
+ @field_validator("tasks")
87
+ @classmethod
88
+ def require_tasks(cls, value: dict[str, TaskConfig]) -> dict[str, TaskConfig]:
89
+ if not value:
90
+ raise ValueError("at least one task is required")
91
+ return value
92
+
93
+
94
+ class PiRuntimeSettings(StrictModel):
95
+ provider: Literal["openshell"]
96
+ model: Annotated[str, Field(pattern=MODEL_IDENTIFIER_PATTERN)]
97
+ thinking: Literal["off", "minimal", "low", "medium", "high", "xhigh", "max"]
98
+
99
+
100
+ class ResolvedProfile(StrictModel):
101
+ profile_path: Path
102
+ profile_dir: Path
103
+ profile: ProfileConfig
104
+ runtime: PiRuntimeSettings
105
+
106
+
107
+ def load_profile(directory: Path) -> ResolvedProfile:
108
+ try:
109
+ profile_dir = directory.resolve(strict=True)
110
+ except OSError as error:
111
+ raise ConfigurationError(f"missing profile directory: {directory}") from error
112
+ if not profile_dir.is_dir():
113
+ raise ConfigurationError(
114
+ f"profile must be a directory containing {PROFILE_FILENAME}: {directory}"
115
+ )
116
+ candidate = profile_dir / PROFILE_FILENAME
117
+ try:
118
+ profile_path = candidate.resolve(strict=True)
119
+ except OSError as error:
120
+ raise ConfigurationError(
121
+ f"missing profile configuration: {candidate}"
122
+ ) from error
123
+ if not profile_path.is_relative_to(profile_dir) or not profile_path.is_file():
124
+ raise ConfigurationError(
125
+ f"profile configuration must be a file inside {profile_dir}: {candidate}"
126
+ )
127
+ try:
128
+ profile = ProfileConfig.model_validate(_load_yaml(profile_path))
129
+ except ValidationError as error:
130
+ raise ConfigurationError(f"invalid profile {profile_path}: {error}") from error
131
+ model_path = _inside(profile_dir, profile_dir / MODELS_FILENAME, "Pi models file")
132
+ settings_path = _inside(
133
+ profile_dir, profile_dir / SETTINGS_FILENAME, "Pi settings file"
134
+ )
135
+ model_id = _load_pi_model_id(model_path)
136
+ resolved = ResolvedProfile(
137
+ profile_path=profile_path,
138
+ profile_dir=profile_dir,
139
+ profile=profile,
140
+ runtime=_load_pi_runtime_settings(settings_path, model_id),
141
+ )
142
+ _validate_profile_resources(resolved)
143
+ return resolved
144
+
145
+
146
+ def resolve_task(profile_directory: Path, task_id: str) -> ResolvedProfile:
147
+ resolved = load_profile(profile_directory)
148
+ if task_id not in resolved.profile.tasks:
149
+ raise ConfigurationError(
150
+ f"unknown task {task_id!r} for profile {resolved.profile.id!r}"
151
+ )
152
+ return resolved
153
+
154
+
155
+ def validate_upload_mappings(values: Sequence[str]) -> tuple[str, ...]:
156
+ if len(values) != len(set(values)):
157
+ raise ValueError("duplicate upload mapping")
158
+ destinations: dict[str, str] = {}
159
+ for value in values:
160
+ source, separator, destination = value.rpartition(":")
161
+ if not separator or not source or not destination.startswith("/"):
162
+ raise ValueError("uploads must use SOURCE:/ABSOLUTE/DESTINATION")
163
+ path = PurePosixPath(destination)
164
+ if destination.startswith("//") or str(path) != destination:
165
+ raise ValueError("upload destinations must use canonical absolute paths")
166
+ if ".." in path.parts:
167
+ raise ValueError("upload destinations must not contain '..'")
168
+ for reserved in (
169
+ PurePosixPath("/sandbox/artifacts"),
170
+ PurePosixPath("/sandbox/oar-runtime"),
171
+ ):
172
+ if path == reserved or path.is_relative_to(reserved):
173
+ raise ValueError(
174
+ "upload destination is reserved for runner resources: "
175
+ f"{destination}"
176
+ )
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
+ return tuple(values)
183
+
184
+
185
+ def validate_environment_assignments(values: Sequence[str]) -> tuple[str, ...]:
186
+ if len(values) != len(set(values)):
187
+ raise ValueError("duplicate environment assignment")
188
+ assignments: dict[str, str] = {}
189
+ for value in values:
190
+ key, separator, assigned = value.partition("=")
191
+ if not separator or not assigned:
192
+ raise ValueError("environment must use non-empty KEY=VALUE syntax")
193
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
194
+ raise ValueError(f"invalid OpenShell environment name: {key!r}")
195
+ if key.startswith("OPENSHELL_"):
196
+ raise ValueError(
197
+ f"environment name uses reserved OPENSHELL_ prefix: {key!r}"
198
+ )
199
+ previous = assignments.get(key)
200
+ if previous is not None and previous != assigned:
201
+ raise ValueError(f"conflicting environment values for key {key!r}")
202
+ assignments[key] = assigned
203
+ return tuple(values)
204
+
205
+
206
+ def _load_yaml(path: Path) -> Any:
207
+ try:
208
+ with path.open(encoding="utf-8") as stream:
209
+ return yaml.safe_load(stream)
210
+ except (OSError, UnicodeError) as error:
211
+ raise ConfigurationError(
212
+ f"cannot read configuration {path}: {error}"
213
+ ) from error
214
+ except yaml.YAMLError as error:
215
+ raise ConfigurationError(f"invalid YAML in {path}: {error}") from error
216
+
217
+
218
+ def _load_pi_model_id(path: Path) -> str:
219
+ try:
220
+ document = json.loads(path.read_text(encoding="utf-8"))
221
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
222
+ raise ConfigurationError(f"invalid Pi models file {path}: {error}") from error
223
+ providers = document.get("providers") if isinstance(document, dict) else None
224
+ if not isinstance(providers, dict) or set(providers) != {"openshell"}:
225
+ raise ConfigurationError(
226
+ "Pi models file must contain exactly one provider named 'openshell'"
227
+ )
228
+ provider = providers["openshell"]
229
+ models = provider.get("models") if isinstance(provider, dict) else None
230
+ if not isinstance(models, list) or len(models) != 1:
231
+ raise ConfigurationError(
232
+ "Pi models file must contain exactly one model under 'openshell'"
233
+ )
234
+ model = models[0]
235
+ model_id = model.get("id") if isinstance(model, dict) else None
236
+ if not isinstance(model_id, str) or not re.fullmatch(
237
+ MODEL_IDENTIFIER_PATTERN, model_id
238
+ ):
239
+ raise ConfigurationError("Pi model must have a valid string id")
240
+ return model_id
241
+
242
+
243
+ def _load_pi_runtime_settings(path: Path, model_id: str) -> PiRuntimeSettings:
244
+ try:
245
+ document = json.loads(path.read_text(encoding="utf-8"))
246
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
247
+ raise ConfigurationError(f"invalid Pi settings file {path}: {error}") from error
248
+ if not isinstance(document, dict):
249
+ raise ConfigurationError(f"Pi settings file must contain an object: {path}")
250
+ unexpected = set(document) - _PI_RUNTIME_SETTING_KEYS
251
+ missing = _PI_RUNTIME_SETTING_KEYS - set(document)
252
+ if unexpected or missing:
253
+ diagnostics = []
254
+ if missing:
255
+ diagnostics.append(f"missing {sorted(missing)}")
256
+ if unexpected:
257
+ diagnostics.append(f"unexpected {sorted(unexpected)}")
258
+ raise ConfigurationError(
259
+ f"Pi settings file must contain only runtime selection keys: "
260
+ f"{', '.join(diagnostics)}"
261
+ )
262
+ try:
263
+ runtime = PiRuntimeSettings.model_validate(
264
+ {
265
+ "provider": document.get("defaultProvider"),
266
+ "model": document.get("defaultModel"),
267
+ "thinking": document.get("defaultThinkingLevel"),
268
+ }
269
+ )
270
+ except ValidationError as error:
271
+ raise ConfigurationError(
272
+ f"invalid Pi runtime settings in {path}: {error}"
273
+ ) from error
274
+ if runtime.model != model_id:
275
+ raise ConfigurationError(
276
+ "Pi settings defaultModel must identify the model in models.json"
277
+ )
278
+ return runtime
279
+
280
+
281
+ def _inside(
282
+ owner: Path, candidate: Path, description: str, *, directory: bool = False
283
+ ) -> Path:
284
+ try:
285
+ resolved = candidate.resolve(strict=True)
286
+ except OSError as error:
287
+ raise ConfigurationError(f"missing {description}: {candidate}") from error
288
+ owner_resolved = owner.resolve(strict=True)
289
+ if not resolved.is_relative_to(owner_resolved):
290
+ raise ConfigurationError(f"{description} escapes {owner_resolved}: {candidate}")
291
+ expected = "directory" if directory else "file"
292
+ if (directory and not resolved.is_dir()) or (
293
+ not directory and not resolved.is_file()
294
+ ):
295
+ raise ConfigurationError(f"{description} must be a {expected}: {candidate}")
296
+ return resolved
297
+
298
+
299
+ def _validate_profile_resources(resolved: ResolvedProfile) -> None:
300
+ directory = resolved.profile_dir
301
+ _inside(directory, directory / resolved.profile.sandbox.policy, "sandbox policy")
302
+ for task_id, task in resolved.profile.tasks.items():
303
+ _inside(directory, directory / task.prompt, f"prompt for task {task_id}")
304
+ if task.output_schema is not None:
305
+ schema = _inside(
306
+ directory,
307
+ directory / task.output_schema,
308
+ f"output schema for task {task_id}",
309
+ )
310
+ _validate_output_schema(schema)
311
+ for skill in task.skills:
312
+ skill_directory = _inside(
313
+ directory,
314
+ directory / skill,
315
+ f"skill for task {task_id}",
316
+ directory=True,
317
+ )
318
+ _inside(
319
+ skill_directory,
320
+ skill_directory / "SKILL.md",
321
+ f"SKILL.md for task {task_id}",
322
+ )
323
+ for descendant in skill_directory.rglob("*"):
324
+ if descendant.is_symlink():
325
+ raise ConfigurationError(
326
+ f"skill for task {task_id} contains a symlink: {descendant}"
327
+ )
328
+ for extension in task.extensions:
329
+ _inside(directory, directory / extension, f"extension for task {task_id}")
330
+
331
+
332
+ def _validate_output_schema(path: Path) -> None:
333
+ try:
334
+ document = json.loads(path.read_text(encoding="utf-8"))
335
+ Draft202012Validator.check_schema(document)
336
+ except (OSError, UnicodeError, json.JSONDecodeError, SchemaError) as error:
337
+ raise ConfigurationError(f"invalid output schema {path}: {error}") from error
338
+ _validate_schema_references(document, path)
339
+
340
+
341
+ def _validate_schema_references(document: Any, path: Path) -> None:
342
+ if not isinstance(document, dict):
343
+ return
344
+
345
+ for key in {"pattern", "patternProperties"}:
346
+ if key in document:
347
+ raise ConfigurationError(
348
+ "output schemas do not support regular-expression keywords "
349
+ f"({key}) because host and sandbox engines use different dialects"
350
+ )
351
+ for key in {"$ref", "$dynamicRef", "$recursiveRef"}:
352
+ if key in document and (
353
+ not isinstance(document[key], str) or not document[key].startswith("#")
354
+ ):
355
+ raise ConfigurationError(
356
+ f"output schema references must stay inside {path}: {document[key]!r}"
357
+ )
358
+
359
+ for key in {"$defs", "definitions", "properties", "dependentSchemas"}:
360
+ value = document.get(key)
361
+ if isinstance(value, dict):
362
+ for schema in value.values():
363
+ _validate_schema_references(schema, path)
364
+ for key in {"allOf", "anyOf", "oneOf", "prefixItems"}:
365
+ value = document.get(key)
366
+ if isinstance(value, list):
367
+ for schema in value:
368
+ _validate_schema_references(schema, path)
369
+ for key in {
370
+ "additionalProperties",
371
+ "contains",
372
+ "contentSchema",
373
+ "else",
374
+ "if",
375
+ "items",
376
+ "not",
377
+ "propertyNames",
378
+ "then",
379
+ "unevaluatedItems",
380
+ "unevaluatedProperties",
381
+ }:
382
+ _validate_schema_references(document.get(key), path)
@@ -0,0 +1,20 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Package-specific errors with stable CLI exit classifications."""
5
+
6
+
7
+ class OarError(Exception):
8
+ """Base expected runner error."""
9
+
10
+
11
+ class ConfigurationError(OarError):
12
+ """Invalid configuration or invocation (exit code 2)."""
13
+
14
+
15
+ class ExecutionError(OarError):
16
+ """OpenShell or agent execution failure (exit code 1)."""
17
+
18
+
19
+ class ArtifactError(OarError):
20
+ """Missing or invalid required artifact (exit code 3)."""
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Bundled agent harnesses."""
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Pi coding-agent harness."""
@@ -0,0 +1,91 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Materialize the explicit native-upload runtime bundle for Pi."""
5
+
6
+ import shutil
7
+ import tempfile
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+ from openshell_agent_runner.config import (
12
+ MODELS_FILENAME,
13
+ SETTINGS_FILENAME,
14
+ ResolvedProfile,
15
+ )
16
+ from openshell_agent_runner.harnesses.resources import PreparedResources
17
+
18
+ SANDBOX_RUNTIME_ROOT = "/sandbox/oar-runtime"
19
+
20
+
21
+ def image_directory() -> Path:
22
+ return Path(str(files("openshell_agent_runner.harnesses.pi") / "runtime" / "image"))
23
+
24
+
25
+ def prepare_resources(resolved: ResolvedProfile, task_id: str) -> PreparedResources:
26
+ temporary = tempfile.TemporaryDirectory(prefix="oar-pi-")
27
+ runtime = Path(temporary.name) / "runtime"
28
+ (runtime / "skills").mkdir(parents=True, exist_ok=True)
29
+ (runtime / "extensions").mkdir(parents=True, exist_ok=True)
30
+ task = resolved.profile.tasks[task_id]
31
+ shutil.copy2(resolved.profile_dir / task.prompt, runtime / "prompt.md")
32
+ shutil.copy2(resolved.profile_dir / MODELS_FILENAME, runtime / MODELS_FILENAME)
33
+ shutil.copy2(resolved.profile_dir / SETTINGS_FILENAME, runtime / SETTINGS_FILENAME)
34
+ arguments = [
35
+ "--provider",
36
+ resolved.runtime.provider,
37
+ "--model",
38
+ resolved.runtime.model,
39
+ "--thinking",
40
+ resolved.runtime.thinking,
41
+ ]
42
+ tools = list(task.tools)
43
+ if task.output_schema is not None:
44
+ shutil.copy2(
45
+ resolved.profile_dir / task.output_schema, runtime / "output.schema.json"
46
+ )
47
+ submit_result = Path(
48
+ str(
49
+ files("openshell_agent_runner.harnesses.pi")
50
+ / "runtime"
51
+ / "extensions"
52
+ / "submit-result.ts"
53
+ )
54
+ )
55
+ shutil.copy2(submit_result, runtime / "extensions" / "oar-submit-result.ts")
56
+ tools.append("submit_result")
57
+ arguments.extend(
58
+ [
59
+ "--extension",
60
+ f"{SANDBOX_RUNTIME_ROOT}/extensions/oar-submit-result.ts",
61
+ ]
62
+ )
63
+ arguments.extend(["--tools", ",".join(tools)] if tools else ["--no-tools"])
64
+ for index, skill in enumerate(task.skills):
65
+ target = runtime / "skills" / f"{index:02d}-{skill.name}"
66
+ shutil.copytree(resolved.profile_dir / skill, target)
67
+ arguments.extend(["--skill", f"{SANDBOX_RUNTIME_ROOT}/skills/{target.name}"])
68
+ for index, extension in enumerate(task.extensions):
69
+ target = runtime / "extensions" / f"{index:02d}-{extension.name}"
70
+ shutil.copy2(resolved.profile_dir / extension, target)
71
+ arguments.extend(
72
+ ["--extension", f"{SANDBOX_RUNTIME_ROOT}/extensions/{target.name}"]
73
+ )
74
+ uploads = [
75
+ f"{runtime / 'prompt.md'}:{SANDBOX_RUNTIME_ROOT}/prompt.md",
76
+ f"{runtime / 'models.json'}:{SANDBOX_RUNTIME_ROOT}/models.json",
77
+ f"{runtime / 'settings.json'}:{SANDBOX_RUNTIME_ROOT}/settings.json",
78
+ ]
79
+ if task.output_schema is not None:
80
+ uploads.append(
81
+ f"{runtime / 'output.schema.json'}:{SANDBOX_RUNTIME_ROOT}/output.schema.json"
82
+ )
83
+ uploads.extend(
84
+ f"{path}:{SANDBOX_RUNTIME_ROOT}/skills"
85
+ for path in sorted((runtime / "skills").iterdir())
86
+ )
87
+ uploads.extend(
88
+ f"{path}:{SANDBOX_RUNTIME_ROOT}/extensions/{path.name}"
89
+ for path in sorted((runtime / "extensions").iterdir())
90
+ )
91
+ return PreparedResources(temporary, tuple(uploads), tuple(arguments))
@@ -0,0 +1,66 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
+
6
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import Ajv2020 from "ajv/dist/2020.js";
8
+ import { Type } from "typebox";
9
+
10
+ const runtimeRoot = process.env.OAR_RUNTIME_ROOT || "/sandbox/oar-runtime";
11
+ const schema = JSON.parse(
12
+ readFileSync(`${runtimeRoot}/output.schema.json`, "utf8"),
13
+ );
14
+ // Match Python jsonschema's Draft 2020-12 behavior: extension keywords and
15
+ // formats remain annotations, while standard structural keywords are enforced.
16
+ const validate = new Ajv2020({
17
+ allErrors: true,
18
+ strict: false,
19
+ validateFormats: false,
20
+ }).compile(schema);
21
+ const parameters = Type.Object({ result: Type.Unsafe(schema) });
22
+ const outputDirectory = "/sandbox/artifacts";
23
+ const outputPath = `${outputDirectory}/result`;
24
+
25
+ const submitResult = defineTool({
26
+ name: "submit_result",
27
+ label: "Submit Result",
28
+ description: "Validate and save the final task result.",
29
+ promptSnippet: "Submit the final result using the configured output schema",
30
+ promptGuidelines: [
31
+ "Call submit_result only when the task is complete.",
32
+ "Correct every validation error and call submit_result again if it is rejected.",
33
+ "Do not return the result as assistant text.",
34
+ ],
35
+ parameters,
36
+ async execute(_toolCallId, { result }) {
37
+ if (!validate(result)) {
38
+ const diagnostics = (validate.errors || [])
39
+ .slice(0, 12)
40
+ .map((error) => `${error.instancePath || "/"}: ${error.message || "invalid"}`)
41
+ .join("\n");
42
+ return {
43
+ content: [{ type: "text" as const, text: `Result rejected:\n${diagnostics}` }],
44
+ details: { accepted: false, diagnostics },
45
+ isError: true,
46
+ };
47
+ }
48
+
49
+ mkdirSync(outputDirectory, { recursive: true, mode: 0o700 });
50
+ const temporaryPath = `${outputPath}.tmp`;
51
+ writeFileSync(temporaryPath, `${JSON.stringify(result, null, 2)}\n`, {
52
+ encoding: "utf8",
53
+ mode: 0o600,
54
+ });
55
+ renameSync(temporaryPath, outputPath);
56
+ return {
57
+ content: [{ type: "text" as const, text: "Result accepted." }],
58
+ details: { accepted: true, outputPath },
59
+ terminate: true,
60
+ };
61
+ },
62
+ });
63
+
64
+ export default function (pi: ExtensionAPI) {
65
+ pi.registerTool(submitResult);
66
+ }
@@ -0,0 +1,28 @@
1
+ FROM node:24-bookworm-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03
2
+
3
+ ARG PI_VERSION=0.84.2
4
+ ARG AJV_VERSION=8.20.0
5
+ ARG TYPEBOX_VERSION=1.3.16
6
+
7
+ ENV NODE_PATH=/usr/local/lib/node_modules
8
+
9
+ RUN apt-get update \
10
+ && apt-get install --yes --no-install-recommends ca-certificates git iproute2 python3 ripgrep \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ RUN npm install --global --ignore-scripts \
14
+ "@earendil-works/pi-coding-agent@${PI_VERSION}" \
15
+ "ajv@${AJV_VERSION}" \
16
+ "typebox@${TYPEBOX_VERSION}" \
17
+ && npm cache clean --force >/dev/null 2>&1 \
18
+ && test "$(pi --version)" = "${PI_VERSION}"
19
+
20
+ RUN mkdir -p /opt/oar/pi /sandbox/artifacts /sandbox/tmp /workspace \
21
+ && chown -R node:node /sandbox /workspace
22
+
23
+ COPY exec.sh /opt/oar/pi/exec.sh
24
+ RUN chmod 0755 /opt/oar/pi/exec.sh \
25
+ && chmod -R a+rX,a-w /opt/oar
26
+
27
+ WORKDIR /sandbox
28
+ USER node
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ set -euo pipefail
6
+ umask 077
7
+
8
+ model_id=""
9
+ arguments=("$@")
10
+ for ((index = 0; index < ${#arguments[@]}; index++)); do
11
+ if [[ "${arguments[$index]}" == "--model" && $((index + 1)) -lt ${#arguments[@]} ]]; then
12
+ model_id="${arguments[$((index + 1))]}"
13
+ break
14
+ fi
15
+ done
16
+ if [[ ! "$model_id" =~ ^[A-Za-z0-9._:/-]{1,256}$ ]]; then
17
+ echo "Pi harness: --model is missing or invalid" >&2
18
+ exit 2
19
+ fi
20
+
21
+ payload=${OAR_RUNTIME_ROOT:-/sandbox/oar-runtime}
22
+ for required in "$payload/prompt.md" "$payload/models.json" "$payload/settings.json"; do
23
+ if [[ ! -f "$required" ]]; then
24
+ echo "Pi harness: missing required file: $required" >&2
25
+ exit 2
26
+ fi
27
+ done
28
+
29
+ pi_home=/sandbox/pi-home
30
+ mkdir -p "$pi_home/.pi/agent" /sandbox/artifacts /sandbox/tmp
31
+ install -m 0600 "$payload/models.json" "$pi_home/.pi/agent/models.json"
32
+ install -m 0600 "$payload/settings.json" "$pi_home/.pi/agent/settings.json"
33
+
34
+ export HOME="$pi_home"
35
+ export TMPDIR=/sandbox/tmp
36
+ export PI_OFFLINE=1
37
+ export PI_SKIP_VERSION_CHECK=1
38
+ export PI_TELEMETRY=0
39
+ export OAR_MODEL_ID="$model_id"
40
+
41
+ agent_workdir=${REPOSITORY_ROOT:-/sandbox}
42
+ if [[ ! -d "$agent_workdir" ]]; then
43
+ echo "Pi harness: REPOSITORY_ROOT is not a directory: $agent_workdir" >&2
44
+ exit 2
45
+ fi
46
+ cd "$agent_workdir"
47
+
48
+ stdout_path=/sandbox/artifacts/result.stdout
49
+ result_path=/sandbox/artifacts/result
50
+ pi \
51
+ --print \
52
+ --no-session \
53
+ --no-extensions \
54
+ --no-skills \
55
+ --no-prompt-templates \
56
+ --no-themes \
57
+ --no-context-files \
58
+ --no-approve \
59
+ --offline \
60
+ "$@" \
61
+ <"$payload/prompt.md" \
62
+ >"$stdout_path"
63
+
64
+ if [[ -s "$result_path" ]]; then
65
+ rm -f "$stdout_path"
66
+ else
67
+ mv "$stdout_path" "$result_path"
68
+ fi
@@ -0,0 +1,17 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Shared resources prepared by an agent harness."""
5
+
6
+ import tempfile
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class PreparedResources:
12
+ temporary: tempfile.TemporaryDirectory[str]
13
+ uploads: tuple[str, ...]
14
+ arguments: tuple[str, ...]
15
+
16
+ def close(self) -> None:
17
+ self.temporary.cleanup()