Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ adapters in Harbor tasks. Harbor options select the model, harness, skills, MCP
servers, tool policy, and telemetry; `FabricAgent` translates them into one
typed `FabricConfig` for the task run.

Harbor's `skills_dir` is a task-side collection containing `<skill_name>/SKILL.md` directories. The task runner expands its immediate entries into individual NeMo Fabric skill paths in name order, preserving explicit `config.skills.paths` and avoiding duplicate paths. An empty collection adds no skills. Missing collections and entries without a regular `SKILL.md` file fail before harness execution; skill contents are still validated by the selected adapter. Collection paths are resolved inside the task environment, relative to `fabric_config_base_dir` when not absolute, never through the host filesystem. Use matching NeMo Fabric versions on the host and in the task environment for this transport contract.

Refer to the [Harbor example](../../../../../../../examples/harbor/README.md)
for runnable SWE-Bench commands, configuration variations, reward checks, and
Relay artifacts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ def _build_spec(self, instruction: str) -> FabricRunPayload:
return FabricRunPayload(
config=config,
config_base_dir=self._environment_config_base_dir,
skills_dir=self.skills_dir,
request=self._build_request(instruction),
environment_env_names=tuple(self.fabric_environment_env),
)
Expand All @@ -531,7 +532,6 @@ def _build_config(self) -> FabricConfig:
enabled_tools=self.fabric_enabled_tools,
telemetry=self.fabric_telemetry,
model_name=self.model_name,
skills_dir=self.skills_dir,
mcp_servers=tuple(
HarborMcpServer.model_validate(server.model_dump(mode="python"))
for server in self.mcp_servers
Expand Down Expand Up @@ -623,7 +623,6 @@ def build_harbor_config(
enabled_tools: list[str] | None = None,
telemetry: Literal["none", "relay"] = "none",
model_name: str | None = None,
skills_dir: str | Path | None = None,
mcp_servers: tuple[HarborMcpServer, ...] = (),
discovery_paths: tuple[str | Path, ...] = (),
) -> FabricConfig:
Expand Down Expand Up @@ -713,8 +712,6 @@ def build_harbor_config(
url=cast(str, server.url),
exposure="harness_native",
)
if skills_dir is not None:
config.add_skill_path(skills_dir)
if telemetry == "relay":
relay_output = f"{artifact_root}/relay"
config.enable_relay(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class FabricRunPayload(BaseModel):
config: FabricConfig
config_base_dir: PurePosixPath
logs_dir: PurePosixPath = PurePosixPath("/logs/agent")
skills_dir: PurePosixPath | None = None
request: RunRequest
environment_env_names: tuple[str, ...] = ()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path

from nemo_fabric import Fabric
from nemo_fabric import FabricConfigError
from nemo_fabric import FabricError
from nemo_fabric import RunResult
from nemo_fabric.integrations.harbor.models import FabricRunPayload
Expand All @@ -26,6 +27,41 @@ async def run(payload: FabricRunPayload) -> RunResult:
if name not in os.environ:
raise ValueError(f"Harbor runner environment variable {name} is not set")
config.environment.env[name] = os.environ[name]
if payload.skills_dir is not None:
base_dir = Path(payload.config_base_dir).resolve()
root = Path(payload.skills_dir)
if not root.is_absolute():
root = base_dir / root
if not root.is_dir():
raise FabricConfigError(
f"Harbor skills collection must be an existing task-side directory: {root}",
stage="configuration",
code="harbor_skills_invalid",
)
skills = sorted(root.iterdir())
for skill in skills:
skill_file = skill / "SKILL.md"
if (
not skill.is_dir()
or skill_file.is_symlink()
or not skill_file.is_file()
):
raise FabricConfigError(
"Harbor skills collection entries must be directories "
f"containing a regular SKILL.md file: {skill}",
stage="configuration",
code="harbor_skills_invalid",
)
existing_paths = (
{(base_dir / path).resolve() for path in config.skills.paths}
if config.skills is not None
else set()
)
for skill in skills:
resolved = skill.resolve()
if resolved not in existing_paths:
config.add_skill_path(skill)
existing_paths.add(resolved)
result = await Fabric().run(
config,
base_dir=payload.config_base_dir,
Expand Down
11 changes: 4 additions & 7 deletions tests/integrations/test_harbor_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,14 @@ def load_codex_adapter():
return adapter


def test_harbor_builder_constructs_complete_config_from_harbor_inputs(tmp_path):
def test_harbor_builder_constructs_complete_config_from_harbor_inputs():
from nemo_fabric.integrations.harbor.fabric_agent import build_harbor_config
from nemo_fabric.integrations.harbor.models import HarborMcpServer

config = build_harbor_config(
adapter_id="demo.fabric.smoke",
workspace="/testbed",
model_name="openai/gpt-5.4",
skills_dir=tmp_path / "skills",
mcp_servers=(
HarborMcpServer(
name="remote",
Expand All @@ -85,8 +84,7 @@ def test_harbor_builder_constructs_complete_config_from_harbor_inputs(tmp_path):
assert config.mcp.servers["local"].url == "mcp-server"
assert config.mcp.servers["local"].args == ["--stdio"]
assert "args" not in config.mcp.servers["local"].extra_fields
assert config.skills is not None
assert config.skills.paths == [str(tmp_path / "skills")]
assert config.skills is None
assert (
json.loads(json.dumps(config.to_mapping()))["metadata"]["name"]
== "harbor-smoke"
Expand Down Expand Up @@ -135,6 +133,7 @@ def test_harbor_transport_models_validate_mcp_targets():
"config",
"config_base_dir",
"logs_dir",
"skills_dir",
"request",
"environment_env_names",
}
Expand Down Expand Up @@ -552,7 +551,6 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path
workspace="/testbed",
telemetry="relay",
model_name="nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
skills_dir="/harbor/skills",
mcp_servers=tuple(
HarborMcpServer.model_validate(server.model_dump(mode="python"))
for server in load_mcp_servers(SWEBENCH_MCP_CONFIG)
Expand Down Expand Up @@ -586,8 +584,7 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path
assert (
relay.models["default"].model == "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
)
assert relay.skills is not None
assert relay.skills.paths == ["/harbor/skills"]
assert relay.skills is None
assert relay.mcp is not None
assert set(relay.mcp.servers) == {"fabric-repo-inspector"}
assert relay.mcp.servers["fabric-repo-inspector"].args == [
Expand Down
Loading
Loading