Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
40e6e9f
feat: add explicit environment lifecycle
AnuradhaKaruppiah Sep 2, 2026
0b7e5f5
feat: run adapters in OpenShell capsules
AnuradhaKaruppiah Sep 2, 2026
b878b36
feat: complete OpenShell LangGraph vertical slice
AnuradhaKaruppiah Sep 4, 2026
b8614ac
docs: fix OpenShell POC sequence diagram
AnuradhaKaruppiah Sep 4, 2026
5f25a56
feat: add OpenShell Rust SDK provider
AnuradhaKaruppiah Sep 5, 2026
ec7668a
feat: attach caller-owned environments
AnuradhaKaruppiah Sep 5, 2026
6e981e1
test: run LangGraph POC against stock OpenShell
AnuradhaKaruppiah Sep 5, 2026
d629548
docs: clarify OpenShell runtime architecture
AnuradhaKaruppiah Sep 7, 2026
8b04110
refactor: clarify OpenShell runtime control boundaries
AnuradhaKaruppiah Sep 7, 2026
b37feef
demo: use published OpenShell release artifacts
AnuradhaKaruppiah Sep 7, 2026
1c0282b
perf: reuse the OpenShell provider process
AnuradhaKaruppiah Sep 7, 2026
4571528
chore: normalize provider copyright headers
AnuradhaKaruppiah Sep 7, 2026
41c81a1
refactor: present OpenShell support as experimental
AnuradhaKaruppiah Sep 8, 2026
9f9408a
docs: clarify OpenShell lifecycle responsibilities
AnuradhaKaruppiah Sep 8, 2026
5a55046
chore: keep runtime control private
AnuradhaKaruppiah Sep 8, 2026
70e1e24
docs: clarify OpenShell provider architecture
AnuradhaKaruppiah Sep 15, 2026
5395e44
fix: harden OpenShell environment lifecycle
AnuradhaKaruppiah Sep 15, 2026
384a51c
Merge upstream main into feat/fabric-openshell-poc
AnuradhaKaruppiah Oct 9, 2026
96137d6
feat: add OpenClaw code review in OpenShell
AnuradhaKaruppiah Oct 11, 2026
f1807a9
docs: explain OpenShell demo function flow
AnuradhaKaruppiah Oct 11, 2026
bcdaafc
docs: clarify and render OpenShell demo flow
AnuradhaKaruppiah Oct 12, 2026
622c2bb
docs: simplify OpenShell quickstart and merge flow
AnuradhaKaruppiah Oct 12, 2026
ae095fa
docs: remove redundant quickstart diagram
AnuradhaKaruppiah Oct 12, 2026
b604b57
docs: replace OpenShell Mermaid with editable Excalidraw
AnuradhaKaruppiah Oct 12, 2026
659a8eb
feat: run Hermes code reviews inside OpenShell
AnuradhaKaruppiah Oct 12, 2026
6f3900f
docs: use reviewed OpenShell diagrams from assets
AnuradhaKaruppiah Oct 12, 2026
04c931c
feat(openshell): unify model access and collect Hermes Relay telemetry
AnuradhaKaruppiah Oct 12, 2026
20f1d10
docs(openshell): remove Model A terminology from READMEs
AnuradhaKaruppiah Oct 12, 2026
80cc157
refactor(examples): share OpenShell launcher
AnuradhaKaruppiah Oct 12, 2026
33fd634
feat(examples): make OpenShell launcher workload independent
AnuradhaKaruppiah Oct 13, 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
43,080 changes: 37,135 additions & 5,945 deletions ATTRIBUTIONS-Rust.md

Large diffs are not rendered by default.

3,077 changes: 2,654 additions & 423 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

