Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3560777
Harden Harbor integration and add SWE-Bench workflow
AnuradhaKaruppiah Jul 13, 2026
60aefd4
fix Harbor SWE-Bench task bootstrap
AnuradhaKaruppiah Jul 14, 2026
b4875f0
fix Harbor telemetry validation across environments
AnuradhaKaruppiah Jul 14, 2026
60e288e
document direct Relay telemetry contract
AnuradhaKaruppiah Jul 14, 2026
80bb851
fix Harbor live qualification issues
AnuradhaKaruppiah Jul 14, 2026
43adda5
align Harbor examples with typed Relay config
AnuradhaKaruppiah Jul 14, 2026
0525b5e
fix rebased Harbor file endings
AnuradhaKaruppiah Jul 14, 2026
45654a8
Merge upstream main and qualify Claude and Hermes configs
AnuradhaKaruppiah Jul 15, 2026
05a8a76
qualify Claude and Hermes Harbor runs
AnuradhaKaruppiah Jul 15, 2026
9667c60
fix(harbor): address integration review findings
AnuradhaKaruppiah Jul 15, 2026
b1dbfaf
fix(harbor): use MCP protocol version error code
AnuradhaKaruppiah Jul 15, 2026
fd99581
fix(harbor): address review feedback
AnuradhaKaruppiah Jul 15, 2026
cc5d4fe
fix(harbor): close remaining review gaps
AnuradhaKaruppiah Jul 15, 2026
60b3d63
Merge upstream/main into ak-harbor-cleanup
AnuradhaKaruppiah Jul 15, 2026
d238e29
docs(harbor): mark local descriptors temporary
AnuradhaKaruppiah Jul 15, 2026
67d6098
refactor(harbor): address example review feedback
AnuradhaKaruppiah Jul 15, 2026
321167e
refactor(harbor): use typed config factories
AnuradhaKaruppiah Jul 16, 2026
a409d8b
Translate Harbor options into FabricConfig
AnuradhaKaruppiah Jul 16, 2026
6b60485
Make Harbor README reproducible
AnuradhaKaruppiah Jul 16, 2026
50616a3
Mark Harbor bootstrap as temporary
AnuradhaKaruppiah Jul 16, 2026
c04647c
Store Harbor workspace patches in LFS
AnuradhaKaruppiah Jul 16, 2026
cf089cf
Document Harbor host and task architecture
AnuradhaKaruppiah Jul 16, 2026
29b256d
Build Harbor FabricConfig on the host
AnuradhaKaruppiah Jul 16, 2026
b71df6a
Drive calculator demo through Harbor inputs
AnuradhaKaruppiah Jul 16, 2026
85afad6
Clarify public Harbor documentation
AnuradhaKaruppiah Jul 16, 2026
ddbaedf
Improve Harbor architecture diagram readability
AnuradhaKaruppiah Jul 16, 2026
21819f3
Clarify Harbor diagram semantics
AnuradhaKaruppiah Jul 16, 2026
7ac47be
Emphasize Harbor process boundaries
AnuradhaKaruppiah Jul 16, 2026
bbbcc4a
Rename Harbor demo to calculator smoke test
AnuradhaKaruppiah Jul 16, 2026
a6e7e62
Split Harbor guides by workflow
AnuradhaKaruppiah Jul 16, 2026
ed8aa2a
Present Harbor examples as walkthroughs
AnuradhaKaruppiah Jul 16, 2026
576a428
Expand Harbor examples overview
AnuradhaKaruppiah Jul 16, 2026
7141f1f
Clarify calculator walkthrough modes
AnuradhaKaruppiah Jul 16, 2026
7ac3659
chore(harbor): store sample artifacts without LFS
AnuradhaKaruppiah Jul 16, 2026
ac080cb
Merge main and fix Harbor post-run lifecycle
AjayThorve Jul 16, 2026
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
2,701 changes: 4 additions & 2,697 deletions ATTRIBUTIONS-Python.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ the [Python SDK guide](docs/sdk/python.mdx). Exact signatures are in the
diagnostics, requests, multi-turn runtimes, parallelism, results, and errors.
- [Getting Started overview](docs/getting-started/overview.mdx): interface
selection and the end-to-end Fabric workflow.
- [Harbor example](examples/harbor/README.md) and
[multi-harness demo](examples/harbor/demo/README.md): ownership,
installation, and complete command matrices.
- [Harbor examples](examples/harbor/README.md): validate the integration with a
deterministic, credential-free calculator smoke, optionally run the same
task with Hermes or Claude, and evaluate real coding tasks with SWE-Bench.
- Adapter guides: [Hermes](adapters/hermes/README.md),
[Codex SDK](adapters/codex/README.md), and
[Deep Agents](adapters/deepagents/README.md).
Expand Down
195 changes: 125 additions & 70 deletions adapters/claude/src/nemo_fabric_adapters/claude/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os
import shlex
import shutil
import subprocess
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import is_dataclass
Expand Down Expand Up @@ -287,8 +288,12 @@ def _native_skill_paths(payload: dict[str, Any]) -> list[Path]:
or {}
)
values = _mapping(native, name="capability_plan.native").get("skill_paths") or []
if not isinstance(values, list) or any(not isinstance(value, (str, Path)) for value in values):
raise AdapterConfigError("claude_invalid_configuration", "native skill_paths must be a list of paths")
if not isinstance(values, list) or any(
not isinstance(value, (str, Path)) for value in values
):
raise AdapterConfigError(
"claude_invalid_configuration", "native skill_paths must be a list of paths"
)
return [_resolve_path(payload, value) for value in values]