[workspace]
members = [
"crates/fabric-runtime-control",
"crates/fabric-core",
"crates/fabric-cli",
"crates/fabric-python",
"environment-providers/openshell",
]
default-members = [
"crates/fabric-core",
Expand All @@ -16,6 +18,7 @@ resolver = "2"
[workspace.package]
version = "0.5.0"
edition = "2024"
rust-version = "1.88"
license = "Apache-2.0"
repository = "https://github.com/NVIDIA/nemo-fabric"

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ NeMo Fabric separates consumers from execution targets through a stable, version

- **Expand the harness ecosystem:** Add high-priority first- and third-party harnesses through the normalized lifecycle and capability contracts.
- **Sandbox-native execution:** Run NeMo Fabric in secure environments through providers such as [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview).
Try the [OpenClaw and Hermes code-review POC](examples/code_review_agent/openshell/README.md) for a two-turn agent loop executing inside an OpenShell sandbox.
- **Deepen NVIDIA ecosystem integrations:** Make NeMo Fabric a standard execution boundary for agent configuration, evaluation, rollouts, artifacts, and telemetry.
- **Enhance session lifecycle management:** Add explicit cold-start, resume, and fork operations with stable session identifiers and capability-aware errors.
- **Compose subagents:** Configure supported subagents through validated, portable NeMo Fabric configuration.
2 changes: 2 additions & 0 deletions adapters/python/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Use the maintained
for adapter and configuration guidance. Source code is available in the
[NVIDIA NeMo Fabric repository](https://github.com/NVIDIA/NeMo-Fabric/).

For OpenShell Relay execution, `load_relay_plugin_config(payload)` materializes the runtime's inline configuration inside the task artifact root and maps file outputs to a runtime directory there. The Hermes adapter declares gzip snapshots in `AgentRunResult.artifacts`; the environment provider collects these into host paths. Local Relay execution continues to use `FABRIC_RELAY_CONFIG_PATH`.

## Persistent Local Hosts

Every local Process or Python adapter implements the ordered persistent-host
Expand Down
22 changes: 20 additions & 2 deletions adapters/python/common/src/nemo_fabric_adapters/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,21 @@ def validate_relay_observability_v3(plugin_config: dict[str, Any]) -> None:


def load_relay_plugin_config(payload: dict[str, Any]) -> dict[str, Any]:
context = runtime_context(payload)
metadata = (context.get("telemetry") or {}).get("metadata") or {}
if "relay_config" in metadata:
# The host cannot stage files on the task filesystem. Materialize the
# inline remote declaration without carrying host output paths across.
root = Path(context["artifacts"]["root"]).resolve()
config_path = (
root / ".fabric" / str(context["runtime_id"]) / "relay-config.json"
)
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(
json.dumps({"relay": {"config": metadata["relay_config"]}}),
encoding="utf-8",
)
os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(config_path)
config_path = os.environ.get("FABRIC_RELAY_CONFIG_PATH")
if not config_path:
raise RuntimeError("FABRIC_RELAY_CONFIG_PATH is required when Relay is enabled")
Expand Down Expand Up @@ -571,6 +586,9 @@ def normalize_relay_output_dirs(

base = Path(base_dir(payload)).resolve()
runtime_id = runtime_context(payload)["runtime_id"]
context = runtime_context(payload)
remote = "relay_config" in ((context.get("telemetry") or {}).get("metadata") or {})
remote_root = Path(context["artifacts"]["root"]) / "relay" if remote else None
for component in plugin_config.get("components", []):
if not isinstance(component, dict) or component.get("kind") != "observability":
continue
Expand All @@ -588,7 +606,7 @@ def normalize_relay_output_dirs(
path = base / path
else:
path = base / "artifacts" / "relay"
sink["output_directory"] = str(path / str(runtime_id))
sink["output_directory"] = str((remote_root or path) / str(runtime_id))
Path(sink["output_directory"]).mkdir(parents=True, exist_ok=True)
sink.setdefault("filename", "events.atof.jsonl")
sink.setdefault("mode", "overwrite")
Expand All @@ -604,7 +622,7 @@ def normalize_relay_output_dirs(
else:
path = base / "artifacts" / "relay"

atif["output_directory"] = str(path / str(runtime_id))
atif["output_directory"] = str((remote_root or path) / str(runtime_id))
Path(atif["output_directory"]).mkdir(parents=True, exist_ok=True)
atif.setdefault("filename_template", "trajectory-{session_id}.atif.json")
atif.setdefault("agent_name", agent_name(payload))
Expand Down
6 changes: 3 additions & 3 deletions adapters/python/hermes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0

# NVIDIA NeMo Fabric Hermes Agent Adapter

This adapter runs Hermes Agent through its Python SDK.
This adapter runs Hermes Agent through its Python SDK. The [code-review OpenShell POC](../../../examples/code_review_agent/openshell/README.md) packages this same adapter and agent loop inside a sandbox, with generated model access, task-side preflight, and optional collected Relay telemetry.

## Install

Expand Down Expand Up @@ -112,13 +112,13 @@ runtime.

## Execution Model

Each NeMo Fabric runtime starts one local adapter host, constructs one Hermes Agent
Each NeMo Fabric runtime starts one adapter host in the selected environment, constructs one Hermes Agent
`AIAgent`, and opens one `SessionDB`. Ordered `Runtime.invoke(...)` calls reuse
those native objects and pass the prior turn's returned transcript back to
`run_conversation(...)`. Runtime stop calls the agent's idempotent `close()`
method, closes the session database, and releases the Relay plugin context when
enabled. Relay subscriber work is flushed after each invocation so ATIF output
is complete before the result returns.
is complete before the result returns. In OpenShell, the adapter declares separate gzip ATIF/ATOF snapshots for each invocation; the environment provider copies them into the host artifact manifest before sandbox cleanup.

## Maintaining The Adapter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(self) -> None:
self._agent: Any = None
self._relay_plugin_config: dict[str, Any] | None = None
self._relay_plugin_config_path: Path | None = None
self._relay_artifact_versions: dict[str, tuple[int, int]] = {}
self._active_invoke_task: asyncio.Task[tuple[dict[str, Any], str]] | None = None

async def start(self, payload: dict[str, Any]) -> None:
Expand Down Expand Up @@ -372,6 +373,13 @@ def clear_active_invoke_task(
status=AgentRunStatus.FAILED if failed else AgentRunStatus.SUCCEEDED,
output=output,
error=error,
artifacts=telemetry.snapshot_remote_relay_artifacts(
self._relay_plugin_config,
runtime_context,
self._relay_artifact_versions,
)
if self._relay_plugin_config is not None
else [],
)

async def _authenticate_mcp_servers(self) -> None:
Expand Down Expand Up @@ -505,6 +513,7 @@ async def stop(self) -> None:
self._conversation_history = None
self._relay_plugin_config = None
self._relay_plugin_config_path = None
self._relay_artifact_versions = {}
self._started = False

if had_relay_plugin:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

from __future__ import annotations

import gzip
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version
from pathlib import Path
import shutil
from typing import Any

from nemo_fabric_adapter_contract.models import RuntimeContext
from nemo_fabric_adapter_contract.models import AgentArtifact, RuntimeContext
import nemo_fabric_adapters.common.utils as common_utils


Expand All @@ -23,6 +25,43 @@
)


def snapshot_remote_relay_artifacts(plugin_config, context: RuntimeContext, versions):
"""Declare immutable per-invocation snapshots for provider collection."""
if not context.telemetry or "relay_config" not in context.telemetry.metadata:
return []
root = Path(context.artifacts.root).resolve()
destination = (
root / "relay-invocations" / context.runtime_id / context.invocation_id
)
declared = []
for artifact in common_utils.collect_relay_artifacts(plugin_config):
source = Path(artifact["path"]).resolve()
if not source.is_relative_to(root):
raise ValueError(
"remote Relay artifact must remain inside the task artifact root"
)
stat = source.stat()
version = (stat.st_mtime_ns, stat.st_size)
if artifact["kind"] == "atif" and versions.get(str(source)) == version:
continue
destination.mkdir(parents=True, exist_ok=True)
# Native trajectories can exceed the provider's per-file byte bound.
# Compress complete snapshots rather than truncate evidence or widen it.
target = destination / (source.name + ".gz")
with source.open("rb") as stream, gzip.open(target, "wb") as snapshot:
shutil.copyfileobj(stream, snapshot)
versions[str(source)] = version
declared.append(
AgentArtifact(
name=source.name + ".gz",
kind=artifact["kind"],
path=target.relative_to(root),
media_type="application/gzip",
)
)
return declared


def finalize_hermes_relay_session(session_id: str) -> None:
"""Finalize one Relay session through the installed Hermes lifecycle API."""
try:
Expand Down
2 changes: 2 additions & 0 deletions adapters/python/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ SPDX-License-Identifier: Apache-2.0

# NVIDIA NeMo Fabric OpenClaw Adapter

For managed execution with the agent loop and tools inside an OpenShell sandbox, see the [code-review OpenShell POC](../../../examples/code_review_agent/openshell/README.md).

This adapter supports two lifecycle shapes:

- Managed runtime: Each NeMo Fabric runtime owns an isolated local OpenClaw
Expand Down
2 changes: 1 addition & 1 deletion adapters/typescript/kilo/test/configuration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test("rejects ambiguous, unsafe, and unsupported model configuration", () => {
assert.throws(() => selectModel({models: {a: {provider: "x", model: "a", api_key_env: "KEY"}, b: {provider: "x", model: "b", api_key_env: "KEY"}}}), (error) => error.code === "kilo_model_ambiguous");
assert.throws(() => selectModel({models: {default: {provider: "x", model: "a", api_key_env: "BAD-KEY"}}}), (error) => error.code === "kilo_invalid_model");
assert.throws(() => selectModel({models: {default: {provider: "x", model: "a", api_key_env: "KEY", base_url: "http://example.com/v1"}}}), (error) => error.code === "kilo_invalid_model");
assert.equal(selectModel({harness: {settings: {allow_insecure_http_model_endpoint: true}}, models: {default: {provider: "x", model: "a", api_key_env: "KEY", base_url: "http://10.86.19.10:8000/v1"}}}).baseUrl, "http://10.86.19.10:8000/v1");
assert.equal(selectModel({harness: {settings: {allow_insecure_http_model_endpoint: true}}, models: {default: {provider: "x", model: "a", api_key_env: "KEY", base_url: "http://192.168.1.10:8000/v1"}}}).baseUrl, "http://192.168.1.10:8000/v1");
for (const url of ["http://example.com/v1", "http://8.8.8.8/v1", "http://169.254.169.254/v1"]) {
assert.throws(() => selectModel({harness: {settings: {allow_insecure_http_model_endpoint: true}}, models: {default: {provider: "x", model: "a", api_key_env: "KEY", base_url: url}}}), (error) => error.code === "kilo_invalid_model");
}
Expand Down
Loading