Expand Down Expand Up @@ -552,10 +557,14 @@ def load_claude_session_id(
raise ValueError("missing Claude session")
return session_id
except (OSError, ValueError, json.JSONDecodeError) as error:
raise AdapterStateError("claude_invalid_runtime_state", "Claude runtime state is invalid") from error
raise AdapterStateError(
"claude_invalid_runtime_state", "Claude runtime state is invalid"
) from error


def save_claude_session_id(payload: dict[str, Any], fabric_runtime_id: str, claude_session_id: str) -> None:
def save_claude_session_id(
payload: dict[str, Any], fabric_runtime_id: str, claude_session_id: str
) -> None:
if not claude_session_id:
raise AdapterStateError(
"claude_invalid_runtime_state", "Claude session ID is missing"
Expand Down Expand Up @@ -602,7 +611,9 @@ def _result_failed(result: ResultMessage) -> bool:
)


def normalize_result(payload: dict[str, Any], messages: list[Message], result: ResultMessage) -> dict[str, Any]:
def normalize_result(
payload: dict[str, Any], messages: list[Message], result: ResultMessage
) -> dict[str, Any]:
del payload
failed = _result_failed(result)
error = None
Expand Down Expand Up @@ -718,6 +729,95 @@ def _relay_output(
return output


def _start_relay_gateway(
payload: dict[str, Any],
relay: ClaudeRelaySettings | None,
) -> subprocess.Popen[Any] | None:
if relay is None:
return None
try:
return relay_gateway.start_relay_gateway(
launch=relay.gateway,
cwd=resolve_cwd(payload),
)
except relay_gateway.RelayGatewayError as error:
raise AdapterRelayError(
"claude_relay_start_failed",
"NeMo Relay gateway failed to start",
metadata={"gateway_log_path": str(relay.gateway.log_path)},
) from error


def _cleanup_relay(
relay: ClaudeRelaySettings | None,
gateway_process: subprocess.Popen[Any] | None,
) -> AdapterRelayError | None:
cleanup_error: AdapterRelayError | None = None
if gateway_process is not None:
try:
relay_gateway.stop_relay_gateway(gateway_process)
except relay_gateway.RelayGatewayError:
cleanup_error = AdapterRelayError(
"claude_relay_stop_failed",
"NeMo Relay gateway failed to stop",
metadata={
"gateway_log_path": str(relay.gateway.log_path)
if relay is not None
else ""
},
)
if relay is not None and relay.plugin_path.exists():
try:
shutil.rmtree(relay.plugin_path)
except OSError:
if cleanup_error is None:
cleanup_error = AdapterRelayError(
"claude_relay_cleanup_failed",
"Claude Relay hook configuration could not be removed",
)
return cleanup_error


def _merge_relay_output(
output: dict[str, Any],
relay: ClaudeRelaySettings | None,
cleanup_error: AdapterRelayError | None,
) -> dict[str, Any]:
if relay is None:
return output
output = _relay_output(output, relay)
if cleanup_error is None:
return output
cleanup: dict[str, Any] = {
"code": cleanup_error.code,
"message": cleanup_error.message,
"retryable": False,
}
if cleanup_error.metadata:
cleanup["metadata"] = cleanup_error.metadata
output["relay_runtime"]["cleanup_error"] = cleanup
if not output["failed"]:
output["completed"] = False
output["failed"] = True
output["error"] = cleanup
return output


def _persist_result_session(
payload: dict[str, Any],
fabric_runtime_id: str,
prior_session_id: str | None,
result: ResultMessage,
) -> dict[str, Any] | None:
if prior_session_id is not None and result.session_id != prior_session_id:
return _failure(
"claude_session_mismatch",
"Claude session identity changed during resume",
)
save_claude_session_id(payload, fabric_runtime_id, result.session_id)
return None


async def run_claude(payload: dict[str, Any]) -> dict[str, Any]:
fabric_runtime_id = runtime_id(payload)
prior_session_id = load_claude_session_id(payload, fabric_runtime_id)
Expand All @@ -727,18 +827,7 @@ async def run_claude(payload: dict[str, Any]) -> dict[str, Any]:
messages: list[Message] = []
result: ResultMessage | None = None
try:
if relay is not None:
try:
gateway_process = relay_gateway.start_relay_gateway(
launch=relay.gateway,
cwd=resolve_cwd(payload),
)
except relay_gateway.RelayGatewayError as error:
raise AdapterRelayError(
"claude_relay_start_failed",
"NeMo Relay gateway failed to start",
metadata={"gateway_log_path": str(relay.gateway.log_path)},
) from error
gateway_process = _start_relay_gateway(payload, relay)
options = build_options(payload, resume=prior_session_id, relay=relay)
try:
async with asyncio.timeout(timeout_seconds(payload)):
Expand Down Expand Up @@ -767,59 +856,19 @@ async def run_claude(payload: dict[str, Any]) -> dict[str, Any]:
else:
output = normalize_result(payload, messages, result)
if not output["failed"]:
if (
prior_session_id is not None
and result.session_id != prior_session_id
):
output = _failure(
"claude_session_mismatch",
"Claude session identity changed during resume",
output = (
_persist_result_session(
payload,
fabric_runtime_id,
prior_session_id,
result,
)
else:
save_claude_session_id(
payload, fabric_runtime_id, result.session_id
)
finally:
if gateway_process is not None:
try:
relay_gateway.stop_relay_gateway(gateway_process)
except relay_gateway.RelayGatewayError:
cleanup_error = AdapterRelayError(
"claude_relay_stop_failed",
"NeMo Relay gateway failed to stop",
metadata={
"gateway_log_path": str(relay.gateway.log_path)
if relay is not None
else ""
},
)
if relay is not None and relay.plugin_path.exists():
try:
shutil.rmtree(relay.plugin_path)
except OSError:
if cleanup_error is None:
cleanup_error = AdapterRelayError(
"claude_relay_cleanup_failed",
"Claude Relay hook configuration could not be removed",
or output
)
finally:
cleanup_error = _cleanup_relay(relay, gateway_process)

if relay is not None:
output = _relay_output(output, relay)
if cleanup_error is not None:
cleanup: dict[str, Any] = {
"code": cleanup_error.code,
"message": cleanup_error.message,
"retryable": False,
}
if cleanup_error.metadata:
cleanup["metadata"] = cleanup_error.metadata
output["relay_runtime"]["cleanup_error"] = cleanup
if not output["failed"]:
output["completed"] = False
output["failed"] = True
output["error"] = cleanup

return output
return _merge_relay_output(output, relay, cleanup_error)


def run(payload: dict[str, Any]) -> dict[str, Any]:
Expand All @@ -830,14 +879,20 @@ def run(payload: dict[str, Any]) -> dict[str, Any]:
except ClaudeAdapterError as error:
return adapter_failure(error)
except Exception: # Adapter boundary must always return normalized JSON.
return _failure("claude_adapter_internal_error", "Claude adapter failed unexpectedly")
return _failure(
"claude_adapter_internal_error", "Claude adapter failed unexpectedly"
)


def main() -> None:
try:
payload = common_utils.load_payload()
except Exception: # Malformed invocation input must still satisfy the process contract.
output = _failure("claude_adapter_internal_error", "Claude adapter failed unexpectedly")
except (
Exception
): # Malformed invocation input must still satisfy the process contract.
output = _failure(
"claude_adapter_internal_error", "Claude adapter failed unexpectedly"
)
else:
output = run(payload)
print(json.dumps(output, sort_keys=True))
Expand Down
1 change: 1 addition & 0 deletions adapters/hermes/fabric-adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"config": {
"accepts": [
"models",
"tools",
"tools.blocked",
"mcp",
Expand Down
10 changes: 10 additions & 0 deletions adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def hermes_mcp_server_config(server: dict[str, Any]) -> dict[str, Any]:
if not target:
raise ValueError("MCP server mapping requires a URL")

if transport == "stdio":
return common_utils.without_none(
{
"enabled": True,
"command": target,
"args": common_utils.normalize_list(server.get("args")) or None,
"env": server.get("env"),
}
)

return {"enabled": True, "url": target, "transport": transport}


Expand Down
Loading
Loading