From 6b8dd63897ed4acc83ff79ffad4ecec402c95921 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 01:34:59 -0700 Subject: [PATCH 01/21] feat: normalize portable adapter configuration Signed-off-by: Ajay Thorve --- adapters/claude/fabric-adapter.json | 11 +- .../nemo_fabric_adapters/claude/adapter.py | 135 ++-- adapters/codex/fabric-adapter.json | 9 +- .../src/nemo_fabric_adapters/codex/adapter.py | 86 ++- .../src/nemo_fabric_adapters/common/utils.py | 69 +- adapters/deepagents/fabric-adapter.json | 11 +- .../deepagents/adapter.py | 86 +-- adapters/hermes/fabric-adapter.json | 8 +- .../nemo_fabric_adapters/hermes/adapter.py | 143 ++-- .../adapters/claude/fabric-adapter.json | 11 +- .../assets/adapters/codex/fabric-adapter.json | 9 +- .../adapters/deepagents/fabric-adapter.json | 11 +- .../adapters/hermes/fabric-adapter.json | 8 +- crates/fabric-cli/src/examples.rs | 10 +- crates/fabric-cli/src/presets.rs | 14 +- crates/fabric-cli/src/scaffold.rs | 115 ++- .../fabric-cli/templates/python/main.py.tmpl | 14 +- crates/fabric-cli/templates/rust/main.rs.tmpl | 4 + crates/fabric-core/src/config.rs | 677 +++++++++++++++++- crates/fabric-core/src/error.rs | 26 +- crates/fabric-core/src/lib.rs | 17 +- crates/fabric-core/src/runtime.rs | 62 +- examples/code_review_agent/config.py | 30 +- .../adapters/claude/fabric-adapter.json | 11 +- .../adapters/hermes/fabric-adapter.json | 8 +- python/src/nemo_fabric/__init__.py | 2 + .../integrations/harbor/fabric_agent.py | 70 +- python/src/nemo_fabric/models.py | 109 ++- python/src/nemo_fabric/types.py | 118 ++- schemas/adapter-descriptor.schema.json | 64 +- schemas/adapter-invocation.schema.json | 7 + schemas/agent.schema.json | 77 +- schemas/environment-handle.schema.json | 7 + schemas/run-plan.schema.json | 161 ++++- schemas/runtime-context.schema.json | 7 + schemas/runtime-handle.schema.json | 7 + tests/_utils/configs.py | 4 +- tests/adapters/test_adapaters_common_utils.py | 78 +- tests/adapters/test_claude_adapter.py | 63 +- tests/adapters/test_codex_adapter.py | 76 +- tests/adapters/test_deepagents.py | 26 +- tests/adapters/test_hermes_adapter.py | 96 ++- tests/e2e/test_claude.py | 24 +- tests/e2e/test_codex.py | 16 +- tests/e2e/test_deepagents.py | 6 +- tests/e2e/test_hermes_config_mapping.py | 28 +- tests/e2e/test_hermes_e2e.py | 6 +- .../adapters/hermes-shim/fabric-adapter.json | 3 +- .../hermes_shim/adapter.py | 4 +- tests/integrations/test_harbor_runner.py | 33 +- tests/python/test_native_sdk.py | 21 +- tests/python/test_sdk_contract.py | 102 ++- tests/python/test_typed_config.py | 5 +- 53 files changed, 2277 insertions(+), 528 deletions(-) diff --git a/adapters/claude/fabric-adapter.json b/adapters/claude/fabric-adapter.json index 9f8996f94..5ea209bb2 100644 --- a/adapters/claude/fabric-adapter.json +++ b/adapters/claude/fabric-adapter.json @@ -7,7 +7,16 @@ "module": "nemo_fabric_adapters.claude.adapter" }, "config": { - "accepts": ["models", "tools", "tools.blocked", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "system_prompt", + "max_turns", + "tools.blocked", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py index 2a57fd10e..ea0d4a50e 100644 --- a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py +++ b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py @@ -46,9 +46,18 @@ "auto", } SETTING_SOURCES = {"user", "project", "local"} +REMOVED_SETTING_FIELDS = { + "cli_path": "the Claude Agent SDK owns its bundled runtime", + "nemo_relay_command": "NeMo Relay is resolved from PATH", +} NORMALIZED_SETTING_FIELDS = { "model_name": "FabricConfig.models", + "base_url": "FabricConfig.models..base_url", "cwd": "FabricConfig.environment.workspace", + "env": "FabricConfig.environment.env", + "system_prompt": "FabricConfig.system_prompt", + "max_turns": "FabricConfig.max_turns", + "timeout_seconds": "FabricConfig.runtime.timeout_seconds", "tools": "FabricConfig.tools", "disallowed_tools": "FabricConfig.tools.blocked", "mcp_servers": "FabricConfig.mcp", @@ -197,18 +206,19 @@ def _validate_settings_boundary(settings: dict[str, Any]) -> None: "claude_invalid_configuration", f"harness.settings.{name} is not supported; use {normalized_field}", ) - - -def _models(payload: dict[str, Any]) -> dict[str, Any]: - return _mapping(common_utils.models_payload(payload), name="models") + for name, reason in REMOVED_SETTING_FIELDS.items(): + if name in settings: + raise AdapterConfigError( + "claude_invalid_configuration", + f"harness.settings.{name} is not supported; {reason}", + ) def _selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: - models = _models(payload) - if not models: - return {} - selected = models.get("default") or next(iter(models.values())) - return _mapping(selected, name="selected model") + return _mapping( + common_utils.selected_model_config(payload), + name="selected model", + ) def _resolve_path(payload: dict[str, Any], value: str | Path) -> Path: @@ -233,7 +243,7 @@ def selected_model(payload: dict[str, Any]) -> str | None: if provider not in {"anthropic", "nvidia"}: raise AdapterConfigError( "claude_invalid_configuration", - "models.default.provider must be anthropic or nvidia for the Claude adapter", + "selected model provider must be anthropic or nvidia for the Claude adapter", ) if not isinstance(value, str) or not value: raise AdapterConfigError( @@ -242,43 +252,49 @@ def selected_model(payload: dict[str, Any]) -> str | None: return value.removeprefix("anthropic/") if provider == "anthropic" else value -def _nvidia_environment(payload: dict[str, Any]) -> dict[str, str]: +def _model_environment( + payload: dict[str, Any], environment: dict[str, str] +) -> dict[str, str]: model = _selected_model_config(payload) - if model.get("provider") != "nvidia": - return {} - api_key_env = model.get("api_key_env") or "NVIDIA_API_KEY" + provider = model.get("provider") + api_key_env = model.get("api_key_env") + if provider == "nvidia" and api_key_env is None: + api_key_env = "NVIDIA_API_KEY" if not isinstance(api_key_env, str) or not api_key_env: + if api_key_env is None: + api_key = None + else: + raise AdapterConfigError( + "claude_invalid_configuration", + "selected model api_key_env must be a non-empty string", + ) + else: + api_key = environment.get(api_key_env) or os.environ.get(api_key_env) + if api_key_env is not None and not api_key: raise AdapterConfigError( "claude_invalid_configuration", - "models.default.api_key_env must be a non-empty string", + f"{api_key_env} is required for the selected model provider", ) - api_key = os.environ.get(api_key_env) - if not api_key: + base_url = common_utils.get_base_url(model) + if provider == "nvidia" and not base_url: raise AdapterConfigError( "claude_invalid_configuration", - f"{api_key_env} is required for the NVIDIA model provider", + "selected model base_url is required for the NVIDIA model provider", ) - settings = _settings(payload) - model_settings = _mapping(model.get("settings"), name="models.default.settings") - base_url = ( - settings.get("base_url") - or model_settings.get("base_url") - or os.environ.get("NVIDIA_FRONTIER_BASE_URL") - ) - if not isinstance(base_url, str) or not base_url: - raise AdapterConfigError( - "claude_invalid_configuration", - "models.default.settings.base_url or NVIDIA_FRONTIER_BASE_URL is required " - "for the NVIDIA model provider", + values: dict[str, str] = {} + if api_key: + values["ANTHROPIC_API_KEY"] = api_key + if base_url: + # Claude Code appends the Anthropic API version path itself, while + # Fabric's NVIDIA endpoint includes it for OpenAI-compatible clients. + values["ANTHROPIC_BASE_URL"] = ( + base_url.rstrip("/").removesuffix("/v1") + if provider == "nvidia" + else base_url.rstrip("/") ) - # Claude Code appends the Anthropic API version path itself, while Fabric's - # shared NVIDIA endpoint includes it for OpenAI-compatible clients. - claude_base_url = base_url.rstrip("/").removesuffix("/v1") - return { - "ANTHROPIC_API_KEY": api_key, - "ANTHROPIC_AUTH_TOKEN": "", - "ANTHROPIC_BASE_URL": claude_base_url, - } + if provider == "nvidia": + values["ANTHROPIC_AUTH_TOKEN"] = "" + return values def _mcp_servers(payload: dict[str, Any]) -> dict[str, Any]: @@ -416,13 +432,7 @@ def prepare_claude_relay(payload: dict[str, Any]) -> ClaudeRelaySettings | None: if not common_utils.relay_enabled(payload): return None - settings = _settings(payload) - command = settings.get("nemo_relay_command") or "nemo-relay" - if not isinstance(command, (str, Path)): - raise AdapterConfigError( - "claude_invalid_configuration", - "nemo_relay_command must be a path", - ) + command = os.environ.get("FABRIC_TEST_NEMO_RELAY_COMMAND", "nemo-relay") try: executable = relay_gateway.resolve_relay_command( Path(common_utils.base_dir(payload)).resolve(), @@ -499,13 +509,7 @@ def build_options( raise AdapterConfigError( "claude_invalid_configuration", "permission_mode is invalid" ) - max_turns = settings.get("max_turns") - if max_turns is not None and ( - isinstance(max_turns, bool) or not isinstance(max_turns, int) or max_turns <= 0 - ): - raise AdapterConfigError( - "claude_invalid_configuration", "max_turns must be positive" - ) + max_turns = common_utils.max_turns(payload) max_budget = settings.get("max_budget_usd") if max_budget is not None: max_budget = _positive_number(max_budget, name="max_budget_usd") @@ -515,17 +519,9 @@ def build_options( raise AdapterConfigError( "claude_invalid_configuration", "setting_sources is invalid" ) - cli_path = settings.get("cli_path") - if cli_path is not None and not isinstance(cli_path, (str, Path)): - raise AdapterConfigError( - "claude_invalid_configuration", "cli_path must be a path" - ) + cli_path = os.environ.get("FABRIC_TEST_CLAUDE_CLI_PATH") - system_prompt = settings.get("system_prompt") - if system_prompt is not None and not isinstance(system_prompt, (str, dict)): - raise AdapterConfigError( - "claude_invalid_configuration", "system_prompt is invalid" - ) + system_prompt = common_utils.system_prompt(payload) plugins = _stage_skill_plugin(payload) has_skill_plugin = bool(plugins) if relay is not None: @@ -542,7 +538,7 @@ def build_options( max_turns=max_turns, max_budget_usd=max_budget, setting_sources=sources, - cli_path=_resolve_path(payload, cli_path) if cli_path is not None else None, + cli_path=_resolve_path(payload, cli_path) if cli_path else None, mcp_servers=_mcp_servers(payload), strict_mcp_config=True, skills="all" if has_skill_plugin else None, @@ -556,7 +552,7 @@ def build_options( def timeout_seconds(payload: dict[str, Any]) -> float: - value = _settings(payload).get("timeout_seconds", 1800) + value = common_utils.timeout_seconds(payload, default=1800) return _positive_number(value, name="timeout_seconds") @@ -679,16 +675,9 @@ def child_environment( api_key_env = model.get("api_key_env") if isinstance(api_key_env, str) and api_key_env in os.environ: values[api_key_env] = os.environ[api_key_env] - configured = _mapping(_settings(payload).get("env"), name="harness.settings.env") - if any( - not isinstance(key, str) or not isinstance(value, str) - for key, value in configured.items() - ): - raise AdapterConfigError( - "claude_invalid_configuration", "harness.settings.env must contain strings" - ) + configured = common_utils.environment_env(payload) values.update(configured) - values.update(_nvidia_environment(payload)) + values.update(_model_environment(payload, values)) if relay_gateway_url is not None: values["NEMO_RELAY_GATEWAY_URL"] = relay_gateway_url values["ANTHROPIC_BASE_URL"] = relay_gateway_url diff --git a/adapters/codex/fabric-adapter.json b/adapters/codex/fabric-adapter.json index 251ab043f..27eac7877 100644 --- a/adapters/codex/fabric-adapter.json +++ b/adapters/codex/fabric-adapter.json @@ -7,7 +7,14 @@ "module": "nemo_fabric_adapters.codex.adapter" }, "config": { - "accepts": ["models", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "system_prompt", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py index 0d08f9277..471712b80 100644 --- a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py +++ b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py @@ -85,8 +85,19 @@ "codex_state_dir", "skip_git_repo_check", } +REMOVED_SETTING_FIELDS = { + "codex_bin": "the Codex SDK owns its pinned app-server runtime", + "nemo_relay_command": "NeMo Relay is resolved from PATH", + "service_name": "the Codex app-server service label is not portable consumer intent", +} NORMALIZED_SETTING_FIELDS = { + "model": "FabricConfig.models", + "base_instructions": "FabricConfig.system_prompt", + "system_prompt": "FabricConfig.system_prompt", "cwd": "FabricConfig.environment.workspace", + "env": "FabricConfig.environment.env", + "base_url": "FabricConfig.models..base_url", + "timeout_seconds": "FabricConfig.runtime.timeout_seconds", "mcp_servers": "FabricConfig.mcp", "model_name": "FabricConfig.models", "skills": "FabricConfig.skills", @@ -158,6 +169,12 @@ def _validate_settings_boundary(settings: dict[str, Any]) -> None: "codex_invalid_configuration", f"harness.settings.{name} is not supported; use {normalized_field}", ) + for name, reason in REMOVED_SETTING_FIELDS.items(): + if name in settings: + raise AdapterConfigError( + "codex_invalid_configuration", + f"harness.settings.{name} is not supported; {reason}", + ) def runtime_id(payload: dict[str, Any]) -> str: @@ -308,10 +325,10 @@ def resolve_cwd(payload: dict[str, Any]) -> Path: def _selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: - settings = _settings(payload) - models = _mapping(common_utils.models_payload(payload), name="models") - selected = models.get(settings.get("model", "default")) or {} - return _mapping(selected, name="selected model") + return _mapping( + common_utils.selected_model_config(payload), + name="selected model", + ) def selected_model(payload: dict[str, Any]) -> str | None: @@ -346,22 +363,19 @@ def nvidia_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: "codex_invalid_configuration", "models.default.api_key_env must be a non-empty string", ) - if not os.environ.get(api_key_env): + if not ( + common_utils.environment_env(payload).get(api_key_env) + or os.environ.get(api_key_env) + ): raise AdapterConfigError( "codex_invalid_configuration", f"{api_key_env} is required for the NVIDIA model provider", ) - model_settings = _mapping( - model_config.get("settings"), name="selected model settings" - ) - base_url = model_settings.get("base_url") or os.environ.get( - "NVIDIA_FRONTIER_BASE_URL" - ) + base_url = common_utils.get_base_url(model_config) if not isinstance(base_url, str) or not base_url: raise AdapterConfigError( "codex_invalid_configuration", - "models.default.settings.base_url or NVIDIA_FRONTIER_BASE_URL is required " - "for the NVIDIA model provider", + "selected model base_url is required for the NVIDIA model provider", ) return { "model_providers": { @@ -375,6 +389,14 @@ def nvidia_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: } +def openai_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: + model_config = _selected_model_config(payload) + if model_config.get("provider") != "openai": + return {} + base_url = common_utils.get_base_url(model_config) + return {"openai_base_url": base_url.rstrip("/")} if base_url else {} + + def sandbox(payload: dict[str, Any]) -> Sandbox: value = _settings(payload).get("sandbox", "read-only") try: @@ -398,7 +420,7 @@ def approval_mode(payload: dict[str, Any]) -> ApprovalMode: def timeout_seconds(payload: dict[str, Any]) -> float: - value = _settings(payload).get("timeout_seconds", DEFAULT_TIMEOUT_SECONDS) + value = common_utils.timeout_seconds(payload, default=DEFAULT_TIMEOUT_SECONDS) if isinstance(value, bool) or not isinstance(value, (int, float)): raise AdapterConfigError( "codex_invalid_configuration", "timeout_seconds must be positive" @@ -453,16 +475,14 @@ def child_environment( api_key_env = model_config.get("api_key_env") if isinstance(api_key_env, str) and api_key_env in os.environ: values[api_key_env] = os.environ[api_key_env] - configured = _mapping(_settings(payload).get("env"), name="harness.settings.env") - if any( - not isinstance(key, str) or not isinstance(value, str) - for key, value in configured.items() - ): - raise AdapterConfigError( - "codex_invalid_configuration", - "harness.settings.env must contain strings", - ) + configured = common_utils.environment_env(payload) values.update(configured) + if ( + selected_model_provider(payload) == "openai" + and isinstance(api_key_env, str) + and api_key_env in values + ): + values["OPENAI_API_KEY"] = values[api_key_env] if selected_model_provider(payload) == "nvidia": codex_home = state_dir(payload) / "nvidia-home" values["CODEX_HOME"] = str(codex_home) @@ -582,11 +602,7 @@ def prepare_codex_relay(payload: dict[str, Any]) -> CodexRelaySettings | None: if not common_utils.relay_enabled(payload): return None - command = _settings(payload).get("nemo_relay_command") or "nemo-relay" - if not isinstance(command, (str, Path)): - raise AdapterConfigError( - "codex_invalid_configuration", "nemo_relay_command must be a path" - ) + command = os.environ.get("FABRIC_TEST_NEMO_RELAY_COMMAND", "nemo-relay") try: executable = relay_gateway.resolve_relay_command( Path(common_utils.base_dir(payload)).resolve(), command @@ -639,6 +655,7 @@ def thread_config( config = native_codex_telemetry_config(payload) _merge_config(config, nvidia_model_provider_config(payload)) + _merge_config(config, openai_model_provider_config(payload)) mcp_servers = _native_mcp_servers(payload) if mcp_servers: config["mcp_servers"] = mcp_servers @@ -677,8 +694,8 @@ def thread_config( def sdk_config( payload: dict[str, Any], relay: CodexRelaySettings | None ) -> CodexConfig: - codex_bin = _optional_string(_settings(payload), "codex_bin") - if codex_bin is not None: + codex_bin = os.environ.get("FABRIC_TEST_CODEX_BIN") + if codex_bin: path = Path(codex_bin) if not path.is_absolute(): path = (Path(common_utils.base_dir(payload)) / path).resolve() @@ -739,7 +756,6 @@ def validate_runtime_payload(payload: dict[str, Any]) -> str: for name in ( "base_instructions", "developer_instructions", - "service_name", "service_tier", ): _optional_string(settings, name) @@ -893,7 +909,7 @@ def _thread_options( settings = _settings(payload) return { "approval_mode": approval_mode(payload), - "base_instructions": _optional_string(settings, "base_instructions"), + "base_instructions": common_utils.system_prompt(payload), "config": thread_config(payload, relay) or None, "cwd": str(resolve_cwd(payload)), "developer_instructions": _optional_string(settings, "developer_instructions"), @@ -911,12 +927,8 @@ async def _open_thread( *, relay: CodexRelaySettings | None, ) -> Any: - settings = _settings(payload) options = _thread_options(payload, relay) - return await codex.thread_start( - **options, - service_name=_optional_string(settings, "service_name"), - ) + return await codex.thread_start(**options) async def _invoke_thread( diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index e8dad5d3b..e81c59cea 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -90,7 +90,12 @@ def runtime_state_directory(base: str | Path, payload: dict[str, Any]) -> Path: def environment_payload(payload: dict[str, Any]) -> dict[str, Any]: - return runtime_context(payload).get("environment") or payload.get("environment") or {} + return ( + runtime_context(payload).get("environment") + or fabric_config(payload).get("environment") + or payload.get("environment") + or {} + ) def settings_payload(payload: dict[str, Any]) -> dict[str, Any]: @@ -108,23 +113,48 @@ def default_base_url(provider: str | None) -> str | None: return None -def get_base_url(settings: dict[str, Any], model_config: dict[str, Any]) -> str | None: - return ( - settings.get("base_url") - or (model_config.get("settings") or {}).get("base_url") - or default_base_url(model_config.get("provider")) - ) +def get_base_url(model_config: dict[str, Any]) -> str | None: + """Return the canonical model endpoint, including provider defaults.""" + + return model_config.get("base_url") or default_base_url(model_config.get("provider")) def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: - settings = settings_payload(payload) models = models_payload(payload) - model_config = models.get(settings.get("model", "default"), {}) + model_config = models.get("default") + if model_config is None and len(models) == 1: + model_config = next(iter(models.values())) if not isinstance(model_config, dict): return {} return model_config +def system_prompt(payload: dict[str, Any]) -> str | None: + value = fabric_config(payload).get("system_prompt") + return value if isinstance(value, str) else None + + +def max_turns(payload: dict[str, Any]) -> int | None: + value = fabric_config(payload).get("max_turns") + return value if isinstance(value, int) and not isinstance(value, bool) else None + + +def timeout_seconds(payload: dict[str, Any], *, default: float) -> float: + value = (fabric_config(payload).get("runtime") or {}).get("timeout_seconds") + return float(default if value is None else value) + + +def environment_env(payload: dict[str, Any]) -> dict[str, str]: + value = environment_payload(payload).get("env") or {} + if not isinstance(value, dict): + return {} + return { + str(name): str(item) + for name, item in value.items() + if isinstance(name, str) and isinstance(item, str) + } + + def telemetry_payload(payload: dict[str, Any]) -> dict[str, Any]: telemetry = fabric_config(payload).get("telemetry") or payload.get("telemetry") or {} return telemetry if isinstance(telemetry, dict) else {} @@ -165,6 +195,22 @@ def blocked_tools(payload: dict[str, Any]) -> list[str]: return normalize_list(blocked) +def toolsets_config(payload: dict[str, Any]) -> dict[str, Any]: + toolsets = tools_config(payload).get("toolsets") or {} + return toolsets if isinstance(toolsets, dict) else {} + + +def enabled_toolsets(payload: dict[str, Any]) -> list[str] | None: + toolsets = toolsets_config(payload) + if "enabled" not in toolsets: + return None + return normalize_list(toolsets.get("enabled")) + + +def blocked_toolsets(payload: dict[str, Any]) -> list[str]: + return normalize_list(toolsets_config(payload).get("blocked")) + + def normalize_list(value: Any) -> list[str]: if value is None: return [] @@ -339,7 +385,4 @@ def write_relay_configs( def relay_model_name(payload: dict[str, Any]) -> str: - settings = settings_payload(payload) - models = models_payload(payload) - model_config = models.get(settings.get("model", "default"), {}) - return settings.get("model_name") or model_config.get("model") or "unknown" + return selected_model_config(payload).get("model") or "unknown" diff --git a/adapters/deepagents/fabric-adapter.json b/adapters/deepagents/fabric-adapter.json index c32aa03b2..5d649557d 100644 --- a/adapters/deepagents/fabric-adapter.json +++ b/adapters/deepagents/fabric-adapter.json @@ -8,7 +8,16 @@ }, "requirements": {}, "config": { - "accepts": ["models", "tools", "tools.blocked", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "models.temperature", + "system_prompt", + "tools.blocked", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py index 2afc77939..baa9e4e7e 100644 --- a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py +++ b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py @@ -28,11 +28,8 @@ import nemo_fabric_adapters.common.utils as common_utils HARNESS = "deepagents" -DEFAULT_NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1" # Providers we serve through the OpenAI-compatible ``ChatOpenAI`` client. OPENAI_COMPATIBLE_PROVIDERS = {"", "nvidia", "openai", "openai-compatible"} -# Providers whose default endpoint is NVIDIA's OpenAI-compatible gateway. -NVIDIA_DEFAULT_PROVIDERS = {"", "nvidia"} # Conventional credential env var per provider; others must set api_key_env. PROVIDER_DEFAULT_API_KEY_ENV = { "": "NVIDIA_API_KEY", @@ -60,6 +57,21 @@ # through harness.settings.deepagents. Executable objects (AgentMiddleware, BaseTool, # Python callables) cannot cross the SDK->JSON->payload boundary and are excluded. DEEPAGENTS_PASSTHROUGH_KEYS = frozenset({"subagents", "interrupt_on"}) +NORMALIZED_SETTING_FIELDS = { + "model": "FabricConfig.models", + "model_name": "FabricConfig.models..model", + "provider": "FabricConfig.models..provider", + "api_key_env": "FabricConfig.models..api_key_env", + "base_url": "FabricConfig.models..base_url", + "temperature": "FabricConfig.models..temperature", + "system_prompt": "FabricConfig.system_prompt", + "workspace": "FabricConfig.environment.workspace", + "env": "FabricConfig.environment.env", + "timeout_seconds": "FabricConfig.runtime.timeout_seconds", +} +REMOVED_SETTING_FIELDS = { + "state_dir": "runtime state is derived from the Fabric artifact root and runtime ID", +} class AdapterConfigError(RuntimeError): @@ -96,7 +108,20 @@ def wrap_tool_call(self, request: Any, handler: Any) -> Any: return handler(request) -def resolve_api_key_env(settings: dict[str, Any], model_config: dict[str, Any]) -> str: +def _validate_settings_boundary(settings: dict[str, Any]) -> None: + for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): + if name in settings: + raise AdapterConfigError( + f"harness.settings.{name} is not supported; use {normalized_field}" + ) + for name, reason in REMOVED_SETTING_FIELDS.items(): + if name in settings: + raise AdapterConfigError( + f"harness.settings.{name} is not supported; {reason}" + ) + + +def resolve_api_key_env(model_config: dict[str, Any]) -> str: """Resolve the credential env var, defaulting per provider. An explicit ``api_key_env`` always wins. Otherwise nvidia/unspecified default @@ -104,10 +129,10 @@ def resolve_api_key_env(settings: dict[str, Any], model_config: dict[str, Any]) set ``api_key_env`` explicitly so a key is never sent to the wrong endpoint. """ - explicit = settings.get("api_key_env") or model_config.get("api_key_env") + explicit = model_config.get("api_key_env") if explicit: return str(explicit) - provider = (settings.get("provider") or model_config.get("provider") or "").lower() + provider = str(model_config.get("provider") or "").lower() default = PROVIDER_DEFAULT_API_KEY_ENV.get(provider) if default is None: raise AdapterConfigError( @@ -140,8 +165,9 @@ def preflight_check(payload: dict[str, Any]) -> None: ) settings = common_utils.settings_payload(payload) + _validate_settings_boundary(settings) model_config = selected_model_config(payload) - api_key_env = resolve_api_key_env(settings, model_config) + api_key_env = resolve_api_key_env(model_config) if api_key_env not in os.environ: raise RuntimeError( f"the model-provider credential env var '{api_key_env}' is not set in the " @@ -151,27 +177,11 @@ def preflight_check(payload: dict[str, Any]) -> None: def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: - settings = common_utils.settings_payload(payload) - models = common_utils.models_payload(payload) - return models.get(settings.get("model", "default"), {}) or {} + return common_utils.selected_model_config(payload) -def resolve_base_url( - settings: dict[str, Any], model_config: dict[str, Any] -) -> str | None: - base_url = ( - settings.get("base_url") - or (model_config.get("settings") or {}).get("base_url") - or model_config.get("base_url") - ) - if base_url: - return base_url - provider = (settings.get("provider") or model_config.get("provider") or "").lower() - # Only NVIDIA (or an unspecified provider) defaults to NVIDIA's endpoint; a - # plain ``openai`` provider must fall through to ChatOpenAI's own default. - if provider in NVIDIA_DEFAULT_PROVIDERS: - return DEFAULT_NVIDIA_BASE_URL - return None +def resolve_base_url(model_config: dict[str, Any]) -> str | None: + return common_utils.get_base_url(model_config) def build_chat_model(payload: dict[str, Any]) -> tuple[Any, str, str | None]: @@ -183,24 +193,21 @@ def build_chat_model(payload: dict[str, Any]) -> tuple[Any, str, str | None]: reworking the adapter. """ - settings = common_utils.settings_payload(payload) model_config = selected_model_config(payload) - model_name = settings.get("model_name") or model_config.get("model") + model_name = model_config.get("model") if not model_name: raise RuntimeError( "models.default.model is required for the Deep Agents adapter" ) - api_key_env = resolve_api_key_env(settings, model_config) + api_key_env = resolve_api_key_env(model_config) api_key = os.environ.get(api_key_env) if not api_key: raise RuntimeError(f"{api_key_env} is required for the Deep Agents adapter") - provider = ( - settings.get("provider") or model_config.get("provider") or "nvidia" - ).lower() - base_url = resolve_base_url(settings, model_config) - temperature = settings.get("temperature", model_config.get("temperature")) + provider = str(model_config.get("provider") or "nvidia").lower() + base_url = resolve_base_url(model_config) + temperature = model_config.get("temperature") if provider not in OPENAI_COMPATIBLE_PROVIDERS: # Generic provider hook: honor an explicit non-OpenAI-compatible provider. @@ -231,9 +238,7 @@ def resolve_backend(payload: dict[str, Any]) -> Any: """Root the Deep Agents filesystem backend at the Fabric workspace, if set.""" environment = common_utils.environment_payload(payload) - workspace = environment.get("workspace") or common_utils.settings_payload( - payload - ).get("workspace") + workspace = environment.get("workspace") if not workspace: return None root = Path(str(workspace)) @@ -322,11 +327,6 @@ def _mcp_connection(name: str, spec: dict[str, Any]) -> dict[str, Any]: def state_dir(payload: dict[str, Any]) -> Path: base_dir = Path(common_utils.base_dir(payload)).resolve() - settings = common_utils.settings_payload(payload) - configured = settings.get("state_dir") - if configured: - path = Path(str(configured)) - return path if path.is_absolute() else base_dir / path artifacts = common_utils.runtime_context(payload).get("artifacts") or {} root = artifacts.get("root") or os.environ.get("FABRIC_ARTIFACTS") if root: @@ -373,7 +373,7 @@ async def build_agent_kwargs( "model": model, "tools": await resolve_tools(payload), # deepagents 0.5.x/0.6.x take the system prompt as ``system_prompt``. - "system_prompt": settings.get("system_prompt"), + "system_prompt": common_utils.system_prompt(payload), "skills": resolve_skills(payload), "backend": resolve_backend(payload), } diff --git a/adapters/hermes/fabric-adapter.json b/adapters/hermes/fabric-adapter.json index ef60fb920..40927fb41 100644 --- a/adapters/hermes/fabric-adapter.json +++ b/adapters/hermes/fabric-adapter.json @@ -14,8 +14,12 @@ "config": { "accepts": [ "models", - "tools", - "tools.blocked", + "models.base_url", + "models.temperature", + "system_prompt", + "max_turns", + "tools.toolsets.enabled", + "tools.toolsets.blocked", "mcp", "skills", "telemetry" diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index b89876eff..9c6fdfb26 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -23,12 +23,64 @@ from nemo_fabric_adapters.common import lifecycle import nemo_fabric_adapters.common.utils as common_utils -# Default agent loop budget when harness.settings.max_iterations is unset. +# Default agent loop budget when FabricConfig.max_turns is unset. # Mirrors Hermes' own AIAgent default (agent/agent_init.py); a lower value such # as 1 silently starves multi-step tasks (they run out of budget before # answering while the trial still reports success). See FABRIC-85. DEFAULT_MAX_ITERATIONS: int = 90 LOGGER = logging.getLogger(__name__) +NORMALIZED_SETTING_FIELDS = { + "model": "FabricConfig.models", + "model_name": "FabricConfig.models..model", + "provider": "FabricConfig.models..provider", + "api_key_env": "FabricConfig.models..api_key_env", + "base_url": "FabricConfig.models..base_url", + "temperature": "FabricConfig.models..temperature", + "system_prompt": "FabricConfig.system_prompt", + "max_iterations": "FabricConfig.max_turns", + "max_turns": "FabricConfig.max_turns", + "workspace": "FabricConfig.environment.workspace", + "env": "FabricConfig.environment.env", + "enabled_toolsets": "FabricConfig.tools.toolsets.enabled", + "disabled_toolsets": "FabricConfig.tools.toolsets.blocked", +} +REMOVED_SETTING_FIELDS = { + "hermes_home": "runtime state is derived from the Fabric artifact root and runtime ID", + "insert_reasoning": "the supported Hermes SDK does not consume this option", + "terminal_backend": "execution placement is owned by FabricConfig.environment", + "toolset_platform": "the adapter owns its Hermes SDK lookup bridge", +} +PROVIDER_DEFAULT_API_KEY_ENV = { + "": "NVIDIA_API_KEY", + "anthropic": "ANTHROPIC_API_KEY", + "nvidia": "NVIDIA_API_KEY", + "openai": "OPENAI_API_KEY", + "openrouter": "OPENROUTER_API_KEY", +} + + +def _validate_settings_boundary(settings: dict[str, Any]) -> None: + for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): + if name in settings: + raise ValueError( + f"harness.settings.{name} is not supported; use {normalized_field}" + ) + for name, reason in REMOVED_SETTING_FIELDS.items(): + if name in settings: + raise ValueError(f"harness.settings.{name} is not supported; {reason}") + + +def _api_key_env(model_config: dict[str, Any]) -> str: + explicit = model_config.get("api_key_env") + if isinstance(explicit, str) and explicit: + return explicit + provider = str(model_config.get("provider") or "").lower() + default = PROVIDER_DEFAULT_API_KEY_ENV.get(provider) + if default is None: + raise ValueError( + f"selected model api_key_env is required for provider {provider!r}" + ) + return default def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: @@ -38,11 +90,7 @@ def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: def disabled_toolsets(payload: dict[str, Any]) -> list[str]: - settings = common_utils.settings_payload(payload) - return common_utils.merge_unique( - common_utils.blocked_tools(payload), - settings.get("disabled_toolsets"), - ) + return common_utils.blocked_toolsets(payload) def build_hermes_config( @@ -53,10 +101,11 @@ def build_hermes_config( native = common_utils.capability_plan(payload).get("native") or {} environment = common_utils.environment_payload(payload) - model_name = settings.get("model_name") or model_config.get("model", "") - provider = settings.get("provider") or model_config.get("provider") - base_url = common_utils.get_base_url(settings, model_config) + model_name = model_config.get("model", "") + provider = model_config.get("provider") + base_url = common_utils.get_base_url(model_config) blocked_toolsets = disabled_toolsets(payload) + enabled_toolsets = common_utils.enabled_toolsets(payload) config: dict[str, Any] = { "model": common_utils.without_none( @@ -68,16 +117,14 @@ def build_hermes_config( ), "agent": common_utils.without_none( { - "max_turns": settings.get("max_iterations"), + "max_turns": common_utils.max_turns(payload), "disabled_toolsets": blocked_toolsets or None, } ), "terminal": common_utils.without_none( { - "backend": settings.get("terminal_backend", "local"), - "cwd": str( - environment.get("workspace") or settings.get("workspace") or "." - ), + "backend": "local", + "cwd": str(environment.get("workspace") or "."), "timeout": settings.get("terminal_timeout", 60), } ), @@ -94,12 +141,8 @@ def build_hermes_config( for name, server in sorted(mcp_servers.items()) } - if "enabled_toolsets" in settings: - config["platform_toolsets"] = { - settings.get("toolset_platform", "cli"): common_utils.normalize_list( - settings.get("enabled_toolsets") - ) - } + if enabled_toolsets is not None: + config["platform_toolsets"] = {"cli": enabled_toolsets} plugins = common_utils.normalize_list(settings.get("plugins_enabled")) if relay_enabled and "observability/nemo_relay" not in plugins: @@ -162,15 +205,23 @@ def main() -> None: def resolve_hermes_toolsets( - settings: dict[str, Any], config: dict[str, Any] + payload: dict[str, Any], config: dict[str, Any] ) -> list[str] | None: - if "enabled_toolsets" in settings: - return common_utils.normalize_list(settings.get("enabled_toolsets")) + enabled = common_utils.enabled_toolsets(payload) + if enabled is not None: + return enabled from hermes_cli.tools_config import _get_platform_tools - platform = settings.get("toolset_platform", "cli") - return sorted(_get_platform_tools(config, platform)) + return sorted(_get_platform_tools(config, "cli")) + + +def _artifact_root(payload: dict[str, Any]) -> Path: + artifacts = common_utils.runtime_context(payload).get("artifacts") or {} + root = artifacts.get("root") if isinstance(artifacts, dict) else None + if root: + return Path(str(root)).resolve() + return Path(common_utils.base_dir(payload)).resolve() / "artifacts" class HermesRuntime: @@ -210,13 +261,11 @@ async def start(self, payload: dict[str, Any]) -> None: self._relay_finalize_hook_invoked = False validate_hermes_telemetry_provider(payload) self._settings = common_utils.settings_payload(payload) + _validate_settings_boundary(self._settings) self._model_config = common_utils.selected_model_config(payload) self._runtime_id = common_utils.runtime_id(payload) - hermes_home_base = Path(common_utils.base_dir(payload)).joinpath( - self._settings.get("hermes_home", "./artifacts/hermes-home") - ) self._hermes_home = common_utils.runtime_state_directory( - hermes_home_base, payload + _artifact_root(payload) / ".fabric" / "hermes", payload ) self._hermes_home.mkdir(parents=True, exist_ok=True) os.environ["HOME"] = str(self._hermes_home) @@ -226,7 +275,7 @@ async def start(self, payload: dict[str, Any]) -> None: os.environ["HERMES_SESSION_SOURCE"] = "fabric" os.environ.setdefault( "TERMINAL_ENV", - self._settings.get("terminal_backend", "local"), + "local", ) os.environ.setdefault( "TERMINAL_TIMEOUT", @@ -249,17 +298,11 @@ async def start(self, payload: dict[str, Any]) -> None: self._hermes_home, relay_enabled=relay_enabled, ) - api_key_env = ( - self._settings.get("api_key_env") - or self._model_config.get("api_key_env") - or "NVIDIA_API_KEY" - ) + api_key_env = _api_key_env(self._model_config) api_key = os.environ.get(api_key_env) if not api_key: raise RuntimeError(f"{api_key_env} is required for Hermes mode") - self._base_url = common_utils.get_base_url( - self._settings, self._model_config - ) + self._base_url = common_utils.get_base_url(self._model_config) self._relay_model_name = common_utils.relay_model_name(payload) from hermes_cli.config import load_config @@ -272,11 +315,11 @@ async def start(self, payload: dict[str, Any]) -> None: discover_plugins(force=True) loaded_hermes_config = load_config() self._enabled_toolsets = resolve_hermes_toolsets( - self._settings, loaded_hermes_config + payload, loaded_hermes_config ) self._session_db = SessionDB() self._conversation_history = None - max_iterations = self._settings.get("max_iterations") + max_iterations = common_utils.max_turns(payload) if max_iterations is None: max_iterations = DEFAULT_MAX_ITERATIONS self._agent = AIAgent( @@ -284,10 +327,8 @@ async def start(self, payload: dict[str, Any]) -> None: AIAgent, base_url=self._base_url, api_key=api_key, - provider=self._settings.get("provider") - or self._model_config.get("provider"), - model=self._settings.get("model_name") - or self._model_config.get("model", ""), + provider=self._model_config.get("provider"), + model=self._model_config.get("model", ""), max_iterations=int(max_iterations), enabled_toolsets=self._enabled_toolsets, disabled_toolsets=disabled_toolsets(payload) or None, @@ -298,16 +339,10 @@ async def start(self, payload: dict[str, Any]) -> None: self._settings.get("save_trajectories", False) ), max_tokens=self._settings.get("max_tokens", 512), - temperature=self._settings.get( - "temperature", - self._model_config.get("temperature", 0.0), - ), + temperature=self._model_config.get("temperature", 0.0), reasoning_config=self._settings.get( "reasoning_config", {"effort": "none"} ), - insert_reasoning=bool( - self._settings.get("insert_reasoning", False) - ), platform="fabric", session_id=self._runtime_id, session_db=self._session_db, @@ -347,7 +382,7 @@ async def invoke(self, invocation: dict[str, Any]) -> dict[str, Any]: self._relay_finalize_hook_invoked = False result, adapter_stdout = _invoke_hermes_turn( agent=self._agent, - settings=self._settings, + system_prompt=common_utils.system_prompt(start_payload), user_message=user_message, conversation_history=self._conversation_history, ) @@ -481,7 +516,7 @@ async def stop(self) -> None: def _invoke_hermes_turn( *, agent: Any, - settings: dict[str, Any], + system_prompt: str | None, user_message: str, conversation_history: list[dict[str, Any]] | None, ) -> tuple[dict[str, Any], str]: @@ -489,7 +524,7 @@ def _invoke_hermes_turn( with redirect_stdout(hermes_stdout): conversation_kwargs = filter_supported_call_kwargs( agent.run_conversation, - system_message=settings.get("system_prompt"), + system_message=system_prompt, conversation_history=conversation_history, sync_honcho=False, dont_review=True, diff --git a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json index 9f8996f94..5ea209bb2 100644 --- a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json @@ -7,7 +7,16 @@ "module": "nemo_fabric_adapters.claude.adapter" }, "config": { - "accepts": ["models", "tools", "tools.blocked", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "system_prompt", + "max_turns", + "tools.blocked", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json index 251ab043f..27eac7877 100644 --- a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json @@ -7,7 +7,14 @@ "module": "nemo_fabric_adapters.codex.adapter" }, "config": { - "accepts": ["models", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "system_prompt", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json index c32aa03b2..5d649557d 100644 --- a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json @@ -8,7 +8,16 @@ }, "requirements": {}, "config": { - "accepts": ["models", "tools", "tools.blocked", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "models.temperature", + "system_prompt", + "tools.blocked", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json index ef60fb920..40927fb41 100644 --- a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json @@ -14,8 +14,12 @@ "config": { "accepts": [ "models", - "tools", - "tools.blocked", + "models.base_url", + "models.temperature", + "system_prompt", + "max_turns", + "tools.toolsets.enabled", + "tools.toolsets.blocked", "mcp", "skills", "telemetry" diff --git a/crates/fabric-cli/src/examples.rs b/crates/fabric-cli/src/examples.rs index 69670c375..91c46949c 100644 --- a/crates/fabric-cli/src/examples.rs +++ b/crates/fabric-cli/src/examples.rs @@ -136,10 +136,12 @@ fn code_review_config(preset: Preset) -> FabricConfig { .as_mut() .expect("CLI presets always define an execution environment"); environment.workspace = Some(PathBuf::from("repo")); - config.skills = Some(SkillConfig { - paths: vec![PathBuf::from("skills/code-review.md")], - extensions: BTreeMap::new(), - }); + if preset.name != "scripted" { + config.skills = Some(SkillConfig { + paths: vec![PathBuf::from("skills/code-review.md")], + extensions: BTreeMap::new(), + }); + } config } diff --git a/crates/fabric-cli/src/presets.rs b/crates/fabric-cli/src/presets.rs index 4eabcfccf..c538274ab 100644 --- a/crates/fabric-cli/src/presets.rs +++ b/crates/fabric-cli/src/presets.rs @@ -251,10 +251,13 @@ fn config( models: default_model .map(|model| BTreeMap::from_iter([("default".to_string(), model)])) .unwrap_or_default(), + system_prompt: None, + max_turns: None, runtime: RuntimeConfig { input_schema: "text".to_string(), output_schema: "message".to_string(), artifacts: None, + timeout_seconds: None, extensions: BTreeMap::new(), }, environment: Some(EnvironmentConfig { @@ -263,6 +266,7 @@ fn config( ownership: EnvironmentOwnership::FabricOwned, workspace: None, artifacts: None, + env: BTreeMap::new(), connection: Map::new(), metadata: Map::new(), settings: Map::new(), @@ -288,9 +292,8 @@ fn model( model: name.to_string(), temperature: None, api_key_env: api_key_env.map(str::to_string), - settings: base_url - .map(|value| Map::from_iter([("base_url".to_string(), json!(value))])) - .unwrap_or_default(), + base_url: base_url.map(str::to_string), + settings: Map::new(), extensions: BTreeMap::new(), } } @@ -349,10 +352,7 @@ mod tests { for name in ["hermes", "deepagents"] { let config = find(name).expect("catalog preset").config(); let model = config.models.get("default").expect("default model"); - assert_eq!( - model.settings.get("base_url").and_then(Value::as_str), - Some(NVIDIA_API_CATALOG_BASE_URL) - ); + assert_eq!(model.base_url.as_deref(), Some(NVIDIA_API_CATALOG_BASE_URL)); } for name in ["claude", "codex"] { diff --git a/crates/fabric-cli/src/scaffold.rs b/crates/fabric-cli/src/scaffold.rs index 2d5cfbcef..58b40e8f7 100644 --- a/crates/fabric-cli/src/scaffold.rs +++ b/crates/fabric-cli/src/scaffold.rs @@ -146,6 +146,45 @@ fn render_python(config: &FabricConfig) -> String { "{{HARNESS_SETTINGS}}", &python_value(&Value::Object(config.harness.settings.clone())), ) + .replace( + "{{SYSTEM_PROMPT}}", + &config + .system_prompt + .as_deref() + .map(python_string) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{MAX_TURNS}}", + &config + .max_turns + .map(|value| value.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{TIMEOUT_SECONDS}}", + &config + .runtime + .timeout_seconds + .map(|value| value.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{ENVIRONMENT_ENV}}", + &python_value(&Value::Object( + config + .environment + .as_ref() + .map(|environment| { + environment + .env + .iter() + .map(|(name, value)| (name.clone(), Value::String(value.clone()))) + .collect() + }) + .unwrap_or_default(), + )), + ) .replace("{{MODELS}}", &python_models(config.models.get("default"))) } @@ -157,8 +196,13 @@ fn python_models(model: Option<&ModelConfig>) -> String { .temperature .map(|value| value.to_string()) .unwrap_or_else(|| "None".to_string()); + let base_url = model + .base_url + .as_deref() + .map(python_string) + .unwrap_or_else(|| "None".to_string()); format!( - "{{\"default\": ModelConfig(provider={}, model={}, temperature={temperature}, api_key_env={}, settings={})}}", + "{{\"default\": ModelConfig(provider={}, model={}, temperature={temperature}, api_key_env={}, base_url={base_url}, settings={})}}", python_string(&model.provider), python_string(&model.model), model @@ -211,9 +255,59 @@ fn render_rust(config: &FabricConfig) -> String { "{{HARNESS_SETTINGS}}", &rust_settings(&config.harness.settings), ) + .replace( + "{{SYSTEM_PROMPT}}", + &config + .system_prompt + .as_deref() + .map(|value| format!("Some({}.to_string())", rust_string(value))) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{MAX_TURNS}}", + &config + .max_turns + .map(|value| format!("Some({value})")) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{TIMEOUT_SECONDS}}", + &config + .runtime + .timeout_seconds + .map(|value| format!("Some({value})")) + .unwrap_or_else(|| "None".to_string()), + ) + .replace( + "{{ENVIRONMENT_ENV}}", + &rust_string_map( + config + .environment + .as_ref() + .map(|environment| &environment.env), + ), + ) .replace("{{MODELS}}", &rust_models(config.models.get("default"))) } +fn rust_string_map(values: Option<&std::collections::BTreeMap>) -> String { + let Some(values) = values.filter(|values| !values.is_empty()) else { + return "BTreeMap::new()".to_string(); + }; + format!( + "BTreeMap::from_iter([{}])", + values + .iter() + .map(|(key, value)| format!( + "({}.to_string(), {}.to_string())", + rust_string(key), + rust_string(value) + )) + .collect::>() + .join(", ") + ) +} + fn rust_settings(settings: &serde_json::Map) -> String { if settings.is_empty() { return "Map::new()".to_string(); @@ -247,8 +341,13 @@ fn rust_models(model: Option<&ModelConfig>) -> String { .temperature .map(|value| format!("Some({value})")) .unwrap_or_else(|| "None".to_string()); + let base_url = model + .base_url + .as_deref() + .map(|value| format!("Some({}.to_string())", rust_string(value))) + .unwrap_or_else(|| "None".to_string()); format!( - "BTreeMap::from_iter([(\"default\".to_string(), nemo_fabric_core::ModelConfig {{ provider: {}.to_string(), model: {}.to_string(), temperature: {temperature}, api_key_env: {api_key}, settings: {}, extensions: BTreeMap::new() }})])", + "BTreeMap::from_iter([(\"default\".to_string(), nemo_fabric_core::ModelConfig {{ provider: {}.to_string(), model: {}.to_string(), temperature: {temperature}, api_key_env: {api_key}, base_url: {base_url}, settings: {}, extensions: BTreeMap::new() }})])", rust_string(&model.provider), rust_string(&model.model), rust_settings(&model.settings), @@ -315,7 +414,7 @@ mod tests { } #[test] - fn renderers_preserve_model_settings_and_temperature() { + fn renderers_preserve_model_base_url_and_temperature() { let mut config = presets::find("hermes").expect("hermes preset").config(); config .models @@ -325,15 +424,13 @@ mod tests { let python = render_python(&config); assert!(python.contains("temperature=0.2")); - assert!( - python.contains("settings={\"base_url\": \"https://integrate.api.nvidia.com/v1\"}") - ); + assert!(python.contains("base_url=\"https://integrate.api.nvidia.com/v1\"")); let rust = render_rust(&config); assert!(rust.contains("temperature: Some(0.2)")); - assert!(rust.contains( - "(\"base_url\".to_string(), serde_json::json!(\"https://integrate.api.nvidia.com/v1\"))" - )); + assert!( + rust.contains("base_url: Some(\"https://integrate.api.nvidia.com/v1\".to_string())") + ); } #[test] diff --git a/crates/fabric-cli/templates/python/main.py.tmpl b/crates/fabric-cli/templates/python/main.py.tmpl index 76c039d1b..f27bde37c 100644 --- a/crates/fabric-cli/templates/python/main.py.tmpl +++ b/crates/fabric-cli/templates/python/main.py.tmpl @@ -30,8 +30,18 @@ def build_config() -> FabricConfig: settings={{HARNESS_SETTINGS}}, ), models={{MODELS}}, - runtime=RuntimeConfig(input_schema="text", output_schema="message"), - environment=EnvironmentConfig(provider="local", workspace="repo"), + system_prompt={{SYSTEM_PROMPT}}, + max_turns={{MAX_TURNS}}, + runtime=RuntimeConfig( + input_schema="text", + output_schema="message", + timeout_seconds={{TIMEOUT_SECONDS}}, + ), + environment=EnvironmentConfig( + provider="local", + workspace="repo", + env={{ENVIRONMENT_ENV}}, + ), ) config.add_skill_path("skills/code-review.md") return config diff --git a/crates/fabric-cli/templates/rust/main.rs.tmpl b/crates/fabric-cli/templates/rust/main.rs.tmpl index edbffadb4..d430fd87c 100644 --- a/crates/fabric-cli/templates/rust/main.rs.tmpl +++ b/crates/fabric-cli/templates/rust/main.rs.tmpl @@ -26,10 +26,13 @@ fn build_config() -> FabricConfig { extensions: BTreeMap::new(), }, models: {{MODELS}}, + system_prompt: {{SYSTEM_PROMPT}}, + max_turns: {{MAX_TURNS}}, runtime: RuntimeConfig { input_schema: "text".to_string(), output_schema: "message".to_string(), artifacts: None, + timeout_seconds: {{TIMEOUT_SECONDS}}, extensions: BTreeMap::new(), }, environment: Some(EnvironmentConfig { @@ -38,6 +41,7 @@ fn build_config() -> FabricConfig { ownership: EnvironmentOwnership::FabricOwned, workspace: Some(PathBuf::from("repo")), artifacts: None, + env: {{ENVIRONMENT_ENV}}, connection: Map::new(), metadata: Map::new(), settings: Map::new(), diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index dcb424254..1c61eccc0 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -17,6 +17,10 @@ use crate::error::{FabricError, Result}; pub const ADAPTER_CONTRACT_VERSION: &str = "fabric.adapter/v1alpha1"; /// Versioned Fabric agent config. +/// +/// Fabric-owned fields apply uniformly, while adapter-translated fields are +/// validated against the selected adapter descriptor. See the +/// [configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct FabricConfig { /// Config schema version. @@ -28,6 +32,12 @@ pub struct FabricConfig { /// Model aliases. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub models: BTreeMap, + /// Portable system instructions for the selected harness. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_prompt: Option, + /// Maximum number of harness turns within one invocation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_turns: Option, /// Runtime input/output contract. pub runtime: RuntimeConfig, /// Environment where the harness or its tools execute. @@ -56,14 +66,31 @@ pub struct FabricConfig { /// Harness-neutral tool capability configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ToolsConfig { - /// Adapter-native tool names or toolset names to block. + /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, + /// Adapter-native toolset selection and blocking policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub toolsets: Option, /// Additive tool configuration fields. #[serde(default, flatten)] pub extensions: BTreeMap, } +/// Harness-neutral toolset capability configuration. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ToolsetConfig { + /// Toolsets to expose. `None` preserves the adapter default; an empty list exposes none. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option>, + /// Toolsets to block. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub blocked: Vec, + /// Additive toolset configuration fields. + #[serde(default, flatten)] + pub extensions: BTreeMap, +} + /// Human-readable metadata. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct MetadataConfig { @@ -328,9 +355,9 @@ pub struct AdapterRequirements { /// Adapter config support. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AdapterConfigSupport { - /// Fabric config areas or policy paths accepted by this adapter. + /// Normalized Fabric config fields accepted by this adapter. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub accepts: Vec, + pub accepts: Vec, /// Harness-native files generated by this adapter. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub generates: Vec, @@ -339,6 +366,46 @@ pub struct AdapterConfigSupport { pub extensions: BTreeMap, } +/// Adapter-translated normalized Fabric configuration fields. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, +)] +pub enum AdapterConfigField { + /// Normalized model selection and credentials. + #[serde(rename = "models")] + Models, + /// Custom model endpoint. + #[serde(rename = "models.base_url")] + ModelBaseUrl, + /// Model temperature. + #[serde(rename = "models.temperature")] + ModelTemperature, + /// Portable system instructions. + #[serde(rename = "system_prompt")] + SystemPrompt, + /// Per-invocation harness turn limit. + #[serde(rename = "max_turns")] + MaxTurns, + /// Adapter-native tool names to block. + #[serde(rename = "tools.blocked")] + BlockedTools, + /// Harness-defined toolsets to expose. + #[serde(rename = "tools.toolsets.enabled")] + EnabledToolsets, + /// Harness-defined toolsets to block. + #[serde(rename = "tools.toolsets.blocked")] + BlockedToolsets, + /// Harness-native MCP servers. + #[serde(rename = "mcp")] + Mcp, + /// Harness-native skills. + #[serde(rename = "skills")] + Skills, + /// Typed telemetry configuration. Exact support remains declared by telemetry provider. + #[serde(rename = "telemetry")] + Telemetry, +} + /// Adapter telemetry support. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AdapterTelemetrySupport { @@ -407,6 +474,9 @@ pub struct ModelConfig { /// Optional environment variable containing an API key. #[serde(default, skip_serializing_if = "Option::is_none")] pub api_key_env: Option, + /// Optional provider endpoint URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_url: Option, /// Provider-specific settings. #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub settings: serde_json::Map, @@ -427,6 +497,9 @@ pub struct RuntimeConfig { /// Artifact directory. #[serde(default, skip_serializing_if = "Option::is_none")] pub artifacts: Option, + /// Maximum duration of one invocation in seconds. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_seconds: Option, /// Additive normalized runtime fields. #[serde(default, flatten)] pub extensions: BTreeMap, @@ -457,6 +530,9 @@ pub struct EnvironmentConfig { /// Artifact path inside or outside the provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub artifacts: Option, + /// Environment variables visible to the harness and its tools. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub env: BTreeMap, /// Provider connection metadata, such as server URL, credential reference, or namespace. #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub connection: serde_json::Map, @@ -1002,9 +1078,96 @@ fn validate_config(config: &FabricConfig) -> Result<()> { available: Vec::new(), }); } + if config.max_turns == Some(0) { + return invalid_config("max_turns", "must be greater than zero"); + } + if let Some(timeout) = config.runtime.timeout_seconds + && (!timeout.is_finite() + || timeout <= 0.0 + || std::time::Duration::try_from_secs_f64(timeout).is_err()) + { + return invalid_config( + "runtime.timeout_seconds", + "must be a finite number greater than zero", + ); + } + for (role, model) in &config.models { + if model.provider.trim().is_empty() + || model.provider.trim() != model.provider + || model.provider.to_ascii_lowercase() != model.provider + { + return invalid_config( + format!("models.{role}.provider"), + "must be a non-empty lowercase identifier", + ); + } + if model.model.trim().is_empty() { + return invalid_config(format!("models.{role}.model"), "must be a non-empty string"); + } + if model + .api_key_env + .as_ref() + .is_some_and(|name| name.trim().is_empty()) + { + return invalid_config( + format!("models.{role}.api_key_env"), + "must be a non-empty string", + ); + } + if let Some(base_url) = &model.base_url + && base_url.trim().is_empty() + { + return invalid_config( + format!("models.{role}.base_url"), + "must be a non-empty string", + ); + } + if model.settings.contains_key("base_url") { + return invalid_config( + format!("models.{role}.settings.base_url"), + "is not supported; use models..base_url", + ); + } + } + if let Some(environment) = &config.environment { + for name in environment.env.keys() { + if name.trim().is_empty() { + return invalid_config("environment.env", "variable names must not be empty"); + } + } + } + if let Some(tools) = &config.tools { + validate_names("tools.blocked", &tools.blocked)?; + if let Some(toolsets) = &tools.toolsets { + if let Some(enabled) = &toolsets.enabled { + validate_names("tools.toolsets.enabled", enabled)?; + if let Some(name) = enabled.iter().find(|name| toolsets.blocked.contains(name)) { + return invalid_config( + "tools.toolsets", + format!("`{name}` cannot be both enabled and blocked"), + ); + } + } + validate_names("tools.toolsets.blocked", &toolsets.blocked)?; + } + } Ok(()) } +fn validate_names(field: &str, names: &[String]) -> Result<()> { + if names.iter().any(|name| name.trim().is_empty()) { + return invalid_config(field, "entries must be non-empty strings"); + } + Ok(()) +} + +fn invalid_config(field: impl Into, reason: impl Into) -> Result { + Err(FabricError::InvalidConfig { + field: field.into(), + reason: reason.into(), + }) +} + /// Resolve a typed Fabric config into a runnable plan. /// /// Callers provide an already-composed typed config and the explicit base @@ -1061,10 +1224,12 @@ fn resolve_run_plan( let descriptor = adapter_descriptor .as_ref() .map(|adapter| &adapter.descriptor); + validate_adapter_config_compatibility(&config, descriptor)?; let resolution = resolve_resolution(&config, descriptor)?; let environment_plan = resolve_environment_plan(&config, &base_dir); validate_control_location(descriptor, environment_plan.as_ref())?; let capability_plan = resolve_capability_plan(&config, &base_dir, adapter_descriptor.as_ref()); + validate_capability_plan_compatibility(&capability_plan, descriptor)?; let capabilities = resolve_runtime_capabilities(&config, descriptor); let telemetry_plan = resolve_telemetry_plan(&config, descriptor)?; Ok(RunPlan { @@ -1080,6 +1245,93 @@ fn resolve_run_plan( }) } +fn validate_capability_plan_compatibility( + capability_plan: &CapabilityPlan, + descriptor: Option<&AdapterDescriptor>, +) -> Result<()> { + let Some(route) = capability_plan + .routes + .iter() + .find(|route| route.target == CapabilityTarget::Unsupported) + else { + return Ok(()); + }; + let field = match route.kind { + CapabilityKind::Mcp => format!("mcp.servers.{}", route.name), + CapabilityKind::Skills => "skills".to_string(), + CapabilityKind::Tools => route.name.clone(), + }; + Err(FabricError::AdapterCompatibility { + adapter_id: descriptor + .map(|descriptor| descriptor.adapter_id.clone()) + .unwrap_or_else(|| "unknown".to_string()), + field, + reason: route.reason.clone(), + }) +} + +fn validate_adapter_config_compatibility( + config: &FabricConfig, + descriptor: Option<&AdapterDescriptor>, +) -> Result<()> { + let Some(descriptor) = descriptor else { + return Ok(()); + }; + let accepts = |field: AdapterConfigField| descriptor.config.accepts.contains(&field); + let incompatible = |field: String, reason: String| { + Err(FabricError::AdapterCompatibility { + adapter_id: descriptor.adapter_id.clone(), + reason, + field, + }) + }; + + if config.system_prompt.is_some() && !accepts(AdapterConfigField::SystemPrompt) { + return incompatible( + "system_prompt".to_string(), + "the adapter does not declare an equivalent native mapping".to_string(), + ); + } + if config.max_turns.is_some() && !accepts(AdapterConfigField::MaxTurns) { + return incompatible( + "max_turns".to_string(), + "the adapter does not declare an equivalent native mapping".to_string(), + ); + } + if !config.models.is_empty() && !accepts(AdapterConfigField::Models) { + return incompatible( + "models".to_string(), + "the adapter does not consume normalized model configuration".to_string(), + ); + } + let selected_model = match (config.models.get_key_value("default"), config.models.len()) { + (Some(model), _) => Some(model), + (None, 0) => None, + (None, 1) => config.models.first_key_value(), + (None, _) => { + return incompatible( + "models".to_string(), + "multiple model roles are configured and no default role selects one".to_string(), + ); + } + }; + if let Some((role, model)) = selected_model { + if model.base_url.is_some() && !accepts(AdapterConfigField::ModelBaseUrl) { + return incompatible( + format!("models.{role}.base_url"), + "the adapter does not declare custom endpoint support".to_string(), + ); + } + if model.temperature.is_some() && !accepts(AdapterConfigField::ModelTemperature) { + return incompatible( + format!("models.{role}.temperature"), + "the adapter does not declare an equivalent native mapping".to_string(), + ); + } + } + Ok(()) +} + fn resolve_adapter_descriptor( config: &FabricConfig, base_dir: &Path, @@ -1197,6 +1449,7 @@ fn resolve_environment_plan(config: &FabricConfig, base_dir: &Path) -> Option, ) -> CapabilityPlan { - let accepts = |area: &str| { + let accepts = |field: AdapterConfigField| { adapter_descriptor - .map(|adapter| { - adapter - .descriptor - .config - .accepts - .iter() - .any(|accepted| accepted == area) - }) + .map(|adapter| adapter.descriptor.config.accepts.contains(&field)) .unwrap_or(false) }; let skill_paths: Vec = config @@ -1231,7 +1477,7 @@ fn resolve_capability_plan( .collect() }) .unwrap_or_default(); - let skills_are_native = !skill_paths.is_empty() && accepts("skills"); + let skills_are_native = !skill_paths.is_empty() && accepts(AdapterConfigField::Skills); let mcp_servers: BTreeMap = config .mcp .as_ref() @@ -1256,15 +1502,26 @@ fn resolve_capability_plan( .as_ref() .map(|tools| tools.blocked.clone()) .unwrap_or_default(); - let tools_configured = !blocked_tools.is_empty(); - let tools_are_native = tools_configured && accepts("tools.blocked"); + let toolsets = config + .tools + .as_ref() + .and_then(|tools| tools.toolsets.clone()); + let blocked_tools_configured = !blocked_tools.is_empty(); + let enabled_toolsets_configured = toolsets + .as_ref() + .is_some_and(|toolsets| toolsets.enabled.is_some()); + let blocked_toolsets_configured = toolsets + .as_ref() + .is_some_and(|toolsets| !toolsets.blocked.is_empty()); + let tools_configured = + blocked_tools_configured || enabled_toolsets_configured || blocked_toolsets_configured; let mut native = CapabilityTargetPlan::default(); let managed = CapabilityTargetPlan::default(); let mut unsupported = CapabilityTargetPlan::default(); let mut routes = Vec::new(); - if tools_configured { - if tools_are_native { + if blocked_tools_configured { + if accepts(AdapterConfigField::BlockedTools) { native.tools_configured = true; routes.push(CapabilityRoute { kind: CapabilityKind::Tools, @@ -1283,6 +1540,43 @@ fn resolve_capability_plan( }); } } + for (configured, support, field, description) in [ + ( + enabled_toolsets_configured, + AdapterConfigField::EnabledToolsets, + "tools.toolsets.enabled", + "enabled-toolsets selection", + ), + ( + blocked_toolsets_configured, + AdapterConfigField::BlockedToolsets, + "tools.toolsets.blocked", + "blocked-toolsets policy", + ), + ] { + if !configured { + continue; + } + if accepts(support) { + native.tools_configured = true; + routes.push(CapabilityRoute { + kind: CapabilityKind::Tools, + name: field.to_string(), + target: CapabilityTarget::HarnessNative, + reason: format!("selected adapter explicitly supports the Fabric {description}"), + }); + } else { + unsupported.tools_configured = true; + routes.push(CapabilityRoute { + kind: CapabilityKind::Tools, + name: field.to_string(), + target: CapabilityTarget::Unsupported, + reason: format!( + "selected adapter does not explicitly declare {description} support" + ), + }); + } + } if !skill_paths.is_empty() { if skills_are_native { @@ -1305,8 +1599,8 @@ fn resolve_capability_plan( } for (name, server) in &mcp_servers { - let can_map_native = - accepts("mcp") && matches!(server.exposure, McpExposure::HarnessNative); + let can_map_native = accepts(AdapterConfigField::Mcp) + && matches!(server.exposure, McpExposure::HarnessNative); if can_map_native { native.mcp_servers.insert(name.clone(), server.clone()); routes.push(CapabilityRoute { @@ -1337,6 +1631,7 @@ fn resolve_capability_plan( CapabilityPlan { tools: ToolsPlan { blocked: blocked_tools, + toolsets, }, tools_configured, skill_paths, @@ -1525,6 +1820,9 @@ pub struct EnvironmentPlan { /// Resolved artifact path. #[serde(default, skip_serializing_if = "Option::is_none")] pub artifacts: Option, + /// Environment variables visible to the harness and its tools. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub env: BTreeMap, /// Provider connection metadata. #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub connection: serde_json::Map, @@ -1568,9 +1866,12 @@ pub struct CapabilityPlan { /// Normalized tool policy for a run. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ToolsPlan { - /// Adapter-native tool names or toolset names to block. + /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, + /// Adapter-native toolset selection and blocking policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub toolsets: Option, } /// Capabilities routed to one target. @@ -1775,6 +2076,342 @@ mod tests { assert!(value.get("effective_config").is_none()); } + #[test] + fn normalized_fields_survive_planning() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.system_prompt = Some("Be concise.".to_string()); + config.max_turns = Some(7); + config.runtime.timeout_seconds = Some(12.5); + config.environment.as_mut().expect("environment").env = + BTreeMap::from([("VISIBLE".to_string(), "yes".to_string())]); + config.models.insert( + "default".to_string(), + ModelConfig { + provider: "nvidia".to_string(), + model: "nvidia/test".to_string(), + temperature: Some(0.2), + api_key_env: Some("NVIDIA_API_KEY".to_string()), + base_url: Some("https://models.example/v1".to_string()), + settings: serde_json::Map::new(), + extensions: BTreeMap::new(), + }, + ); + config.tools = Some(ToolsConfig { + blocked: Vec::new(), + toolsets: Some(ToolsetConfig { + enabled: Some(vec!["terminal".to_string()]), + blocked: vec!["browser".to_string()], + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + + let plan = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-normalized")) + .expect("normalized plan"); + + assert_eq!(plan.config.system_prompt.as_deref(), Some("Be concise.")); + assert_eq!(plan.config.max_turns, Some(7)); + assert_eq!(plan.config.runtime.timeout_seconds, Some(12.5)); + assert_eq!( + plan.environment_plan + .as_ref() + .and_then(|environment| environment.env.get("VISIBLE")), + Some(&"yes".to_string()) + ); + assert_eq!( + plan.capability_plan + .tools + .toolsets + .as_ref() + .and_then(|toolsets| toolsets.enabled.as_ref()), + Some(&vec!["terminal".to_string()]) + ); + assert!(plan.capability_plan.routes.iter().any(|route| { + route.name == "tools.toolsets.enabled" + && route.target == CapabilityTarget::HarnessNative + })); + } + + #[test] + fn unsupported_normalized_scalar_reports_adapter_config_incompatibility() { + for adapter_id in ["nvidia.fabric.codex", "nvidia.fabric.langchain.deepagents"] { + let mut config = typed_config(adapter_id); + config.max_turns = Some(3); + + let error = resolve_run_plan_from_config( + config, + ResolveContext::new("/tmp/fabric-incompatible"), + ) + .expect_err("adapter does not advertise max_turns"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id: actual, + field, + .. + } if actual == adapter_id && field == "max_turns" + )); + } + } + + #[test] + fn unsupported_model_temperature_reports_canonical_field() { + for (adapter_id, provider) in [ + ("nvidia.fabric.claude", "anthropic"), + ("nvidia.fabric.codex", "openai"), + ] { + let mut config = typed_config(adapter_id); + config.models.insert( + "review".to_string(), + ModelConfig { + provider: provider.to_string(), + model: "test-model".to_string(), + temperature: Some(0.2), + api_key_env: None, + base_url: None, + settings: serde_json::Map::new(), + extensions: BTreeMap::new(), + }, + ); + + let error = resolve_run_plan_from_config( + config, + ResolveContext::new("/tmp/fabric-temperature"), + ) + .expect_err("adapter does not advertise model temperature"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id: actual, + field, + .. + } if actual == adapter_id && field == "models.review.temperature" + )); + } + } + + #[test] + fn unsupported_toolsets_report_canonical_field() { + for adapter_id in [ + "nvidia.fabric.claude", + "nvidia.fabric.codex", + "nvidia.fabric.langchain.deepagents", + ] { + let mut config = typed_config(adapter_id); + config.tools = Some(ToolsConfig { + blocked: Vec::new(), + toolsets: Some(ToolsetConfig { + enabled: Some(vec!["terminal".to_string()]), + blocked: Vec::new(), + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + + let error = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-toolsets")) + .expect_err("adapter does not advertise enabled toolsets"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id: actual, + field, + .. + } if actual == adapter_id && field == "tools.toolsets.enabled" + )); + } + } + + #[test] + fn model_settings_rejects_normalized_base_url_alias() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.models.insert( + "default".to_string(), + ModelConfig { + provider: "nvidia".to_string(), + model: "nvidia/test".to_string(), + temperature: None, + api_key_env: None, + base_url: None, + settings: serde_json::Map::from_iter([( + "base_url".to_string(), + Value::String("https://legacy.example/v1".to_string()), + )]), + extensions: BTreeMap::new(), + }, + ); + + let error = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-model-alias")) + .expect_err("normalized aliases must be rejected"); + + assert!(matches!( + error, + FabricError::InvalidConfig { field, reason } + if field == "models.default.settings.base_url" + && reason.contains("models..base_url") + )); + } + + #[test] + fn sole_named_model_is_selected_without_forcing_default_role() { + let mut config = typed_config("nvidia.fabric.claude"); + config.models.insert( + "review".to_string(), + ModelConfig { + provider: "anthropic".to_string(), + model: "claude-test".to_string(), + temperature: None, + api_key_env: None, + base_url: None, + settings: serde_json::Map::new(), + extensions: BTreeMap::new(), + }, + ); + + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-model-role")) + .expect("sole named model"); + } + + #[test] + fn multiple_models_require_an_explicit_default_role() { + let mut config = typed_config("nvidia.fabric.claude"); + for role in ["fast", "slow"] { + config.models.insert( + role.to_string(), + ModelConfig { + provider: "anthropic".to_string(), + model: format!("claude-{role}"), + temperature: None, + api_key_env: None, + base_url: None, + settings: serde_json::Map::new(), + extensions: BTreeMap::new(), + }, + ); + } + + let error = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-model-role")) + .expect_err("ambiguous model roles"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { field, reason, .. } + if field == "models" && reason.contains("no default role") + )); + } + + #[test] + fn tool_and_toolset_policies_are_routed_independently() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.tools = Some(ToolsConfig { + blocked: Vec::new(), + toolsets: Some(ToolsetConfig { + enabled: Some(Vec::new()), + blocked: vec!["browser".to_string()], + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + + let plan = resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-tools")) + .expect("tool capability plan"); + + assert!(plan.capability_plan.routes.iter().any(|route| { + route.name == "tools.toolsets.enabled" + && route.target == CapabilityTarget::HarnessNative + })); + assert!(plan.capability_plan.routes.iter().any(|route| { + route.name == "tools.toolsets.blocked" + && route.target == CapabilityTarget::HarnessNative + })); + } + + #[test] + fn unsupported_tool_policy_fails_during_planning() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.tools = Some(ToolsConfig { + blocked: vec!["Bash".to_string()], + toolsets: None, + extensions: BTreeMap::new(), + }); + + let error = resolve_run_plan_from_config( + config, + ResolveContext::new("/tmp/fabric-unsupported-tools"), + ) + .expect_err("Hermes does not support per-tool blocking"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id, + field, + .. + } if adapter_id == "nvidia.fabric.hermes" && field == "tools.blocked" + )); + } + + #[test] + fn unsupported_mcp_reports_canonical_config_path() { + let mut config = typed_config("nvidia.fabric.claude"); + config.mcp = Some(McpConfig { + servers: BTreeMap::from([( + "docs".to_string(), + McpServerConfig { + transport: "streamable-http".to_string(), + url: "https://mcp.example".to_string(), + exposure: McpExposure::FabricManaged, + extensions: BTreeMap::new(), + }, + )]), + extensions: BTreeMap::new(), + }); + + let error = resolve_run_plan_from_config( + config, + ResolveContext::new("/tmp/fabric-unsupported-mcp"), + ) + .expect_err("Fabric-managed MCP is not implemented"); + + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id, + field, + .. + } if adapter_id == "nvidia.fabric.claude" && field == "mcp.servers.docs" + )); + } + + #[test] + fn overlapping_toolset_policy_is_invalid() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.tools = Some(ToolsConfig { + blocked: Vec::new(), + toolsets: Some(ToolsetConfig { + enabled: Some(vec!["browser".to_string()]), + blocked: vec!["browser".to_string()], + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + + let error = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-invalid-tools")) + .expect_err("overlapping toolset policy"); + + assert!(matches!( + error, + FabricError::InvalidConfig { field, .. } if field == "tools.toolsets" + )); + } + #[test] fn loads_and_validates_json_adapter_descriptor() { let descriptor = diff --git a/crates/fabric-core/src/error.rs b/crates/fabric-core/src/error.rs index ce2de11b8..27b03c1ff 100644 --- a/crates/fabric-core/src/error.rs +++ b/crates/fabric-core/src/error.rs @@ -64,6 +64,24 @@ pub enum FabricError { /// Validation message. message: String, }, + /// A normalized Fabric config field is invalid. + #[error("invalid Fabric configuration at `{field}`: {reason}")] + InvalidConfig { + /// Canonical configuration path. + field: String, + /// Validation failure. + reason: String, + }, + /// A valid normalized field cannot be implemented by the selected adapter. + #[error("adapter `{adapter_id}` cannot implement configuration at `{field}`: {reason}")] + AdapterCompatibility { + /// Selected adapter id. + adapter_id: String, + /// Canonical configuration path. + field: String, + /// Compatibility failure. + reason: String, + }, /// A requested schema is not known. #[error("unknown schema `{schema}`; available schemas: {available:?}")] UnknownSchema { @@ -103,14 +121,6 @@ pub enum FabricError { /// Bounded adapter-host diagnostics. diagnostics: String, }, - /// The selected harness cannot enforce the configured blocked-tools policy. - #[error("harness `{harness}` cannot enforce configured blocked tools: {reason}")] - UnsupportedToolsPolicy { - /// Harness type. - harness: String, - /// Capability-routing explanation. - reason: String, - }, /// A runtime handle was used with a different run plan than the one that created it. #[error( "runtime handle does not match run plan for `{field}`: expected `{expected}` but found `{actual}` (runtime `{runtime_id}`)" diff --git a/crates/fabric-core/src/lib.rs b/crates/fabric-core/src/lib.rs index c2fc71dcd..717c55f23 100644 --- a/crates/fabric-core/src/lib.rs +++ b/crates/fabric-core/src/lib.rs @@ -10,14 +10,15 @@ pub mod runtime; pub mod schema; pub use config::{ - ADAPTER_CONTRACT_VERSION, AdapterConfigSupport, AdapterDescriptor, AdapterDescriptorSource, - AdapterKind, AdapterRequirements, AdapterTelemetryProviderSupport, AdapterTelemetrySupport, - CapabilityPlan, ControlLocation, EnvironmentConfig, EnvironmentOwnership, EnvironmentPlan, - FabricConfig, HarnessConfig, McpConfig, McpExposure, McpServerPlan, MetadataConfig, - ModelConfig, ResolutionStrategy, ResolveContext, ResolvedAdapterDescriptor, RunPlan, - RuntimeCapabilities, RuntimeConfig, SkillConfig, TelemetryConfig, TelemetryPlan, - TelemetryProvider, TelemetryProviderConfig, load_adapter_descriptor, - resolve_run_plan_from_config, resolve_run_plan_from_config_with_adapter_directories, + ADAPTER_CONTRACT_VERSION, AdapterConfigField, AdapterConfigSupport, AdapterDescriptor, + AdapterDescriptorSource, AdapterKind, AdapterRequirements, AdapterTelemetryProviderSupport, + AdapterTelemetrySupport, CapabilityPlan, ControlLocation, EnvironmentConfig, + EnvironmentOwnership, EnvironmentPlan, FabricConfig, HarnessConfig, McpConfig, McpExposure, + McpServerPlan, MetadataConfig, ModelConfig, ResolutionStrategy, ResolveContext, + ResolvedAdapterDescriptor, RunPlan, RuntimeCapabilities, RuntimeConfig, SkillConfig, + TelemetryConfig, TelemetryPlan, TelemetryProvider, TelemetryProviderConfig, ToolsConfig, + ToolsetConfig, load_adapter_descriptor, resolve_run_plan_from_config, + resolve_run_plan_from_config_with_adapter_directories, }; pub use doctor::{DoctorCheck, DoctorReport, DoctorStatus, doctor_plan}; pub use error::{FabricError, Result}; diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index 4c5b9b806..506535430 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -20,8 +20,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::config::{ - AdapterKind, CapabilityKind, CapabilityPlan, CapabilityTarget, ControlLocation, - EnvironmentOwnership, FabricConfig, RunPlan, TelemetryPlan, + AdapterKind, CapabilityPlan, CapabilityTarget, ControlLocation, EnvironmentOwnership, + FabricConfig, RunPlan, TelemetryPlan, }; use crate::error::{FabricError, Result}; @@ -242,6 +242,9 @@ pub struct EnvironmentHandle { /// Artifact root visible to the harness runtime. #[serde(default, skip_serializing_if = "Option::is_none")] pub artifacts: Option, + /// Environment variables visible to the harness and its tools. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub env: BTreeMap, /// Whether Fabric owns the environment resource. pub ownership: EnvironmentOwnership, /// Provider connection metadata. @@ -475,6 +478,7 @@ pub fn prepare_environment(plan: &RunPlan) -> Result { ownership, workspace, artifacts, + environment_env, connection_settings, environment_metadata, settings, @@ -485,6 +489,7 @@ pub fn prepare_environment(plan: &RunPlan) -> Result { environment.ownership, environment.workspace.clone(), environment.artifacts.clone(), + environment.env.clone(), environment.connection.clone(), environment.metadata.clone(), environment.settings.clone(), @@ -500,6 +505,7 @@ pub fn prepare_environment(plan: &RunPlan) -> Result { .artifacts .as_ref() .map(|artifacts| resolve_path(&plan.base_dir, artifacts)), + BTreeMap::new(), serde_json::Map::new(), serde_json::Map::new(), serde_json::Map::new(), @@ -524,6 +530,7 @@ pub fn prepare_environment(plan: &RunPlan) -> Result { control_location, workspace, artifacts, + env: environment_env, ownership, connection, metadata, @@ -532,7 +539,7 @@ pub fn prepare_environment(plan: &RunPlan) -> Result { /// Start or connect to a harness runtime. pub fn start_runtime(plan: &RunPlan) -> Result { - validate_blocked_tools_support(plan)?; + validate_adapter_compatibility(plan)?; let environment = prepare_environment(plan)?; if uses_local_host(plan) { return LocalHostAdapter.start(plan, environment); @@ -549,7 +556,7 @@ pub fn invoke_runtime( runtime: &RuntimeHandle, request: RunRequest, ) -> Result { - validate_blocked_tools_support(plan)?; + validate_adapter_compatibility(plan)?; validate_runtime_handle(plan, runtime)?; if uses_local_host(plan) { return LocalHostAdapter.invoke(plan, runtime, request); @@ -560,12 +567,16 @@ pub fn invoke_runtime( }) } -fn validate_blocked_tools_support(plan: &RunPlan) -> Result<()> { - if let Some(route) = plan.capability_plan.routes.iter().find(|route| { - route.kind == CapabilityKind::Tools && route.target == CapabilityTarget::Unsupported - }) { - return Err(FabricError::UnsupportedToolsPolicy { - harness: harness(plan), +fn validate_adapter_compatibility(plan: &RunPlan) -> Result<()> { + if let Some(route) = plan + .capability_plan + .routes + .iter() + .find(|route| route.target == CapabilityTarget::Unsupported) + { + return Err(FabricError::AdapterCompatibility { + adapter_id: adapter_id(plan).unwrap_or_else(|| harness(plan)), + field: route.name.clone(), reason: route.reason.clone(), }); } @@ -647,6 +658,7 @@ struct RuntimeEnvironmentBinding<'a> { control_location: ControlLocation, workspace: &'a Option, artifacts: &'a Option, + env: &'a BTreeMap, ownership: EnvironmentOwnership, connection: &'a BTreeMap, metadata: &'a BTreeMap, @@ -658,6 +670,7 @@ fn runtime_environment_binding(environment: &EnvironmentHandle) -> RuntimeEnviro control_location: environment.control_location, workspace: &environment.workspace, artifacts: &environment.artifacts, + env: &environment.env, ownership: environment.ownership, connection: &environment.connection, metadata: &environment.metadata, @@ -894,7 +907,16 @@ fn run_local_host_adapter( runtime: &RuntimeHandle, request: RunRequest, ) -> Result { - run_local_host_adapter_with_timeout(plan, runtime, request, LOCAL_HOST_INVOKE_TIMEOUT) + let timeout = match plan.config.runtime.timeout_seconds { + Some(seconds) => { + Duration::try_from_secs_f64(seconds).map_err(|_| FabricError::InvalidConfig { + field: "runtime.timeout_seconds".to_string(), + reason: "must be a finite number greater than zero".to_string(), + })? + } + None => LOCAL_HOST_INVOKE_TIMEOUT, + }; + run_local_host_adapter_with_timeout(plan, runtime, request, timeout) } fn run_local_host_adapter_with_timeout( @@ -1336,7 +1358,8 @@ fn process_local_host_command( command .args(&command_args) .current_dir(cwd) - .envs(&settings.env); + .envs(&settings.env) + .envs(&runtime.environment.env); let display = std::iter::once(command_path.to_string_lossy().into_owned()) .chain(command_args) .collect::>() @@ -1359,7 +1382,8 @@ fn python_local_host_command(plan: &RunPlan, runtime: &RuntimeHandle) -> Result< .arg(&settings.module) .args(&settings.args) .current_dir(cwd) - .envs(&settings.env); + .envs(&settings.env) + .envs(&runtime.environment.env); Ok(( command, format!("{} -m {}", python.to_string_lossy(), settings.module), @@ -2460,6 +2484,7 @@ for line in sys.stdin: "runtime_id": invocation["runtime_context"]["runtime_id"], "invocation_id": invocation["runtime_context"]["invocation_id"], "request_id": invocation["runtime_context"]["request_id"], + "normalized_env": os.environ.get("FABRIC_NORMALIZED_ENV"), } if MODE == "adapter_reported_failure": output.update({ @@ -2494,17 +2519,15 @@ for line in sys.stdin: "env": {"FABRIC_FAKE_HOST_MODE": mode}, }, }, - "models": { - "default": { - "provider": "test", - "model": "test-model", - }, - }, "runtime": { "input_schema": "text", "output_schema": "text", "artifacts": "./artifacts", }, + "environment": { + "provider": "local", + "env": {"FABRIC_NORMALIZED_ENV": "visible"}, + }, }); if relay { config_value["telemetry"] = serde_json::json!({ @@ -2538,6 +2561,7 @@ for line in sys.stdin: assert_eq!(first.output["invocation_count"], serde_json::json!(1)); assert_eq!(second.output["invocation_count"], serde_json::json!(2)); assert_eq!(first.output["input"], serde_json::json!("first")); + assert_eq!(first.output["normalized_env"], serde_json::json!("visible")); assert_eq!(second.output["input"], serde_json::json!("second")); assert_eq!(first.metadata["host_pid"], second.metadata["host_pid"]); assert_eq!( diff --git a/examples/code_review_agent/config.py b/examples/code_review_agent/config.py index 906f8e169..803eae0ba 100644 --- a/examples/code_review_agent/config.py +++ b/examples/code_review_agent/config.py @@ -36,7 +36,7 @@ def base_config() -> FabricConfig: harness=HarnessConfig( adapter_id="nvidia.fabric.hermes", resolution="preinstalled", - settings={"workspace": WORKSPACE}, + settings={}, ), models={ "default": ModelConfig( @@ -70,17 +70,16 @@ def hermes_config() -> FabricConfig: adapter_id="nvidia.fabric.hermes", resolution="preinstalled", settings={ - "workspace": WORKSPACE, - "hermes_home": "./artifacts/hermes-home", - "base_url": "https://integrate.api.nvidia.com/v1", - "max_iterations": 1, "max_tokens": 512, - "temperature": 0.0, "reasoning_config": {"effort": "none"}, - "enabled_toolsets": [], - "system_prompt": "You are a concise smoke test assistant.", }, ) + model = config.models["default"] + assert isinstance(model, ModelConfig) + model.base_url = "https://integrate.api.nvidia.com/v1" + config.system_prompt = "You are a concise smoke test assistant." + config.max_turns = 1 + config.configure_toolsets(enabled=[]) config.runtime = RuntimeConfig( input_schema="chat", output_schema="message", @@ -128,11 +127,9 @@ def deepagents_config() -> FabricConfig: config.harness = HarnessConfig( adapter_id="nvidia.fabric.langchain.deepagents", resolution="preinstalled", - settings={ - "workspace": WORKSPACE, - "system_prompt": "You are a concise smoke test assistant.", - }, + settings={}, ) + config.system_prompt = "You are a concise smoke test assistant." config.runtime = RuntimeConfig( input_schema="chat", output_schema="message", @@ -151,8 +148,9 @@ def claude_config() -> FabricConfig: """Return the complete Claude adapter variant. The Claude adapter reads the working directory from ``environment.workspace`` - and rejects ``cwd`` in ``harness.settings``; only Claude-specific controls - such as ``system_prompt`` and ``permission_mode`` belong there. + and reads portable instructions from ``FabricConfig.system_prompt``. + Claude-specific controls such as ``permission_mode`` stay in + ``harness.settings``. """ config = base_config().model_copy(deep=True) @@ -160,10 +158,12 @@ def claude_config() -> FabricConfig: adapter_id="nvidia.fabric.claude", resolution="preinstalled", settings={ - "system_prompt": "You are a concise code reviewer. Point out correctness bugs and risks.", "permission_mode": "dontAsk", }, ) + config.system_prompt = ( + "You are a concise code reviewer. Point out correctness bugs and risks." + ) config.models = { "default": ModelConfig( provider="anthropic", diff --git a/examples/harbor/swebench/adapters/claude/fabric-adapter.json b/examples/harbor/swebench/adapters/claude/fabric-adapter.json index 9f8996f94..5ea209bb2 100644 --- a/examples/harbor/swebench/adapters/claude/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/claude/fabric-adapter.json @@ -7,7 +7,16 @@ "module": "nemo_fabric_adapters.claude.adapter" }, "config": { - "accepts": ["models", "tools", "tools.blocked", "mcp", "skills", "telemetry"] + "accepts": [ + "models", + "models.base_url", + "system_prompt", + "max_turns", + "tools.blocked", + "mcp", + "skills", + "telemetry" + ] }, "telemetry": { "providers": { diff --git a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json index ef60fb920..40927fb41 100644 --- a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json @@ -14,8 +14,12 @@ "config": { "accepts": [ "models", - "tools", - "tools.blocked", + "models.base_url", + "models.temperature", + "system_prompt", + "max_turns", + "tools.toolsets.enabled", + "tools.toolsets.blocked", "mcp", "skills", "telemetry" diff --git a/python/src/nemo_fabric/__init__.py b/python/src/nemo_fabric/__init__.py index e9836d2b7..aff7b3d8a 100644 --- a/python/src/nemo_fabric/__init__.py +++ b/python/src/nemo_fabric/__init__.py @@ -35,6 +35,7 @@ from nemo_fabric.models import TelemetryConfig from nemo_fabric.models import TelemetryProviderConfig from nemo_fabric.models import ToolsConfig +from nemo_fabric.models import ToolsetConfig from nemo_fabric.runtime import Runtime from nemo_fabric.runtime import RuntimeStatus from nemo_fabric.types import AdapterInfo @@ -99,4 +100,5 @@ "TelemetryProviderConfig", "TelemetryRef", "ToolsConfig", + "ToolsetConfig", ] diff --git a/python/src/nemo_fabric/integrations/harbor/fabric_agent.py b/python/src/nemo_fabric/integrations/harbor/fabric_agent.py index 6d21d9316..0a261e15d 100644 --- a/python/src/nemo_fabric/integrations/harbor/fabric_agent.py +++ b/python/src/nemo_fabric/integrations/harbor/fabric_agent.py @@ -28,6 +28,7 @@ from nemo_fabric import RunRequest from nemo_fabric import RunResult from nemo_fabric import RuntimeConfig +from nemo_fabric import ToolsetConfig from nemo_fabric import ToolsConfig from nemo_fabric.integrations.harbor.models import FabricRunPayload from nemo_fabric.integrations.harbor.models import HarborMcpServer @@ -99,7 +100,14 @@ def __init__( fabric_config_target: str = "/tmp/nemo-fabric-config", fabric_workspace: str = HARBOR_DEFAULT_WORKSPACE, fabric_harness_settings: dict[str, Any] | None = None, + fabric_model_base_url: str | None = None, + fabric_system_prompt: str | None = None, + fabric_max_turns: int | None = None, + fabric_runtime_timeout_seconds: float | None = None, + fabric_environment_env: dict[str, str] | None = None, fabric_blocked_tools: list[str] | None = None, + fabric_enabled_toolsets: list[str] | None = None, + fabric_blocked_toolsets: list[str] | None = None, fabric_telemetry: Literal["none", "relay"] = "none", fabric_python: str = "python3", fabric_package: str | None = None, @@ -136,7 +144,18 @@ def __init__( self.fabric_config_target = fabric_config_target self.fabric_workspace = str(workspace) self.fabric_harness_settings = dict(fabric_harness_settings or {}) + self.fabric_model_base_url = fabric_model_base_url + self.fabric_system_prompt = fabric_system_prompt + self.fabric_max_turns = fabric_max_turns + self.fabric_runtime_timeout_seconds = fabric_runtime_timeout_seconds + self.fabric_environment_env = dict(fabric_environment_env or {}) self.fabric_blocked_tools = blocked_tools + self.fabric_enabled_toolsets = ( + list(fabric_enabled_toolsets) + if fabric_enabled_toolsets is not None + else None + ) + self.fabric_blocked_toolsets = list(fabric_blocked_toolsets or []) self.fabric_telemetry = fabric_telemetry self.fabric_python = fabric_python self.fabric_package = fabric_package @@ -259,7 +278,14 @@ def _build_config(self) -> FabricConfig: adapter_id=self.fabric_adapter_id, workspace=self.fabric_workspace, harness_settings=self.fabric_harness_settings, + model_base_url=self.fabric_model_base_url, + system_prompt=self.fabric_system_prompt, + max_turns=self.fabric_max_turns, + timeout_seconds=self.fabric_runtime_timeout_seconds, + environment_env=self.fabric_environment_env, blocked_tools=self.fabric_blocked_tools, + enabled_toolsets=self.fabric_enabled_toolsets, + blocked_toolsets=self.fabric_blocked_toolsets, telemetry=self.fabric_telemetry, model_name=self.model_name, skills_dir=self.skills_dir, @@ -340,7 +366,14 @@ def build_harbor_config( adapter_id: str, workspace: str, harness_settings: dict[str, Any] | None = None, + model_base_url: str | None = None, + system_prompt: str | None = None, + max_turns: int | None = None, + timeout_seconds: float | None = None, + environment_env: dict[str, str] | None = None, blocked_tools: list[str] | None = None, + enabled_toolsets: list[str] | None = None, + blocked_toolsets: list[str] | None = None, telemetry: Literal["none", "relay"] = "none", model_name: str | None = None, skills_dir: str | Path | None = None, @@ -352,6 +385,23 @@ def build_harbor_config( artifact_root = f"{HARBOR_ARTIFACT_ROOT}/{name}" settings = harbor_harness_defaults(adapter_id) settings.update(harness_settings or {}) + if adapter_id == "nvidia.fabric.claude": + if max_turns is None: + max_turns = 75 + if timeout_seconds is None: + timeout_seconds = 1800 + environment_env = { + "IS_SANDBOX": "1", + **(environment_env or {}), + } + toolsets = ( + ToolsetConfig( + enabled=enabled_toolsets, + blocked=list(blocked_toolsets or []), + ) + if enabled_toolsets is not None or blocked_toolsets + else None + ) config = FabricConfig( metadata=MetadataConfig( name=name, @@ -366,19 +416,33 @@ def build_harbor_config( input_schema="text", output_schema="message", artifacts=artifact_root, + timeout_seconds=timeout_seconds, ), environment=EnvironmentConfig( provider="local", workspace=workspace, artifacts=artifact_root, + env=dict(environment_env or {}), + ), + system_prompt=system_prompt, + max_turns=max_turns, + tools=( + ToolsConfig( + blocked=list(blocked_tools or []), + toolsets=toolsets, + ) + if blocked_tools or toolsets is not None + else None ), - tools=(ToolsConfig(blocked=list(blocked_tools)) if blocked_tools else None), ) if model_name: config.models["default"] = ModelConfig( provider=model_provider(model_name), model=model_name, + base_url=model_base_url, ) + elif model_base_url is not None: + raise ValueError("model_base_url requires model_name") for server in mcp_servers: if server.transport == "stdio": config.add_mcp_server( @@ -434,15 +498,11 @@ def harbor_harness_defaults(adapter_id: str) -> dict[str, Any]: if adapter_id == "nvidia.fabric.hermes": return { - "hermes_home": "/tmp/fabric-hermes", "terminal_timeout": 300, } if adapter_id == "nvidia.fabric.claude": return { "permission_mode": "bypassPermissions", - "max_turns": 75, - "timeout_seconds": 1800, - "env": {"IS_SANDBOX": "1"}, } return {} diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index b93d67143..3e2abcfb9 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -111,6 +111,7 @@ class RuntimeConfig(FabricBaseModel): input_schema: str | None = None output_schema: str | None = None artifacts: str | Path | None = None + timeout_seconds: float | None = Field(default=None, gt=0) class EnvironmentConfig(FabricBaseModel): @@ -139,6 +140,10 @@ class EnvironmentConfig(FabricBaseModel): default=None, description="Environment-specific artifact path.", ) + env: dict[str, str] = Field( + default_factory=dict, + description="Environment variables visible to the harness and its tools.", + ) settings: dict[str, Any] = Field( default_factory=dict, description="Provider-specific configuration interpreted by the environment provider.", @@ -160,6 +165,13 @@ class EnvironmentConfig(FabricBaseModel): description="Whether Fabric control code runs outside or inside the environment.", ) + @field_validator("env") + @classmethod + def _validate_env_names(cls, value: dict[str, str]) -> dict[str, str]: + if any(not name.strip() for name in value): + raise ValueError("environment.env variable names must not be empty") + return value + class ModelConfig(FabricBaseModel): """Model alias configuration.""" @@ -168,8 +180,40 @@ class ModelConfig(FabricBaseModel): model: str = Field(min_length=1) api_key_env: str | None = None temperature: float | None = None + base_url: str | None = Field(default=None, min_length=1) settings: dict[str, Any] = Field(default_factory=dict) + @field_validator("provider") + @classmethod + def _validate_provider(cls, value: str) -> str: + if not value.strip() or value != value.strip() or value != value.lower(): + raise ValueError("provider must be a non-empty lowercase identifier") + return value + + @field_validator("model", "api_key_env") + @classmethod + def _validate_nonempty_model_fields(cls, value: str | None) -> str | None: + if value is not None and not value.strip(): + raise ValueError("model fields must be non-empty strings") + return value + + @field_validator("base_url") + @classmethod + def _validate_base_url(cls, value: str | None) -> str | None: + if value is not None and not value.strip(): + raise ValueError("base_url must be a non-empty string") + return value + + @field_validator("settings") + @classmethod + def _reject_normalized_aliases(cls, value: dict[str, Any]) -> dict[str, Any]: + if "base_url" in value: + raise ValueError( + "models..settings.base_url is not supported; use " + "models..base_url" + ) + return value + class SkillConfig(FabricBaseModel): """Skill capability configuration.""" @@ -406,26 +450,63 @@ def remove_provider(self, provider: Literal["relay", "native"]) -> Self: return self +class ToolsetConfig(FabricBaseModel): + """Harness-neutral toolset selection and blocking policy.""" + + enabled: list[str] | None = None + blocked: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_policy(self) -> Self: + for field, values in ( + ("enabled", self.enabled or []), + ("blocked", self.blocked), + ): + if any(not value.strip() for value in values): + raise ValueError(f"tools.toolsets.{field} entries must not be empty") + if self.enabled is not None: + overlap = set(self.enabled).intersection(self.blocked) + if overlap: + name = sorted(overlap)[0] + raise ValueError(f"toolset {name!r} cannot be both enabled and blocked") + return self + + class ToolsConfig(FabricBaseModel): """Harness-neutral tool capability configuration.""" blocked: list[str] = Field(default_factory=list) + toolsets: ToolsetConfig | None = None + + @field_validator("blocked") + @classmethod + def _validate_blocked(cls, value: list[str]) -> list[str]: + if any(not tool.strip() for tool in value): + raise ValueError("tools.blocked entries must not be empty") + return value class FabricConfig(FabricBaseModel): - """SDK-facing typed Fabric agent configuration.""" + """SDK-facing typed Fabric agent configuration. + + Fabric-owned fields apply uniformly. Adapter-translated fields are checked + against the selected descriptor; see the normalized configuration + compatibility table in ``docs/sdk/python.mdx``. + """ schema_version: str = "fabric.agent/v1alpha1" metadata: MetadataConfig harness: HarnessConfig runtime: RuntimeConfig = Field(default_factory=RuntimeConfig) environment: EnvironmentConfig | None = None - models: dict[str, ModelConfig | dict[str, Any]] = Field(default_factory=dict) + models: dict[str, ModelConfig] = Field(default_factory=dict) + system_prompt: str | None = None + max_turns: int | None = Field(default=None, gt=0) mcp: McpConfig | None = None skills: SkillConfig | None = None telemetry: TelemetryConfig | None = None relay: RelayConfig | dict[str, Any] | None = None - tools: ToolsConfig | dict[str, Any] | None = None + tools: ToolsConfig | None = None @classmethod def from_mapping(cls, value: Mapping[str, Any]) -> Self: @@ -490,10 +571,10 @@ def remove_skill_path(self, path: str | Path) -> Self: return self def block_tools(self, *tools: str) -> Self: - """Block adapter-native tool names or toolsets and return this config.""" + """Block adapter-native tool names and return this config.""" - if self.tools is None or isinstance(self.tools, dict): - self.tools = ToolsConfig.model_validate(self.tools or {}) + if self.tools is None: + self.tools = ToolsConfig() existing = list(self.tools.blocked) for tool in tools: if tool not in existing: @@ -501,6 +582,22 @@ def block_tools(self, *tools: str) -> Self: self.tools.blocked = existing return self + def configure_toolsets( + self, + *, + enabled: Sequence[str] | None = None, + blocked: Sequence[str] = (), + ) -> Self: + """Set adapter-native toolset selection and blocking policy.""" + + if self.tools is None: + self.tools = ToolsConfig() + self.tools.toolsets = ToolsetConfig( + enabled=None if enabled is None else list(enabled), + blocked=list(blocked), + ) + return self + def enable_relay( self, *, diff --git a/python/src/nemo_fabric/types.py b/python/src/nemo_fabric/types.py index f9a4cb1e0..c347e864a 100644 --- a/python/src/nemo_fabric/types.py +++ b/python/src/nemo_fabric/types.py @@ -220,10 +220,11 @@ class _RuntimeConfig(_ConfigMapping): input_schema: Optional logical input contract identifier. output_schema: Optional logical output contract identifier. artifacts: Optional artifact-root path. + timeout_seconds: Optional invocation deadline in seconds. extra_fields: Preserved extension fields not recognized by this SDK. """ - _fields = frozenset({"input_schema", "output_schema", "artifacts"}) + _fields = frozenset({"input_schema", "output_schema", "artifacts", "timeout_seconds"}) def __init__( self, @@ -231,6 +232,7 @@ def __init__( input_schema: str | None = None, output_schema: str | None = None, artifacts: str | Path | None = None, + timeout_seconds: float | None = None, extra_fields: Mapping[str, Any] | None = None, ) -> None: values: dict[str, Any] = {} @@ -241,6 +243,17 @@ def __init__( ): if item is not None: values[key] = item + if timeout_seconds is not None: + if ( + isinstance(timeout_seconds, bool) + or not isinstance(timeout_seconds, (int, float)) + or not math.isfinite(timeout_seconds) + or timeout_seconds <= 0 + ): + raise FabricConfigError( + "runtime timeout_seconds must be a finite number greater than zero" + ) + values["timeout_seconds"] = float(timeout_seconds) super().__init__(values, extra_fields=extra_fields) @classmethod @@ -252,6 +265,7 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_RuntimeConfig": input_schema=data.get("input_schema"), output_schema=data.get("output_schema"), artifacts=data.get("artifacts"), + timeout_seconds=data.get("timeout_seconds"), extra_fields={key: item for key, item in data.items() if key not in cls._fields}, ) @@ -263,13 +277,14 @@ class _EnvironmentConfig(_ConfigMapping): provider: Environment provider identifier; defaults to ``local``. workspace: Optional workspace path visible to the harness. artifacts: Optional environment-specific artifact path. + env: Environment variables visible to the harness and its tools. settings: JSON-compatible provider settings. metadata: JSON-compatible caller metadata. extra_fields: Preserved extension fields not recognized by this SDK. """ - _fields = frozenset({"provider", "workspace", "artifacts", "settings", "metadata"}) - _omit_if_empty = frozenset({"settings", "metadata"}) + _fields = frozenset({"provider", "workspace", "artifacts", "env", "settings", "metadata"}) + _omit_if_empty = frozenset({"env", "settings", "metadata"}) def __init__( self, @@ -277,12 +292,14 @@ def __init__( provider: str = "local", workspace: str | Path | None = None, artifacts: str | Path | None = None, + env: Mapping[str, str] | None = None, settings: Mapping[str, Any] | None = None, metadata: Mapping[str, Any] | None = None, extra_fields: Mapping[str, Any] | None = None, ) -> None: values: dict[str, Any] = { "provider": _required_text(provider, "environment provider"), + "env": {}, "settings": _mapping( {} if settings is None else settings, "environment settings", @@ -292,6 +309,13 @@ def __init__( "environment metadata", ), } + raw_env = _mapping({} if env is None else env, "environment env") + if any(not isinstance(value, str) for value in raw_env.values()): + raise FabricConfigError("environment env values must be strings") + values["env"] = { + _required_text(name, "environment variable name"): value + for name, value in raw_env.items() + } if workspace is not None: values["workspace"] = workspace if artifacts is not None: @@ -307,6 +331,7 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_EnvironmentConfig": provider=data.get("provider", "local"), workspace=data.get("workspace"), artifacts=data.get("artifacts"), + env=data.get("env"), settings=data.get("settings"), metadata=data.get("metadata"), extra_fields={key: item for key, item in data.items() if key not in cls._fields}, @@ -360,25 +385,81 @@ def remove_path(self, path: str | Path) -> "_SkillConfig": return self +class _ToolsetConfig(_ConfigMapping): + """Toolset selection and blocking policy.""" + + _fields = frozenset({"enabled", "blocked"}) + _omit_if_empty = frozenset({"blocked"}) + + def __init__( + self, + *, + enabled: Sequence[str] | None = None, + blocked: Sequence[str] | None = None, + extra_fields: Mapping[str, Any] | None = None, + ) -> None: + for name, values in (("enabled", enabled), ("blocked", blocked)): + if values is not None and ( + isinstance(values, (str, bytes)) or not isinstance(values, Sequence) + ): + raise FabricConfigError( + f"tools toolsets {name} must be an ordered sequence of strings" + ) + enabled_values = ( + None + if enabled is None + else [_required_text(toolset, "enabled toolset") for toolset in enabled] + ) + blocked_values = [ + _required_text(toolset, "blocked toolset") for toolset in (blocked or []) + ] + overlap = set(enabled_values or []).intersection(blocked_values) + if overlap: + name = sorted(overlap)[0] + raise FabricConfigError(f"toolset {name!r} cannot be both enabled and blocked") + values: dict[str, Any] = {"blocked": blocked_values} + if enabled_values is not None: + values["enabled"] = enabled_values + super().__init__(values, extra_fields=extra_fields) + + @classmethod + def from_mapping(cls, value: Mapping[str, Any]) -> "_ToolsetConfig": + """Validate a toolset mapping and preserve extension fields.""" + + data = _mapping(value, "tools toolsets") + return cls( + enabled=data.get("enabled"), + blocked=data.get("blocked", []), + extra_fields={key: item for key, item in data.items() if key not in cls._fields}, + ) + + class _ToolsConfig(_ConfigMapping): """Harness-neutral tool capability configuration.""" - _fields = frozenset({"blocked"}) + _fields = frozenset({"blocked", "toolsets"}) _omit_if_empty = frozenset({"blocked"}) def __init__( self, *, blocked: Sequence[str] | None = None, + toolsets: _ToolsetConfig | Mapping[str, Any] | None = None, extra_fields: Mapping[str, Any] | None = None, ) -> None: - if blocked is not None and (isinstance(blocked, (str, bytes)) or not isinstance(blocked, Sequence)): + if blocked is not None and ( + isinstance(blocked, (str, bytes)) or not isinstance(blocked, Sequence) + ): raise FabricConfigError("tools blocked must be an ordered sequence of strings") - values: dict[str, Any] = {"blocked": [_required_text(tool, "blocked tool") for tool in (blocked or [])]} + values: dict[str, Any] = { + "blocked": [_required_text(tool, "blocked tool") for tool in (blocked or [])] + } + if toolsets is not None: + values["toolsets"] = _coerce(_ToolsetConfig, toolsets, "tools toolsets") super().__init__(values, extra_fields=extra_fields) @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> _ToolsConfig: + def from_mapping(cls, value: Mapping[str, Any]) -> "_ToolsConfig": """Validate a tools mapping and preserve extension fields.""" data = _mapping(value, "tools") @@ -387,11 +468,12 @@ def from_mapping(cls, value: Mapping[str, Any]) -> _ToolsConfig: raise FabricConfigError("tools blocked must be an ordered sequence of strings") return cls( blocked=blocked, + toolsets=data.get("toolsets"), extra_fields={key: item for key, item in data.items() if key not in cls._fields}, ) def block(self, *tools: str) -> _ToolsConfig: - """Block adapter-native tool names or toolsets.""" + """Block adapter-native tool names.""" blocked = list(self.get("blocked", [])) for tool in tools: @@ -555,6 +637,8 @@ class _FabricConfigSnapshot(_ConfigMapping): runtime: Runtime input/output configuration. environment: Optional execution environment configuration. models: Named, JSON-compatible model configurations. + system_prompt: Optional agent system instructions. + max_turns: Optional shared harness turn limit. mcp: Optional MCP configuration. skills: Optional skill configuration. telemetry: Optional telemetry configuration. @@ -571,6 +655,8 @@ class _FabricConfigSnapshot(_ConfigMapping): "runtime", "environment", "models", + "system_prompt", + "max_turns", "mcp", "skills", "telemetry", @@ -589,6 +675,8 @@ def __init__( schema_version: str = "fabric.agent/v1alpha1", environment: _EnvironmentConfig | Mapping[str, Any] | None = None, models: Mapping[str, Any] | None = None, + system_prompt: str | None = None, + max_turns: int | None = None, mcp: Mapping[str, Any] | None = None, skills: Mapping[str, Any] | None = None, telemetry: Mapping[str, Any] | None = None, @@ -616,6 +704,18 @@ def __init__( "runtime": runtime_value, "models": _mapping({} if models is None else models, "models"), } + if system_prompt is not None: + if not isinstance(system_prompt, str): + raise FabricConfigError("system_prompt must be a string") + values["system_prompt"] = system_prompt + if max_turns is not None: + if ( + isinstance(max_turns, bool) + or not isinstance(max_turns, int) + or max_turns <= 0 + ): + raise FabricConfigError("max_turns must be greater than zero") + values["max_turns"] = max_turns for key, item in ( ("environment", environment_value), ("mcp", mcp_value), @@ -649,6 +749,8 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_FabricConfigSnapshot": runtime=data.get("runtime"), environment=data.get("environment"), models=data.get("models"), + system_prompt=data.get("system_prompt"), + max_turns=data.get("max_turns"), mcp=data.get("mcp"), skills=data.get("skills"), telemetry=data.get("telemetry"), diff --git a/schemas/adapter-descriptor.schema.json b/schemas/adapter-descriptor.schema.json index 97d42cb96..8188b1469 100644 --- a/schemas/adapter-descriptor.schema.json +++ b/schemas/adapter-descriptor.schema.json @@ -1,13 +1,73 @@ { "$defs": { + "AdapterConfigField": { + "description": "Adapter-translated normalized Fabric configuration fields.", + "oneOf": [ + { + "const": "models", + "description": "Normalized model selection and credentials.", + "type": "string" + }, + { + "const": "models.base_url", + "description": "Custom model endpoint.", + "type": "string" + }, + { + "const": "models.temperature", + "description": "Model temperature.", + "type": "string" + }, + { + "const": "system_prompt", + "description": "Portable system instructions.", + "type": "string" + }, + { + "const": "max_turns", + "description": "Per-invocation harness turn limit.", + "type": "string" + }, + { + "const": "tools.blocked", + "description": "Adapter-native tool names to block.", + "type": "string" + }, + { + "const": "tools.toolsets.enabled", + "description": "Harness-defined toolsets to expose.", + "type": "string" + }, + { + "const": "tools.toolsets.blocked", + "description": "Harness-defined toolsets to block.", + "type": "string" + }, + { + "const": "mcp", + "description": "Harness-native MCP servers.", + "type": "string" + }, + { + "const": "skills", + "description": "Harness-native skills.", + "type": "string" + }, + { + "const": "telemetry", + "description": "Typed telemetry configuration. Exact support remains declared by telemetry provider.", + "type": "string" + } + ] + }, "AdapterConfigSupport": { "additionalProperties": true, "description": "Adapter config support.", "properties": { "accepts": { - "description": "Fabric config areas or policy paths accepted by this adapter.", + "description": "Normalized Fabric config fields accepted by this adapter.", "items": { - "type": "string" + "$ref": "#/$defs/AdapterConfigField" }, "type": "array" }, diff --git a/schemas/adapter-invocation.schema.json b/schemas/adapter-invocation.schema.json index 756caa979..92523320f 100644 --- a/schemas/adapter-invocation.schema.json +++ b/schemas/adapter-invocation.schema.json @@ -84,6 +84,13 @@ "$ref": "#/$defs/ControlLocation", "description": "Where Fabric control code runs." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "environment_id": { "description": "Environment handle id.", "type": "string" diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 17b0bf863..5f3c338ee 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -36,6 +36,13 @@ "default": "in_env_control", "description": "Where Fabric control code runs relative to the environment." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "metadata": { "additionalProperties": true, "description": "Consumer-provided environment metadata.", @@ -198,6 +205,13 @@ "null" ] }, + "base_url": { + "description": "Optional provider endpoint URL.", + "type": [ + "string", + "null" + ] + }, "model": { "description": "Provider model identifier.", "type": "string" @@ -895,6 +909,14 @@ "default": "text", "description": "Output schema label.", "type": "string" + }, + "timeout_seconds": { + "description": "Maximum duration of one invocation in seconds.", + "format": "double", + "type": [ + "number", + "null" + ] } }, "type": "object" @@ -942,11 +964,46 @@ "description": "Harness-neutral tool capability configuration.", "properties": { "blocked": { - "description": "Adapter-native tool names or toolset names to block.", + "description": "Adapter-native tool names to block.", + "items": { + "type": "string" + }, + "type": "array" + }, + "toolsets": { + "anyOf": [ + { + "$ref": "#/$defs/ToolsetConfig" + }, + { + "type": "null" + } + ], + "description": "Adapter-native toolset selection and blocking policy." + } + }, + "type": "object" + }, + "ToolsetConfig": { + "additionalProperties": true, + "description": "Harness-neutral toolset capability configuration.", + "properties": { + "blocked": { + "description": "Toolsets to block.", "items": { "type": "string" }, "type": "array" + }, + "enabled": { + "description": "Toolsets to expose. `None` preserves the adapter default; an empty list exposes none.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" @@ -954,7 +1011,7 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": true, - "description": "Versioned Fabric agent config.", + "description": "Versioned Fabric agent config.\n\nFabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", "properties": { "environment": { "anyOf": [ @@ -971,6 +1028,15 @@ "$ref": "#/$defs/HarnessConfig", "description": "Harness selection and harness-specific settings." }, + "max_turns": { + "description": "Maximum number of harness turns within one invocation.", + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, "mcp": { "anyOf": [ { @@ -1023,6 +1089,13 @@ ], "description": "Skill capability configuration." }, + "system_prompt": { + "description": "Portable system instructions for the selected harness.", + "type": [ + "string", + "null" + ] + }, "telemetry": { "anyOf": [ { diff --git a/schemas/environment-handle.schema.json b/schemas/environment-handle.schema.json index 8f39a50c7..3b7f69ccf 100644 --- a/schemas/environment-handle.schema.json +++ b/schemas/environment-handle.schema.json @@ -50,6 +50,13 @@ "$ref": "#/$defs/ControlLocation", "description": "Where Fabric control code runs." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "environment_id": { "description": "Environment handle id.", "type": "string" diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 41ad885ef..6b25096e3 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -1,13 +1,73 @@ { "$defs": { + "AdapterConfigField": { + "description": "Adapter-translated normalized Fabric configuration fields.", + "oneOf": [ + { + "const": "models", + "description": "Normalized model selection and credentials.", + "type": "string" + }, + { + "const": "models.base_url", + "description": "Custom model endpoint.", + "type": "string" + }, + { + "const": "models.temperature", + "description": "Model temperature.", + "type": "string" + }, + { + "const": "system_prompt", + "description": "Portable system instructions.", + "type": "string" + }, + { + "const": "max_turns", + "description": "Per-invocation harness turn limit.", + "type": "string" + }, + { + "const": "tools.blocked", + "description": "Adapter-native tool names to block.", + "type": "string" + }, + { + "const": "tools.toolsets.enabled", + "description": "Harness-defined toolsets to expose.", + "type": "string" + }, + { + "const": "tools.toolsets.blocked", + "description": "Harness-defined toolsets to block.", + "type": "string" + }, + { + "const": "mcp", + "description": "Harness-native MCP servers.", + "type": "string" + }, + { + "const": "skills", + "description": "Harness-native skills.", + "type": "string" + }, + { + "const": "telemetry", + "description": "Typed telemetry configuration. Exact support remains declared by telemetry provider.", + "type": "string" + } + ] + }, "AdapterConfigSupport": { "additionalProperties": true, "description": "Adapter config support.", "properties": { "accepts": { - "description": "Fabric config areas or policy paths accepted by this adapter.", + "description": "Normalized Fabric config fields accepted by this adapter.", "items": { - "type": "string" + "$ref": "#/$defs/AdapterConfigField" }, "type": "array" }, @@ -387,6 +447,13 @@ "default": "in_env_control", "description": "Where Fabric control code runs relative to the environment." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "metadata": { "additionalProperties": true, "description": "Consumer-provided environment metadata.", @@ -453,6 +520,13 @@ "$ref": "#/$defs/ControlLocation", "description": "Fabric control location." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "metadata": { "additionalProperties": true, "description": "Consumer-provided environment metadata.", @@ -488,7 +562,7 @@ }, "FabricConfig": { "additionalProperties": true, - "description": "Versioned Fabric agent config.", + "description": "Versioned Fabric agent config.\n\nFabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", "properties": { "environment": { "anyOf": [ @@ -505,6 +579,15 @@ "$ref": "#/$defs/HarnessConfig", "description": "Harness selection and harness-specific settings." }, + "max_turns": { + "description": "Maximum number of harness turns within one invocation.", + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, "mcp": { "anyOf": [ { @@ -557,6 +640,13 @@ ], "description": "Skill capability configuration." }, + "system_prompt": { + "description": "Portable system instructions for the selected harness.", + "type": [ + "string", + "null" + ] + }, "telemetry": { "anyOf": [ { @@ -726,6 +816,13 @@ "null" ] }, + "base_url": { + "description": "Optional provider endpoint URL.", + "type": [ + "string", + "null" + ] + }, "model": { "description": "Provider model identifier.", "type": "string" @@ -1482,6 +1579,14 @@ "default": "text", "description": "Output schema label.", "type": "string" + }, + "timeout_seconds": { + "description": "Maximum duration of one invocation in seconds.", + "format": "double", + "type": [ + "number", + "null" + ] } }, "type": "object" @@ -1592,11 +1697,22 @@ "description": "Harness-neutral tool capability configuration.", "properties": { "blocked": { - "description": "Adapter-native tool names or toolset names to block.", + "description": "Adapter-native tool names to block.", "items": { "type": "string" }, "type": "array" + }, + "toolsets": { + "anyOf": [ + { + "$ref": "#/$defs/ToolsetConfig" + }, + { + "type": "null" + } + ], + "description": "Adapter-native toolset selection and blocking policy." } }, "type": "object" @@ -1605,11 +1721,46 @@ "description": "Normalized tool policy for a run.", "properties": { "blocked": { - "description": "Adapter-native tool names or toolset names to block.", + "description": "Adapter-native tool names to block.", + "items": { + "type": "string" + }, + "type": "array" + }, + "toolsets": { + "anyOf": [ + { + "$ref": "#/$defs/ToolsetConfig" + }, + { + "type": "null" + } + ], + "description": "Adapter-native toolset selection and blocking policy." + } + }, + "type": "object" + }, + "ToolsetConfig": { + "additionalProperties": true, + "description": "Harness-neutral toolset capability configuration.", + "properties": { + "blocked": { + "description": "Toolsets to block.", "items": { "type": "string" }, "type": "array" + }, + "enabled": { + "description": "Toolsets to expose. `None` preserves the adapter default; an empty list exposes none.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" diff --git a/schemas/runtime-context.schema.json b/schemas/runtime-context.schema.json index cda168be9..bc224e21b 100644 --- a/schemas/runtime-context.schema.json +++ b/schemas/runtime-context.schema.json @@ -84,6 +84,13 @@ "$ref": "#/$defs/ControlLocation", "description": "Where Fabric control code runs." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "environment_id": { "description": "Environment handle id.", "type": "string" diff --git a/schemas/runtime-handle.schema.json b/schemas/runtime-handle.schema.json index 9cda47dfd..94db8ba83 100644 --- a/schemas/runtime-handle.schema.json +++ b/schemas/runtime-handle.schema.json @@ -59,6 +59,13 @@ "$ref": "#/$defs/ControlLocation", "description": "Where Fabric control code runs." }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables visible to the harness and its tools.", + "type": "object" + }, "environment_id": { "description": "Environment handle id.", "type": "string" diff --git a/tests/_utils/configs.py b/tests/_utils/configs.py index c72d23ed6..a23be868e 100644 --- a/tests/_utils/configs.py +++ b/tests/_utils/configs.py @@ -14,7 +14,7 @@ def hermes_shim_config() -> FabricConfig: harness=HarnessConfig( adapter_id="test.fabric.hermes_shim", resolution="preinstalled", - settings={"workspace": "./repos/my-service"}, + settings={}, ), models={"default": ModelConfig(provider="test", model="test-model", temperature=0.0)}, runtime=RuntimeConfig(input_schema="chat", output_schema="message", artifacts="./artifacts"), @@ -41,7 +41,6 @@ def swebench_shim_config() -> FabricConfig: resolution="preinstalled", settings={ "mode": "swebench_shim", - "workspace": "./repos/my-service", "target_file": "calculator.py", "expected_before": "return 41", "replacement": "return 42", @@ -70,7 +69,6 @@ def harbor_swebench_config() -> FabricConfig: resolution="preinstalled", settings={ "mode": "swebench_shim", - "workspace": workspace, "target_file": "django/contrib/auth/forms.py", "expected_before": " kwargs.setdefault(\"required\", False)\n super().__init__(*args, **kwargs)", "replacement": " kwargs.setdefault(\"required\", False)\n kwargs.setdefault('disabled', True)\n super().__init__(*args, **kwargs)", diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index 2a8c498aa..1c4561667 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -91,57 +91,52 @@ def test_default_base_url( @pytest.mark.parametrize( - ("settings", "model_config", "expected"), + ("model_config", "expected"), [ ( - {"base_url": "https://settings.example/v1"}, - {"provider": "nvidia", "settings": {"base_url": "https://model.example/v1"}}, - "https://settings.example/v1", - ), - ( - {}, - {"provider": "openai", "settings": {"base_url": "https://model.example/v1"}}, + {"provider": "nvidia", "base_url": "https://model.example/v1"}, "https://model.example/v1", ), - ({}, {"provider": "nvidia"}, "https://integrate.api.nvidia.com/v1"), - ({}, {"provider": "other"}, None), + ({"provider": "openai", "base_url": "https://model.example/v1"}, "https://model.example/v1"), + ({"provider": "nvidia"}, "https://integrate.api.nvidia.com/v1"), + ({"provider": "other"}, None), ], ) def test_get_base_url( - settings: dict[str, object], model_config: dict[str, object], expected: str | None, ): - assert common_utils.get_base_url(settings, model_config) == expected + assert common_utils.get_base_url(model_config) == expected @pytest.mark.parametrize( - ("selected_model", "models", "expected"), + ("models", "expected"), [ ( - "fast", {"fast": {"provider": "nvidia", "model": "fast-model"}}, {"provider": "nvidia", "model": "fast-model"}, ), ( - None, {"default": {"provider": "nvidia", "model": "default-model"}}, {"provider": "nvidia", "model": "default-model"}, ), - ("bad", {"bad": "not-a-model-config"}, {}), + ({"bad": "not-a-model-config"}, {}), + ( + { + "fast": {"provider": "nvidia", "model": "fast-model"}, + "slow": {"provider": "nvidia", "model": "slow-model"}, + }, + {}, + ), ], ) def test_selected_model_config( - selected_model: str | None, models: dict[str, object], expected: dict[str, object], ): - settings = {} - if selected_model is not None: - settings["model"] = selected_model payload = { "config": { - "harness": {"settings": settings}, + "harness": {"settings": {}}, "models": models, } } @@ -149,6 +144,47 @@ def test_selected_model_config( assert common_utils.selected_model_config(payload) == expected +def test_normalized_agent_and_toolset_accessors(): + payload = { + "config": { + "system_prompt": "Be concise.", + "max_turns": 7, + "runtime": {"timeout_seconds": 12.5}, + "tools": { + "blocked": ["Bash"], + "toolsets": {"enabled": [], "blocked": ["browser"]}, + }, + }, + "runtime_context": { + "environment": {"env": {"VISIBLE": "yes"}}, + }, + } + + assert common_utils.system_prompt(payload) == "Be concise." + assert common_utils.max_turns(payload) == 7 + assert common_utils.timeout_seconds(payload, default=30) == 12.5 + assert common_utils.environment_env(payload) == {"VISIBLE": "yes"} + assert common_utils.blocked_tools(payload) == ["Bash"] + assert common_utils.enabled_toolsets(payload) == [] + assert common_utils.blocked_toolsets(payload) == ["browser"] + + +def test_environment_payload_uses_normalized_config_without_runtime_context(): + payload = { + "config": { + "environment": { + "workspace": "/config-workspace", + "env": {"VISIBLE": "yes"}, + } + } + } + + assert common_utils.environment_payload(payload) == { + "workspace": "/config-workspace", + "env": {"VISIBLE": "yes"}, + } + + def test_payload_accessors_use_canonical_plan_fields(tmp_path): base_dir = str(tmp_path / "outer") payload = { diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index f575fe0b9..27efcfaac 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -89,12 +89,14 @@ def test_claude_descriptor_is_narrow_and_versioned(): "config": { "accepts": [ "models", - "tools", + "models.base_url", + "system_prompt", + "max_turns", "tools.blocked", "mcp", "skills", "telemetry", - ] + ], }, "telemetry": { "providers": { @@ -121,16 +123,15 @@ def claude_payload_fixture(tmp_path) -> dict[str, Any]: "harness": { "adapter_id": "nvidia.fabric.claude", "settings": { - "system_prompt": "Review carefully.", "allowed_tools": ["Read"], "permission_mode": "dontAsk", - "max_turns": 4, "max_budget_usd": 1.5, "setting_sources": [], - "timeout_seconds": 30, - "env": {"ANTHROPIC_API_KEY": "configured-secret"}, }, }, + "system_prompt": "Review carefully.", + "max_turns": 4, + "runtime": {"timeout_seconds": 30}, "models": { "default": { "provider": "anthropic", @@ -143,7 +144,10 @@ def claude_payload_fixture(tmp_path) -> dict[str, Any]: "runtime_context": { "runtime_id": "runtime-claude-1", "invocation_id": "invocation-1", - "environment": {"workspace": str(workspace)}, + "environment": { + "workspace": str(workspace), + "env": {"ANTHROPIC_API_KEY": "configured-secret"}, + }, "artifacts": {"root": str(tmp_path / "artifacts"), "artifacts": []}, }, "request": {"request_id": "request-1", "input": "Inspect the patch"}, @@ -374,7 +378,12 @@ def test_build_options_maps_blocked_tools_to_disallowed_tools(claude_payload): ("name", "normalized_field"), [ ("model_name", "FabricConfig.models"), + ("base_url", "FabricConfig.models..base_url"), ("cwd", "FabricConfig.environment.workspace"), + ("env", "FabricConfig.environment.env"), + ("system_prompt", "FabricConfig.system_prompt"), + ("max_turns", "FabricConfig.max_turns"), + ("timeout_seconds", "FabricConfig.runtime.timeout_seconds"), ("tools", "FabricConfig.tools"), ("disallowed_tools", "FabricConfig.tools.blocked"), ("mcp_servers", "FabricConfig.mcp"), @@ -392,6 +401,16 @@ def test_build_options_rejects_normalized_capabilities_in_harness_settings( adapter.build_options(claude_payload) +@pytest.mark.parametrize("setting", ["cli_path", "nemo_relay_command"]) +def test_build_options_rejects_removed_runtime_settings(claude_payload, setting): + claude_payload["config"]["harness"]["settings"][setting] = "custom" + + with pytest.raises( + adapter.AdapterConfigError, match=rf"harness\.settings\.{setting}" + ): + adapter.build_options(claude_payload) + + def test_build_options_rejects_skill_path_without_skill_manifest(claude_payload): skill_path = Path(claude_payload["capability_plan"]["native"]["skill_paths"][0]) (skill_path / "SKILL.md").unlink() @@ -409,7 +428,7 @@ def test_build_options_maps_nvidia_provider_to_claude_gateway_environment( "provider": "nvidia", "model": "aws/anthropic/claude-opus-4-5", "api_key_env": "NVIDIA_API_KEY", - "settings": {"base_url": "https://nvidia.example/v1/"}, + "base_url": "https://nvidia.example/v1/", } ) os.environ["NVIDIA_API_KEY"] = "nvidia-secret" @@ -433,31 +452,16 @@ def test_build_options_uses_nvidia_provider_endpoint_and_default_credential( } ) model.pop("api_key_env") - claude_payload["config"]["harness"]["settings"].pop("env") + claude_payload["runtime_context"]["environment"]["env"].pop("ANTHROPIC_API_KEY") os.environ["NVIDIA_API_KEY"] = "nvidia-secret" - os.environ["NVIDIA_FRONTIER_BASE_URL"] = "https://frontier.example/v1" options = adapter.build_options(claude_payload) - assert options.env["ANTHROPIC_BASE_URL"] == "https://frontier.example" - assert options.env["ANTHROPIC_API_KEY"] == "nvidia-secret" - - -def test_build_options_requires_nvidia_provider_endpoint(claude_payload): - model = claude_payload["config"]["models"]["default"] - model.update( - { - "provider": "nvidia", - "model": "aws/anthropic/claude-opus-4-5", - "api_key_env": "NVIDIA_API_KEY", - } + assert ( + options.env["ANTHROPIC_BASE_URL"] + == "https://integrate.api.nvidia.com" ) - model.pop("settings", None) - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" - os.environ.pop("NVIDIA_FRONTIER_BASE_URL", None) - - with pytest.raises(adapter.AdapterConfigError, match="NVIDIA_FRONTIER_BASE_URL"): - adapter.build_options(claude_payload) + assert options.env["ANTHROPIC_API_KEY"] == "nvidia-secret" def test_build_options_requires_nvidia_provider_credential(claude_payload): @@ -1040,8 +1044,7 @@ def test_build_options_forwards_anthropic_auth_environment( ): model = claude_payload["config"]["models"]["default"] model.pop("api_key_env") - settings = claude_payload["config"]["harness"]["settings"] - settings.pop("env") + claude_payload["runtime_context"]["environment"].pop("env") for name in ANTHROPIC_AUTH_ENV_NAMES: os.environ.pop(name, None) os.environ["FABRIC_UNRELATED_SECRET"] = "do-not-forward" diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index 51c323cc6..84604dd3d 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -75,6 +75,7 @@ def codex_payload_fixture(tmp_path): "model": "openai/gpt-5.4", } }, + "system_prompt": "Review carefully.", "runtime": {}, }, "runtime_context": { @@ -179,7 +180,7 @@ def test_single_invocation_uses_native_thread_and_turn_contract( os.environ["CODEX_HOME"] = str(tmp_path / "codex-home") os.environ["CODEX_INTERNAL_ORIGINATOR_OVERRIDE"] = "parent-codex" os.environ["FABRIC_UNRELATED_SECRET"] = "do-not-forward" - codex_payload["config"]["harness"]["settings"]["env"] = { + codex_payload["runtime_context"]["environment"]["env"] = { "CODEX_EXPLICIT": "forward-me" } @@ -209,6 +210,7 @@ def test_single_invocation_uses_native_thread_and_turn_contract( start = client.thread_start.await_args.kwargs assert start["model"] == "gpt-5.4" assert start["model_provider"] == "openai" + assert start["base_instructions"] == "Review carefully." assert start["sandbox"] == adapter.Sandbox.workspace_write assert start["config"] == { "features": {"web_search": False}, @@ -397,11 +399,13 @@ def test_sdk_rejects_falsy_non_list_skill_paths(codex_payload, mock_codex, skill mock_codex.assert_not_called() -def test_sdk_can_use_an_explicit_codex_runtime(codex_payload, mock_codex, tmp_path): +def test_sdk_test_override_can_use_an_explicit_codex_runtime( + codex_payload, mock_codex, tmp_path, monkeypatch +): codex_bin = tmp_path / "bin" / "codex" codex_bin.parent.mkdir() codex_bin.touch() - codex_payload["config"]["harness"]["settings"]["codex_bin"] = str(codex_bin) + monkeypatch.setenv("FABRIC_TEST_CODEX_BIN", str(codex_bin)) output = invoke_once(codex_payload) @@ -410,8 +414,10 @@ def test_sdk_can_use_an_explicit_codex_runtime(codex_payload, mock_codex, tmp_pa @pytest.mark.parametrize("codex_bin", ["bin/codex", "~/bin/codex"]) -def test_sdk_resolves_relative_codex_runtime_from_base_dir(codex_payload, codex_bin): - codex_payload["config"]["harness"]["settings"]["codex_bin"] = codex_bin +def test_sdk_test_override_resolves_relative_runtime_from_base_dir( + codex_payload, codex_bin, monkeypatch +): + monkeypatch.setenv("FABRIC_TEST_CODEX_BIN", codex_bin) config = adapter.sdk_config(codex_payload, relay=None) @@ -419,9 +425,11 @@ def test_sdk_resolves_relative_codex_runtime_from_base_dir(codex_payload, codex_ assert config.codex_bin == str((base_dir / codex_bin).resolve()) -def test_sdk_keeps_absolute_codex_runtime_path(codex_payload, tmp_path): +def test_sdk_test_override_keeps_absolute_runtime_path( + codex_payload, tmp_path, monkeypatch +): codex_bin = tmp_path / "bin" / ".." / "codex" - codex_payload["config"]["harness"]["settings"]["codex_bin"] = str(codex_bin) + monkeypatch.setenv("FABRIC_TEST_CODEX_BIN", str(codex_bin)) config = adapter.sdk_config(codex_payload, relay=None) @@ -582,7 +590,7 @@ def test_nvidia_provider_uses_responses_api_and_nvidia_credential( "provider": "nvidia", "model": "openai/gpt-oss-120b", "api_key_env": "NVIDIA_API_KEY", - "settings": {"base_url": "https://nvidia.example/v1/"}, + "base_url": "https://nvidia.example/v1/", } ) os.environ["NVIDIA_API_KEY"] = "nvidia-secret" @@ -620,7 +628,7 @@ def test_nvidia_provider_normalizes_codex_home_creation_failure( "provider": "nvidia", "model": "openai/gpt-oss-120b", "api_key_env": "NVIDIA_API_KEY", - "settings": {"base_url": "https://nvidia.example/v1"}, + "base_url": "https://nvidia.example/v1", } ) os.environ["NVIDIA_API_KEY"] = "nvidia-secret" @@ -655,7 +663,7 @@ def test_nvidia_provider_requires_credential(codex_payload, mock_codex): mock_codex.assert_not_called() -def test_nvidia_provider_requires_endpoint(codex_payload, mock_codex): +def test_nvidia_provider_uses_default_endpoint(codex_payload, mock_codex): model = codex_payload["config"]["models"]["default"] model.update( { @@ -664,16 +672,15 @@ def test_nvidia_provider_requires_endpoint(codex_payload, mock_codex): "api_key_env": "NVIDIA_API_KEY", } ) - model.pop("settings", None) os.environ["NVIDIA_API_KEY"] = "nvidia-secret" - os.environ.pop("NVIDIA_FRONTIER_BASE_URL", None) - error = runtime_start_error(codex_payload) + output = invoke_once(codex_payload) - assert error.code == "codex_invalid_configuration" - assert "NVIDIA_FRONTIER_BASE_URL" in error.message - assert not (adapter.state_dir(codex_payload) / "nvidia-home").exists() - mock_codex.assert_not_called() + assert output["completed"] is True + config = mock_codex.instances[0].thread_start.await_args.kwargs["config"] + assert config["model_providers"]["nvidia"]["base_url"] == ( + "https://integrate.api.nvidia.com/v1" + ) def test_relay_uses_gateway_and_request_scoped_sdk_config( @@ -745,7 +752,7 @@ def test_relay_rejects_nvidia_provider(codex_payload, mock_codex): "provider": "nvidia", "model": "openai/gpt-oss-120b", "api_key_env": "NVIDIA_API_KEY", - "settings": {"base_url": "https://nvidia.example/v1"}, + "base_url": "https://nvidia.example/v1", } ) codex_payload["telemetry_plan"] = { @@ -852,7 +859,6 @@ def test_native_sdk_controls_and_telemetry_are_request_scoped( { "personality": "pragmatic", "reasoning_effort": "xhigh", - "service_name": "fabric-codex-test", "output_schema": { "type": "object", "properties": {"summary": {"type": "string"}}, @@ -887,7 +893,7 @@ def test_native_sdk_controls_and_telemetry_are_request_scoped( client = mock_codex.instances[0] start = client.thread_start.await_args.kwargs assert start["personality"] == adapter.Personality.pragmatic - assert start["service_name"] == "fabric-codex-test" + assert "service_name" not in start assert start["config"]["otel"] == { "environment": "test", "trace_exporter": { @@ -910,7 +916,7 @@ async def block(): mock_blocking_thread.handle.run.side_effect = block mock_codex.next_thread = mock_blocking_thread - codex_payload["config"]["harness"]["settings"]["timeout_seconds"] = 0.01 + codex_payload["config"]["runtime"]["timeout_seconds"] = 0.01 output = invoke_once(codex_payload) @@ -934,7 +940,14 @@ def test_cli_only_settings_are_rejected(codex_payload, setting): @pytest.mark.parametrize( ("setting", "normalized_field"), - [("mcp_servers", "FabricConfig.mcp"), ("skills", "FabricConfig.skills")], + [ + ("model", "FabricConfig.models"), + ("base_url", "FabricConfig.models..base_url"), + ("env", "FabricConfig.environment.env"), + ("timeout_seconds", "FabricConfig.runtime.timeout_seconds"), + ("mcp_servers", "FabricConfig.mcp"), + ("skills", "FabricConfig.skills"), + ], ) def test_normalized_capabilities_reject_harness_settings( codex_payload, mock_codex, setting, normalized_field @@ -948,6 +961,21 @@ def test_normalized_capabilities_reject_harness_settings( mock_codex.assert_not_called() +@pytest.mark.parametrize( + "setting", ["codex_bin", "nemo_relay_command", "service_name"] +) +def test_removed_runtime_settings_are_rejected( + codex_payload, mock_codex, setting +): + codex_payload["config"]["harness"]["settings"][setting] = "custom" + + error = runtime_start_error(codex_payload) + + assert error.code == "codex_invalid_configuration" + assert f"harness.settings.{setting}" in error.message + mock_codex.assert_not_called() + + def test_adapter_rejects_structured_input(codex_payload): codex_payload["request"]["input"] = { "messages": [{"role": "user", "content": "Inspect the change."}] @@ -971,6 +999,8 @@ def test_descriptor_has_no_codex_binary_requirement(): } assert descriptor["config"]["accepts"] == [ "models", + "models.base_url", + "system_prompt", "mcp", "skills", "telemetry", @@ -1025,7 +1055,7 @@ def test_environment_preserves_runtime_telemetry_env(codex_payload): "CODEX_EXPLICIT": "telemetry", } } - codex_payload["config"]["harness"]["settings"]["env"] = { + codex_payload["runtime_context"]["environment"]["env"] = { "CODEX_EXPLICIT": "configured" } os.environ["FABRIC_RELAY_CONFIG_PATH"] = "/tmp/parent-relay.json" diff --git a/tests/adapters/test_deepagents.py b/tests/adapters/test_deepagents.py index d718050b4..9e5313468 100644 --- a/tests/adapters/test_deepagents.py +++ b/tests/adapters/test_deepagents.py @@ -13,6 +13,7 @@ import importlib.machinery import os +import re import sys import types from collections.abc import AsyncIterator @@ -151,7 +152,8 @@ def make(tmp_path: Path, *, runtime_id: str = "run-1") -> dict[str, Any]: return { "base_dir": str(tmp_path), "config": { - "harness": {"settings": {"system_prompt": "be concise"}}, + "harness": {"settings": {}}, + "system_prompt": "be concise", "models": { "default": { "provider": "nvidia", @@ -1095,6 +1097,28 @@ async def test_openai_compatible_provider_requires_api_key_env(tmp_path, make_pa await adapter.DeepAgentsRuntime().start(lifecycle_start_payload(payload)) +def test_removed_state_dir_setting_is_rejected(): + with pytest.raises( + adapter.AdapterConfigError, match=r"harness\.settings\.state_dir" + ): + adapter._validate_settings_boundary({"state_dir": "./state"}) + + +@pytest.mark.parametrize( + ("setting", "target"), + sorted(adapter.NORMALIZED_SETTING_FIELDS.items()), +) +def test_normalized_fields_are_rejected_in_harness_settings( + setting: str, + target: str, +): + with pytest.raises( + adapter.AdapterConfigError, + match=rf"harness\.settings\.{setting}.*{re.escape(target)}", + ): + adapter._validate_settings_boundary({setting: "legacy"}) + + def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 5c1186aac..bc97bd8df 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -9,6 +9,7 @@ import inspect import json import os +import re import sys from pathlib import Path from types import ModuleType, SimpleNamespace @@ -181,22 +182,22 @@ def test_build_hermes_config_maps_fabric_config_to_hermes_config(): "config": { "harness": { "settings": { - "model": "review", - "max_iterations": 4, - "disabled_toolsets": ["browser"], - "terminal_backend": "local", "terminal_timeout": 90, - "enabled_toolsets": "git", - "toolset_platform": "cli", "plugins_enabled": ["custom/plugin"], } }, - "tools": {"blocked": ["shell", "browser"]}, + "max_turns": 4, + "tools": { + "toolsets": { + "enabled": ["git"], + "blocked": ["browser"], + } + }, "models": { "review": { "provider": "nvidia", "model": "nvidia/review-model", - "settings": {"base_url": "https://model.example/v1"}, + "base_url": "https://model.example/v1", } }, }, @@ -212,7 +213,7 @@ def test_build_hermes_config_maps_fabric_config_to_hermes_config(): }, "agent": { "max_turns": 4, - "disabled_toolsets": ["shell", "browser"], + "disabled_toolsets": ["browser"], }, "terminal": { "backend": "local", @@ -249,8 +250,37 @@ def test_default_max_iterations_matches_hermes_library_default(): assert adapter.DEFAULT_MAX_ITERATIONS == hermes_default -def test_build_hermes_config_omits_max_turns_when_max_iterations_unset(): - # When max_iterations is unset the config layer must leave agent.max_turns +@pytest.mark.parametrize( + ("setting", "target"), + [ + ("api_key_env", "FabricConfig.models..api_key_env"), + ("base_url", "FabricConfig.models..base_url"), + ("disabled_toolsets", "FabricConfig.tools.toolsets.blocked"), + ("enabled_toolsets", "FabricConfig.tools.toolsets.enabled"), + ("env", "FabricConfig.environment.env"), + ("max_iterations", "FabricConfig.max_turns"), + ("max_turns", "FabricConfig.max_turns"), + ("model", "FabricConfig.models"), + ("model_name", "FabricConfig.models..model"), + ("provider", "FabricConfig.models..provider"), + ("system_prompt", "FabricConfig.system_prompt"), + ("temperature", "FabricConfig.models..temperature"), + ("workspace", "FabricConfig.environment.workspace"), + ], +) +def test_normalized_fields_are_rejected_in_harness_settings( + setting: str, + target: str, +): + with pytest.raises( + ValueError, + match=rf"harness\.settings\.{setting}.*{re.escape(target)}", + ): + adapter._validate_settings_boundary({setting: "legacy"}) + + +def test_build_hermes_config_omits_max_turns_when_fabric_limit_unset(): + # When max_turns is unset the config layer must leave agent.max_turns # absent so Hermes applies its own default rather than a starving override. payload = { "config": { @@ -264,12 +294,13 @@ def test_build_hermes_config_omits_max_turns_when_max_iterations_unset(): assert "max_turns" not in config["agent"] -def test_build_hermes_config_omits_max_turns_when_max_iterations_null(): - # An explicit null max_iterations is treated like unset: agent.max_turns is +def test_build_hermes_config_omits_max_turns_when_fabric_limit_null(): + # An explicit null max_turns is treated like unset: agent.max_turns is # omitted so Hermes applies its own default instead of a starving override. payload = { "config": { - "harness": {"settings": {"max_iterations": None}}, + "harness": {"settings": {}}, + "max_turns": None, "models": {"default": {"provider": "nvidia", "model": "nvidia/test-model"}}, } } @@ -336,13 +367,9 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( "base_dir": str(tmp_path), "config": { "harness": { - "settings": { - "model": "review", - "enabled_toolsets": ["git", "shell"], - "toolset_platform": "cli", - "terminal_backend": "local", - } + "settings": {} }, + "tools": {"toolsets": {"enabled": ["git", "shell"]}}, "models": { "review": { "provider": "nvidia", @@ -537,15 +564,10 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( "agent_name": "demo", "base_dir": str(tmp_path), "config": { - "harness": { - "settings": { - "hermes_home": "./hermes-home", - "enabled_toolsets": [], - "system_prompt": "system", - # Explicit null must resolve to DEFAULT_MAX_ITERATIONS (not int(None)). - "max_iterations": None, - } - }, + "harness": {"settings": {}}, + "system_prompt": "system", + "max_turns": None, + "tools": {"toolsets": {"enabled": []}}, "models": { "default": { "provider": "test-provider", @@ -626,10 +648,24 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( assert second["response"] == "second response" assert "session_id" not in second assert Path(second["hermes_home"]) == ( - tmp_path / "hermes-home" / "runtimes" / "runtime-fabric-123" + tmp_path + / "artifacts" + / ".fabric" + / "hermes" + / "runtimes" + / "runtime-fabric-123" ) +@pytest.mark.parametrize( + "setting", + ["hermes_home", "insert_reasoning", "terminal_backend", "toolset_platform"], +) +def test_removed_runtime_settings_are_rejected(setting): + with pytest.raises(ValueError, match=rf"harness\.settings\.{setting}"): + adapter._validate_settings_boundary({setting: "value"}) + + def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/e2e/test_claude.py b/tests/e2e/test_claude.py index 986705b47..16a09fd1a 100644 --- a/tests/e2e/test_claude.py +++ b/tests/e2e/test_claude.py @@ -75,19 +75,18 @@ def fabric_config( "setting_sources": [], "permission_mode": "dontAsk", } - if cli_path is not None: - settings.update( - { - "cli_path": str(cli_path), - "env": { - "CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK": "1", - "MOCK_CLAUDE_CLI_LOG": str(tmp_path / "claude-args.jsonl"), - "MOCK_CLAUDE_CLI_ENV_LOG": str(tmp_path / "claude-env.jsonl"), - }, - } - ) + environment_env = ( + { + "FABRIC_TEST_CLAUDE_CLI_PATH": str(cli_path), + "CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK": "1", + "MOCK_CLAUDE_CLI_LOG": str(tmp_path / "claude-args.jsonl"), + "MOCK_CLAUDE_CLI_ENV_LOG": str(tmp_path / "claude-env.jsonl"), + } + if cli_path is not None + else {} + ) if nemo_relay_command is not None: - settings["nemo_relay_command"] = str(nemo_relay_command) + environment_env["FABRIC_TEST_NEMO_RELAY_COMMAND"] = str(nemo_relay_command) config = FabricConfig( metadata=MetadataConfig(name="claude-runtime-test"), harness=HarnessConfig( @@ -109,6 +108,7 @@ def fabric_config( provider="local", workspace=tmp_path, artifacts=tmp_path / "artifacts", + env=environment_env, ), ) if cli_path is not None: diff --git a/tests/e2e/test_codex.py b/tests/e2e/test_codex.py index ac059d4d7..da0c9039b 100644 --- a/tests/e2e/test_codex.py +++ b/tests/e2e/test_codex.py @@ -17,13 +17,6 @@ from _utils.utils import assert_semantic_relay_artifacts -def _select_codex_runtime(config): - codex_bin = os.environ.get("FABRIC_TEST_CODEX_BIN") - if codex_bin: - config.harness.settings["codex_bin"] = codex_bin - return config - - async def test_codex_sdk(): if os.environ.get("RUN_FABRIC_CODEX_INTEGRATION") != "1": pytest.skip("set RUN_FABRIC_CODEX_INTEGRATION=1 to run") @@ -46,14 +39,14 @@ async def test_codex_sdk_with_relay(): ) if relay_command is None: pytest.fail("the nemo-relay CLI is required") - await _run_relay(relay_command) + await _run_relay() async def _run() -> None: from examples.code_review_agent import BASE_DIR, codex_config from nemo_fabric import Fabric - config = _select_codex_runtime(codex_config()) + config = codex_config() nonce = f"fabric-{uuid.uuid4().hex[:8]}" client = Fabric() single = await client.run( @@ -87,12 +80,11 @@ async def _run() -> None: assert second["output"]["usage"] is not None, second.to_mapping() -async def _run_relay(relay_command: str) -> None: +async def _run_relay() -> None: from examples.code_review_agent import BASE_DIR, codex_config, with_relay from nemo_fabric import Fabric - config = _select_codex_runtime(with_relay(codex_config())) - config.harness.settings["nemo_relay_command"] = relay_command + config = with_relay(codex_config()) client = Fabric() result = await client.run( config, diff --git a/tests/e2e/test_deepagents.py b/tests/e2e/test_deepagents.py index 5a90d3d07..6700f1675 100644 --- a/tests/e2e/test_deepagents.py +++ b/tests/e2e/test_deepagents.py @@ -23,8 +23,7 @@ async def test_deepagents_persistent_host_with_mock_model(api_server, tmp_path): from nemo_fabric import EnvironmentConfig, Fabric, RuntimeConfig config = deepagents_config() - config.harness.settings["base_url"] = f"{api_server}/v1" - config.harness.settings["workspace"] = str(tmp_path) + config.models["default"].base_url = f"{api_server}/v1" config.environment = EnvironmentConfig( provider="local", workspace=tmp_path, @@ -59,8 +58,7 @@ async def test_deepagents_persistent_host_with_relay_and_mock_model( from nemo_fabric import EnvironmentConfig, Fabric, RuntimeConfig config = with_relay(deepagents_config()) - config.harness.settings["base_url"] = f"{api_server}/v1" - config.harness.settings["workspace"] = str(tmp_path) + config.models["default"].base_url = f"{api_server}/v1" config.environment = EnvironmentConfig( provider="local", workspace=tmp_path, diff --git a/tests/e2e/test_hermes_config_mapping.py b/tests/e2e/test_hermes_config_mapping.py index 9920260c2..ad175d454 100644 --- a/tests/e2e/test_hermes_config_mapping.py +++ b/tests/e2e/test_hermes_config_mapping.py @@ -52,19 +52,25 @@ def payload(tmpdir: str) -> dict: return { "agent_name": "code-review-agent", "base_dir": tmpdir, - "environment": { - "workspace": f"{tmpdir}/workspace", - }, - "models": { - "default": { - "provider": "nvidia", - "model": "nvidia/nemotron-3-nano-30b-a3b", - "api_key_env": "NVIDIA_API_KEY", - } + "config": { + "environment": { + "workspace": f"{tmpdir}/workspace", + }, + "models": { + "default": { + "provider": "nvidia", + "model": "nvidia/nemotron-3-nano-30b-a3b", + "api_key_env": "NVIDIA_API_KEY", + "base_url": "https://integrate.api.nvidia.com/v1", + } + }, + "tools": { + "toolsets": { + "enabled": [], + } + }, }, "settings": { - "enabled_toolsets": [], - "terminal_backend": "local", "terminal_timeout": 30, }, "capabilities": { diff --git a/tests/e2e/test_hermes_e2e.py b/tests/e2e/test_hermes_e2e.py index eb63e298e..d89931417 100644 --- a/tests/e2e/test_hermes_e2e.py +++ b/tests/e2e/test_hermes_e2e.py @@ -28,7 +28,7 @@ async def test_hermes_persistent_host_reuses_native_session( ): os.environ["ADAPTER_PYTHON"] = sys.executable config = hermes_config() - config.harness.settings["base_url"] = f"{api_server}/v1" + config.models["default"].base_url = f"{api_server}/v1" async with await Fabric().start_runtime( config, base_dir=code_review_agent_dir @@ -50,7 +50,7 @@ async def test_hermes_persistent_host_with_relay( ): os.environ["ADAPTER_PYTHON"] = sys.executable config = with_relay(hermes_config()) - config.harness.settings["base_url"] = f"{api_server}/v1" + config.models["default"].base_url = f"{api_server}/v1" async with await Fabric().start_runtime( config, base_dir=code_review_agent_dir @@ -104,7 +104,7 @@ async def run_hermes_with_relay( self.code_review_agent_dir = code_review_agent_dir self.api_server = api_server config = self.config_builder() - config.harness.settings["base_url"] = f"{api_server}/v1" + config.models["default"].base_url = f"{api_server}/v1" config = with_relay(config) self.result = await Fabric().run( diff --git a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json index 3df5e70c0..118d54ba9 100644 --- a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json +++ b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json @@ -17,7 +17,8 @@ }, "config": { "accepts": [ - "tools", + "models", + "models.temperature", "mcp", "skills", "telemetry" diff --git a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py index d89373e1c..4547cbb84 100644 --- a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py +++ b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py @@ -87,7 +87,7 @@ def run_shim(payload: dict[str, Any]) -> dict[str, Any]: "mode": "shim", "received": request.get("input"), "runtime_id": context.get("runtime_id"), - "workspace": environment.get("workspace") or settings.get("workspace"), + "workspace": environment.get("workspace"), "native_skill_paths": (capabilities.get("native") or {}).get("skill_paths", []), "native_mcp_servers": sorted( (capabilities.get("native") or {}).get("mcp_servers", {}).keys() @@ -108,7 +108,7 @@ def run_swebench_shim(payload: dict[str, Any]) -> dict[str, Any]: request = request_payload(payload) context = request.get("context", {}) environment = environment_payload(payload) - workspace = Path(environment.get("workspace") or settings.get("workspace") or ".") + workspace = Path(environment.get("workspace") or ".") target_file = workspace / settings.get("target_file", "calculator.py") before = settings.get("expected_before") after = settings.get("replacement") diff --git a/tests/integrations/test_harbor_runner.py b/tests/integrations/test_harbor_runner.py index 3e8f239eb..0d925f622 100644 --- a/tests/integrations/test_harbor_runner.py +++ b/tests/integrations/test_harbor_runner.py @@ -243,13 +243,15 @@ def test_claude_calculator_run_uses_current_adapter_contract(): adapter_id="nvidia.fabric.claude", workspace="/app", model_name="anthropic/claude-sonnet-4-5", - harness_settings={"max_turns": 20, "timeout_seconds": 600}, + max_turns=20, + timeout_seconds=600, ) settings = config.harness.settings assert config.harness.adapter_id == "nvidia.fabric.claude" assert settings["permission_mode"] == "bypassPermissions" - assert settings["max_turns"] == 20 + assert config.max_turns == 20 + assert config.runtime.timeout_seconds == 600 assert config.models["default"].provider == "anthropic" dockerfile = CALCULATOR_DOCKERFILE.read_text(encoding="utf-8") assert "-e /opt/nemo-fabric/adapters/claude" in dockerfile @@ -424,17 +426,22 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path ), ) tools = build_harbor_config( - adapter_id="nvidia.fabric.hermes", + adapter_id="nvidia.fabric.deepagents", workspace="/testbed", blocked_tools=["browser"], telemetry="relay", ) + toolsets = build_harbor_config( + adapter_id="nvidia.fabric.hermes", + workspace="/testbed", + enabled_toolsets=[], + blocked_toolsets=["browser"], + telemetry="relay", + ) claude = build_harbor_config( adapter_id="nvidia.fabric.claude", workspace="/testbed", - harness_settings={ - "nemo_relay_command": "/tmp/nemo-fabric-config/.relay/bin/nemo-relay" - }, + harness_settings={"allowed_tools": ["Read"]}, ) assert base.environment is not None @@ -454,17 +461,21 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path ] assert tools.tools is not None assert tools.tools.blocked == ["browser"] - assert "enabled_toolsets" not in tools.harness.settings + assert tools.tools.toolsets is None + assert toolsets.tools is not None + assert toolsets.tools.blocked == [] + assert toolsets.tools.toolsets is not None + assert toolsets.tools.toolsets.enabled == [] + assert toolsets.tools.toolsets.blocked == ["browser"] assert relay.telemetry is not None assert "relay" in relay.telemetry.providers assert relay.relay is not None assert relay.relay.observability.atif.enabled is True assert relay.relay.observability.atof.enabled is True assert claude.harness.settings["permission_mode"] == "bypassPermissions" - assert claude.harness.settings["env"] == {"IS_SANDBOX": "1"} - assert claude.harness.settings["nemo_relay_command"] == ( - "/tmp/nemo-fabric-config/.relay/bin/nemo-relay" - ) + assert claude.environment is not None + assert claude.environment.env == {"IS_SANDBOX": "1"} + assert claude.harness.settings["allowed_tools"] == ["Read"] assert not list(SWEBENCH_ROOT.rglob("*.yaml")) assert not (SWEBENCH_ROOT / "harbor_swebench_config.py").exists() diff --git a/tests/python/test_native_sdk.py b/tests/python/test_native_sdk.py index 7ca05cb92..097905c71 100644 --- a/tests/python/test_native_sdk.py +++ b/tests/python/test_native_sdk.py @@ -65,6 +65,17 @@ def test_native_run_rejects_multiple_request_sources(hermes_shim_agent_dir: Path ) +def test_plan_rejects_adapter_incompatible_normalized_tool_policy(): + config = base_config() + config.block_tools("Bash") + + with pytest.raises( + FabricConfigError, + match=r"nvidia\.fabric\.hermes.*tools\.blocked", + ): + Fabric().plan(config, base_dir=BASE_DIR) + + async def smoke(client: Fabric, fixture_agent: Path) -> None: example_config = base_config() @@ -96,10 +107,7 @@ async def smoke(client: Fabric, fixture_agent: Path) -> None: "harness": { "adapter_id": "test.fabric.hermes_shim", "resolution": "preinstalled", - "settings": { - "workspace": "./repos/my-service", - "timeout_seconds": 30, - }, + "settings": {}, }, "models": { "default": { @@ -112,6 +120,7 @@ async def smoke(client: Fabric, fixture_agent: Path) -> None: "input_schema": "chat", "output_schema": "message", "artifacts": "./artifacts", + "timeout_seconds": 30, }, "environment": { "provider": "local", @@ -145,8 +154,8 @@ async def smoke(client: Fabric, fixture_agent: Path) -> None: assert typed_plan["telemetry_plan"]["relay_enabled"] is True resolved_config = typed_plan.config.to_mapping() assert resolved_config["harness"]["adapter_id"] == "test.fabric.hermes_shim" - assert resolved_config["harness"]["settings"]["workspace"] == "./repos/my-service" - assert resolved_config["harness"]["settings"]["timeout_seconds"] == 30 + assert "settings" not in resolved_config["harness"] + assert resolved_config["runtime"]["timeout_seconds"] == 30 assert resolved_config["consumer_extension"] == { "base": True, "custom": True, diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index e37ee1487..f60c703f3 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -45,6 +45,7 @@ from nemo_fabric import RuntimeHandle from nemo_fabric import SkillConfig from nemo_fabric import TelemetryConfig +from nemo_fabric import ToolsetConfig from nemo_fabric import ToolsConfig from nemo_fabric.types import _FabricConfigSnapshot from nemo_fabric.types import _ToolsConfig @@ -145,13 +146,20 @@ def test_typed_config_authoring_helpers_emit_schema_shape(): output_dir="./artifacts/relay", ) config.block_tools("browser", "shell", "browser") + config.configure_toolsets(enabled=["terminal"], blocked=["browser"]) assert isinstance(config.mcp, McpConfig) assert isinstance(config.skills, SkillConfig) assert isinstance(config.telemetry, TelemetryConfig) assert isinstance(config.tools, ToolsConfig) - assert config.to_mapping()["tools"] == {"blocked": ["browser", "shell"]} + assert config.to_mapping()["tools"] == { + "blocked": ["browser", "shell"], + "toolsets": { + "enabled": ["terminal"], + "blocked": ["browser"], + }, + } assert config.to_mapping()["skills"] == {"paths": ["./skills/review"]} assert config.to_mapping()["mcp"] == { "servers": { @@ -201,6 +209,65 @@ def test_typed_tools_config_serializes_blocked_policy(): assert config.to_mapping()["tools"] == {"blocked": ["browser", "shell"]} +def test_typed_config_serializes_normalized_execution_fields(): + config = FabricConfig( + metadata=MetadataConfig(name="demo"), + harness=HarnessConfig(adapter_id="test.fabric.shim"), + system_prompt="Be concise.", + max_turns=7, + runtime=RuntimeConfig(timeout_seconds=12.5), + environment=EnvironmentConfig(env={"VISIBLE": "yes"}), + models={ + "default": { + "provider": "nvidia", + "model": "nvidia/test", + "base_url": "https://models.example/v1", + } + }, + tools=ToolsConfig( + toolsets=ToolsetConfig(enabled=[], blocked=["browser"]) + ), + ) + + mapping = config.to_mapping() + assert mapping["system_prompt"] == "Be concise." + assert mapping["max_turns"] == 7 + assert mapping["runtime"]["timeout_seconds"] == 12.5 + assert mapping["environment"]["env"] == {"VISIBLE": "yes"} + assert mapping["models"]["default"]["base_url"] == ( + "https://models.example/v1" + ) + assert mapping["tools"]["toolsets"] == { + "enabled": [], + "blocked": ["browser"], + } + + with pytest.raises(ValidationError, match="greater than 0"): + RuntimeConfig(timeout_seconds=0) + with pytest.raises(ValidationError, match="greater than 0"): + FabricConfig( + metadata=MetadataConfig(name="demo"), + harness=HarnessConfig(adapter_id="test.fabric.shim"), + max_turns=0, + ) + with pytest.raises(ValidationError, match="both enabled and blocked"): + ToolsetConfig(enabled=["browser"], blocked=["browser"]) + with pytest.raises(ValidationError, match="models..base_url"): + FabricConfig( + metadata=MetadataConfig(name="demo"), + harness=HarnessConfig(adapter_id="test.fabric.shim"), + models={ + "default": { + "provider": "nvidia", + "model": "nvidia/test", + "settings": { + "base_url": "https://legacy.example/v1", + }, + } + }, + ) + + def test_run_plan_config_block_tools_emits_canonical_shape(): config = _FabricConfigSnapshot.from_mapping(_plan()["config"]) @@ -209,6 +276,32 @@ def test_run_plan_config_block_tools_emits_canonical_shape(): assert config.to_mapping()["tools"] == {"blocked": ["browser", "shell"]} +def test_run_plan_config_preserves_normalized_toolsets_and_execution_fields(): + raw = _plan()["config"] + raw.update( + { + "system_prompt": "Be concise.", + "max_turns": 5, + "runtime": {"timeout_seconds": 9}, + "environment": {"provider": "local", "env": {"VISIBLE": "yes"}}, + "tools": { + "toolsets": {"enabled": [], "blocked": ["browser"]}, + }, + } + ) + + config = _FabricConfigSnapshot.from_mapping(raw) + + assert config.to_mapping()["system_prompt"] == "Be concise." + assert config.to_mapping()["max_turns"] == 5 + assert config.to_mapping()["runtime"]["timeout_seconds"] == 9 + assert config.to_mapping()["environment"]["env"] == {"VISIBLE": "yes"} + assert config.to_mapping()["tools"]["toolsets"] == { + "enabled": [], + "blocked": ["browser"], + } + + def test_run_plan_tools_config_rejects_scalar_blocked_value(): with pytest.raises(FabricConfigError, match="tools blocked"): _ToolsConfig(blocked="browser") # type: ignore[arg-type] @@ -411,6 +504,7 @@ def test_environment_model_defines_extension_field_ownership(): properties = EnvironmentConfig.model_json_schema()["properties"] assert "environment provider" in properties["settings"]["description"] + assert "harness and its tools" in properties["env"]["description"] assert "without Fabric semantics" in properties["metadata"]["description"] assert "existing environment" in properties["connection"]["description"] assert "environment teardown" in properties["ownership"]["description"] @@ -1185,7 +1279,7 @@ def test_fabric_config_constructors_emit_schema_shaped_mappings(): harness=HarnessConfig( adapter_id="test.fabric.shim", resolution="preinstalled", - settings={"workspace": "./ws"}, + settings={"custom_option": "original"}, ), runtime=RuntimeConfig( input_schema="chat", @@ -1193,13 +1287,13 @@ def test_fabric_config_constructors_emit_schema_shaped_mappings(): ), ) copied = config.to_mapping() - copied["harness"]["settings"]["workspace"] = "mutated" + copied["harness"]["settings"]["custom_option"] = "mutated" assert config.schema_version == "fabric.agent/v1alpha1" assert config.metadata.to_mapping() == {"name": "demo"} assert config.harness.adapter_id == "test.fabric.shim" assert config.runtime.input_schema == "chat" - assert config.harness.settings["workspace"] == "./ws" + assert config.harness.settings["custom_option"] == "original" client = NativeClient(NativeRecorder()) client.plan(config) diff --git a/tests/python/test_typed_config.py b/tests/python/test_typed_config.py index 8dc8446b7..82f5f8e2d 100644 --- a/tests/python/test_typed_config.py +++ b/tests/python/test_typed_config.py @@ -19,8 +19,11 @@ from pathlib import Path from shutil import copytree +import pytest + from nemo_fabric import Fabric from nemo_fabric import FabricConfig +from nemo_fabric import FabricConfigError from nemo_fabric import RunRequest from nemo_fabric import RunResult @@ -69,7 +72,7 @@ def _shim_adapter_config() -> FabricConfig: config["harness"] = { "adapter_id": "test.fabric.hermes_shim", "resolution": "preinstalled", - "settings": {"workspace": "./ws"}, + "settings": {}, } config["models"] = { "default": {"provider": "test", "model": "test-model", "temperature": 0.0} From bc614e0238c6178b4a45112a008ab4f180c70946 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 01:35:09 -0700 Subject: [PATCH 02/21] docs: document normalized adapter compatibility Signed-off-by: Ajay Thorve --- README.md | 14 +- adapters/README.md | 60 ++++- adapters/claude/README.md | 47 ++-- adapters/codex/README.md | 34 ++- adapters/deepagents/README.md | 18 +- adapters/hermes/README.md | 21 +- docs/getting-started/quickstart.mdx | 6 +- docs/integrations/claude.mdx | 7 +- docs/integrations/codex.mdx | 15 +- .../api/python-library-reference/index.md | 1 + .../nemo_fabric.client.md | 2 +- .../nemo_fabric.models.md | 80 +++++- .../config/enum-adapterconfigfield.mdx | 234 ++++++++++++++++++ .../config/enum-adapterdescriptorsource.mdx | 2 +- .../config/enum-adapterkind.mdx | 2 +- .../config/enum-capabilitykind.mdx | 2 +- .../config/enum-capabilitytarget.mdx | 2 +- .../config/enum-controllocation.mdx | 2 +- .../config/enum-environmentownership.mdx | 2 +- .../config/enum-mcpexposure.mdx | 2 +- .../config/enum-relayatifstorageconfig.mdx | 2 +- .../config/enum-relayatofmode.mdx | 2 +- .../config/enum-relayatofsinkconfig.mdx | 2 +- .../enum-relayatofstreamfieldnamepolicy.mdx | 2 +- .../config/enum-relayatofstreamtransport.mdx | 2 +- .../config/enum-relayotlptransport.mdx | 2 +- .../config/enum-relayunsupportedbehavior.mdx | 2 +- .../config/enum-resolutionstrategy.mdx | 2 +- .../config/enum-telemetryprovider.mdx | 2 +- .../config/fn-load-adapter-descriptor.mdx | 2 +- .../fn-resolve-run-plan-from-config.mdx | 2 +- .../nemo-fabric-core/config/index.mdx | 4 +- .../config/struct-adapterconfigsupport.mdx | 8 +- .../config/struct-adapterdescriptor.mdx | 2 +- .../config/struct-adapterrequirements.mdx | 2 +- ...struct-adaptertelemetryprovidersupport.mdx | 2 +- .../config/struct-adaptertelemetrysupport.mdx | 2 +- .../config/struct-capabilityplan.mdx | 2 +- .../config/struct-environmentconfig.mdx | 8 +- .../config/struct-environmentplan.mdx | 8 +- .../config/struct-fabricconfig.mdx | 14 +- .../config/struct-harnessconfig.mdx | 2 +- .../config/struct-mcpconfig.mdx | 2 +- .../config/struct-mcpserverplan.mdx | 2 +- .../config/struct-metadataconfig.mdx | 2 +- .../config/struct-modelconfig.mdx | 8 +- .../config/struct-resolvecontext.mdx | 2 +- .../struct-resolvedadapterdescriptor.mdx | 2 +- .../config/struct-runplan.mdx | 2 +- .../config/struct-runtimecapabilities.mdx | 2 +- .../config/struct-runtimeconfig.mdx | 8 +- .../config/struct-skillconfig.mdx | 2 +- .../config/struct-telemetryconfig.mdx | 2 +- .../config/struct-telemetryplan.mdx | 2 +- .../config/struct-telemetryproviderconfig.mdx | 2 +- .../config/struct-toolsconfig.mdx | 10 +- .../config/struct-toolsetconfig.mdx | 110 ++++++++ .../config/struct-toolsplan.mdx | 8 +- .../doctor/enum-doctorstatus.mdx | 2 +- .../doctor/fn-doctor-plan.mdx | 2 +- .../nemo-fabric-core/doctor/index.mdx | 2 +- .../doctor/struct-doctorcheck.mdx | 2 +- .../doctor/struct-doctorreport.mdx | 2 +- .../error/enum-fabricerror.mdx | 56 +++-- .../nemo-fabric-core/error/index.mdx | 2 +- .../nemo-fabric-core/error/type-result.mdx | 2 +- .../nemo-fabric-core/fn-version.mdx | 2 +- .../nemo-fabric-core/index.mdx | 3 + .../nemo-fabric-core/runtime/index.mdx | 2 +- .../runtime/struct-environmenthandle.mdx | 6 +- .../nemo-fabric-core/schema/index.mdx | 2 +- docs/sdk/python.mdx | 87 ++++++- examples/harbor/README.md | 10 +- examples/harbor/calculator/README.md | 10 +- examples/harbor/swebench/README.md | 6 +- examples/notebooks/01_quickstart.ipynb | 20 +- skills/nemo-fabric-integrate/SKILL.md | 21 +- .../references/config-mapping.md | 29 ++- 78 files changed, 842 insertions(+), 221 deletions(-) create mode 100644 docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx create mode 100644 docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx diff --git a/README.md b/README.md index ec1c630d7..841111af4 100644 --- a/README.md +++ b/README.md @@ -191,17 +191,19 @@ runtimes, authentication, and execution details. - **Experimentation CLI:** presets provide quick probes, examples provide maintained runnable workflows, and `example init` generates editable Python or Rust applications that call a language API directly. -- **Tools policy:** use top-level `tools.blocked` for harness-neutral blocked - tool policy. Names are interpreted by the selected adapter: +- **Tools policy:** use `tools.blocked` for individual tool names and + `tools.toolsets` for harness-defined tool bundles. They are separate policies + and adapters declare support for each independently: ```python config.block_tools("browser", "shell") + config.configure_toolsets(enabled=["web", "terminal"], blocked=["browser"]) ``` - The selected adapter interprets these names: Hermes Agent maps them to disabled - toolsets, Claude maps them to `disallowed_tools`, Deep Agents enforces them - with middleware, and adapters without a native deny mechanism route the - policy as unsupported. + Claude maps blocked tools to `disallowed_tools`; Deep Agents enforces them + with middleware. Hermes Agent maps `tools.toolsets` into its toolset controls. + An adapter that cannot enforce a configured policy fails planning with a + compatibility error instead of applying only part of it. - **Adapters:** harness-specific integrations selected by `harness.adapter_id`. Harness-specific extensions belong under `harness.settings` so the normalized contract can remain stable. Refer to the diff --git a/adapters/README.md b/adapters/README.md index 1487586e0..8fbdea124 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -43,19 +43,63 @@ provider should expose more precise provenance. ## Configuration Compatibility -| Agent Harness | Models | Tools / Blocked Tools | MCP | Skills | Subagents | +| Agent Harness | Models | Tool Policy | MCP | Skills | Subagents | | --- | --- | --- | --- | --- | --- | -| [Claude](claude/README.md) | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | `allowed_tools` adapter setting / normalized `tools.blocked` | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | -| [Codex](codex/README.md) | OpenAI; NVIDIA Responses-compatible models without Relay | Codex-native tools / configuring `tools.blocked` is unsupported and raises `UnsupportedToolsPolicy` | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | -| [LangChain Deep Agents](deepagents/README.md) | LangChain model providers | Built-ins and MCP / normalized middleware block list | Normalized through `langchain-mcp-adapters` | Normalized | Constrained declarative local delegation | -| [Hermes Agent](hermes/README.md) | Normalized provider, model, and base URL | Toolsets / normalized disabled toolsets | Normalized | Normalized | Not exposed | +| [Claude](claude/README.md) | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | `tools.blocked` maps to Claude `disallowed_tools`; toolsets unsupported | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | +| [Codex](codex/README.md) | OpenAI and NVIDIA-hosted Responses-compatible models | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | +| [LangChain Deep Agents](deepagents/README.md) | LangChain model providers | `tools.blocked` middleware covers built-ins, MCP, and local subagents; toolsets unsupported | Normalized through `langchain-mcp-adapters` | Normalized | Constrained declarative local delegation | +| [Hermes Agent](hermes/README.md) | Configurable provider, model, and base URL | `tools.toolsets` maps to Hermes toolset controls; per-tool blocking unsupported | Normalized | Normalized | Not exposed | "Normalized" means that the adapter accepts the corresponding `FabricConfig` field. "Not exposed" does not mean that the underlying harness lacks the feature; it means that NeMo Fabric does not provide a portable configuration surface -for it. NeMo Fabric normalizes a blocked-tool list, not a portable tool-definition -catalog. Deep Agents subagents are limited to declarative local subagents that -inherit the parent agent's capabilities. +for it. Individual tools and harness-defined toolsets are separate normalized +policies. Planning fails when the selected adapter cannot enforce a configured +policy; NeMo Fabric does not reinterpret one as the other. Deep Agents subagents +are limited to declarative local subagents that inherit the parent agent's +capabilities. + +### Complete `FabricConfig` Support + +`Core` means Fabric owns the behavior and applies it uniformly before or around +adapter execution. `Yes` means the adapter translates the normalized field into +its harness. `No` means an explicitly configured value fails planning instead +of being ignored. Provider-specific Relay subfields and additive extension maps +are grouped because their support does not vary by adapter. + +| `FabricConfig` field | Claude | Codex | Deep Agents | Hermes Agent | +| --- | --- | --- | --- | --- | +| `schema_version` | Core | Core | Core | Core | +| `metadata.name`, `.description` | Core | Core | Core | Core | +| `harness.adapter_id`, `.resolution` | Core | Core | Core | Core | +| `harness.settings` | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | +| `models..provider` | `anthropic`, `nvidia` | `openai`, `nvidia` | Dynamic LangChain provider | Dynamic Hermes provider | +| `models..model` | Yes | Yes | Yes | Yes | +| `models..api_key_env` | Yes | Yes | Yes | Yes | +| `models..base_url` | Yes | Yes | Yes | Yes | +| `models..temperature` | No | No | Yes | Yes | +| `models..settings.` | No keys declared | No keys declared | No keys declared | No keys declared | +| `system_prompt` | Yes | Yes; base instructions | Yes | Yes | +| `max_turns` | Yes | No | No | Yes; iteration limit | +| `runtime.input_schema`, `.output_schema` | Core | Core | Core | Core | +| `runtime.artifacts`, `.timeout_seconds` | Core | Core | Core | Core | +| `environment.provider`, `.control_location`, `.ownership` | Core | Core | Core | Core | +| `environment.workspace`, `.artifacts`, `.env` | Core | Core | Core | Core | +| `environment.connection`, `.metadata`, `.settings` | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | +| `tools.blocked` | Yes | No | Yes | No | +| `tools.toolsets.enabled`, `.blocked` | No | No | No | Yes | +| `skills.paths` | Yes | Yes | Yes | Yes | +| `mcp.servers..transport`, `.url` with `harness_native` exposure | Yes | Yes | Yes | Yes | +| `mcp.servers..exposure = "fabric_managed"` | No; not implemented | No; not implemented | No; not implemented | No; not implemented | +| `telemetry.providers.relay` | Yes | Yes | Yes | Yes | +| `telemetry.providers.native` | No | Yes; OpenTelemetry | Yes; OpenTelemetry and OpenInference | No | +| `telemetry.providers..config` | Declared-provider pass-through | Declared-provider pass-through | Declared-provider pass-through | Declared-provider pass-through | +| `relay.project`, `.output_dir`, `.observability` | Yes | Yes | Yes | Yes | +| `relay.components`, `.policy` | Yes | Yes | Yes | Yes | +| Additive `extensions` on typed config objects | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | + +The selected model role is `default`, or the sole configured role when no +`default` exists. More than one role without `default` fails planning. ## Runtime and Observability Compatibility diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 745bf271e..5a6837cf9 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -9,9 +9,8 @@ The `nvidia.fabric.claude` adapter uses the official Claude Agent SDK for Python behind NeMo Fabric's normalized invocation contract. The SDK is an implementation detail; consumers select the Claude harness by adapter ID. -This adapter pins `claude-agent-sdk==0.2.120`. The SDK supplies its compatible -Claude Code runtime unless `harness.settings.cli_path` explicitly selects -another executable. +This adapter pins `claude-agent-sdk==0.2.120`. The SDK supplies and selects its +compatible Claude Code runtime. ## Install @@ -35,12 +34,12 @@ bearer credential, `ANTHROPIC_API_KEY` for a static API credential, or Anthropic Workload Identity Federation (WIF) for production and CI workloads that should not store a long-lived API key. -When `models.default.provider` is `nvidia`, the adapter reads the selected +When the selected model provider is `nvidia`, the adapter reads the selected model's credential from `api_key_env` (default: `NVIDIA_API_KEY`) and translates the configured NVIDIA `/v1` endpoint into the host URL expected by Claude Code. -Set the endpoint in `models.default.settings.base_url` or -`NVIDIA_FRONTIER_BASE_URL`; the adapter does not assume a default frontier -endpoint. This request-scoped mapping does not change the parent environment. +`models..base_url` overrides the public NVIDIA API Catalog endpoint. Set +it explicitly for a frontier or self-hosted endpoint. This request-scoped +mapping does not change the parent environment. The adapter forwards the Anthropic profile and federation environment variables that Claude Code and the Claude Agent SDK consume. This includes @@ -59,7 +58,6 @@ for mode selection, required WIF variables, and the Relay boundary. Package installation is verified by the adapter wheel and module-entrypoint tests. Relay-enabled runs also require the external `nemo-relay` CLI. Refer to the [NeMo Relay CLI](https://docs.nvidia.com/nemo/fabric/getting-started/install#nemo-relay-cli) install guide for instructions on installing the CLI tool. -``` The Python `nemo-relay` package does not install this executable. Refer to the [NeMo Relay installation guide](https://docs.nvidia.com/nemo/relay/getting-started/installation) @@ -86,8 +84,12 @@ Configure portable capabilities through the normalized `FabricConfig` fields: - `models` selects the Claude model. The adapter accepts the native `anthropic` provider and NVIDIA-hosted Anthropic Messages-compatible models through the - `nvidia` provider. -- `environment.workspace` sets the Claude working directory. + `nvidia` provider. `models..base_url` selects an explicit endpoint. +- `system_prompt` supplies the Claude system instructions. +- `max_turns` sets the Claude turn limit. +- `runtime.timeout_seconds` sets the Fabric invocation deadline. +- `environment.workspace` sets the Claude working directory, and + `environment.env` supplies explicit harness-visible variables. - `tools.blocked` maps to Claude `disallowed_tools` using Claude-native tool names. - `mcp` configures stdio, HTTP, streamable HTTP, or SSE servers. For stdio, @@ -97,20 +99,18 @@ Configure portable capabilities through the normalized `FabricConfig` fields: Only Claude-specific controls belong in `harness.settings`: -- `system_prompt`, `allowed_tools`, and `permission_mode` -- `max_turns`, `max_budget_usd`, and `timeout_seconds` +- `allowed_tools` and `permission_mode` +- `max_budget_usd` - `setting_sources` (defaults to `[]` for deterministic isolation) -- `cli_path` for testing or an explicitly installed Claude Code executable -- `nemo_relay_command` for an explicitly installed NeMo Relay CLI executable -- `env` for variables explicitly forwarded to Claude Code -Putting `model_name`, `cwd`, `tools`, `disallowed_tools`, `mcp_servers`, or -`skills` in `harness.settings` is an error. Use the corresponding normalized -field so the same consumer configuration can compose with other adapters. +Putting `model_name`, `base_url`, `cwd`, `system_prompt`, `max_turns`, +`timeout_seconds`, `env`, `tools`, `disallowed_tools`, `mcp_servers`, or `skills` +in `harness.settings` is an error. Use the corresponding normalized field so +the same consumer configuration can compose with other adapters. The adapter filters the inherited environment before launching Claude Code. It retains portable OS/config variables, the selected model's `api_key_env`, -and explicitly configured `settings.env` values. Raw Claude stderr is consumed +and explicitly configured `environment.env` values. Raw Claude stderr is consumed by the SDK and is not persisted as a NeMo Fabric artifact. ## Relay Observability @@ -136,8 +136,7 @@ The NeMo Fabric result includes `relay_runtime.gateway_config_path`, `relay_runtime.gateway_log_path`, and the collected `relay_artifacts`. Relay startup failures return a stable adapter error and retain the gateway log for diagnosis. The default Claude Agent SDK dependency bundles a compatible Claude -Code executable. An executable supplied with `cli_path` must support the Relay -plugin's complete hook set, including `UserPromptExpansion`. +Code executable. ## Typed Configuration @@ -168,9 +167,7 @@ config = FabricConfig( adapter_id="nvidia.fabric.claude", resolution="preinstalled", settings={ - "system_prompt": "Review changes for correctness and regressions.", "permission_mode": "dontAsk", - "max_turns": 8, }, ), models={ @@ -180,7 +177,9 @@ config = FabricConfig( api_key_env="ANTHROPIC_API_KEY", ) }, - runtime=RuntimeConfig(artifacts="./artifacts"), + system_prompt="Review changes for correctness and regressions.", + max_turns=8, + runtime=RuntimeConfig(artifacts="./artifacts", timeout_seconds=600), environment=EnvironmentConfig(provider="local", workspace="."), tools=ToolsConfig(blocked=["WebFetch"]), mcp=McpConfig( diff --git a/adapters/codex/README.md b/adapters/codex/README.md index 660af0ffd..bfd5ce0ac 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -49,25 +49,21 @@ the SDK runtime. The current real-agent acceptance path validates an existing Codex login; it does not yet claim a raw environment variable as a complete login flow. -When `models.default.provider` is `nvidia`, the adapter defines a Codex model +When the selected model provider is `nvidia`, the adapter defines a Codex model provider for the configured NVIDIA Responses endpoint. `Fabric.run(...)` owns that provider for one invocation, while `Fabric.start_runtime(...)` fixes it for the lifetime of the persistent runtime. The adapter reads the credential from `api_key_env` (default: `NVIDIA_API_KEY`) and isolates Codex state under the NeMo Fabric artifact root, so execution does not depend on or modify a user's Codex login. Set the endpoint in -`models.default.settings.base_url` or `NVIDIA_FRONTIER_BASE_URL`; the adapter -does not assume a default frontier endpoint. +`models..base_url`; it defaults to the public NVIDIA API Catalog endpoint. +Set it explicitly for a frontier or self-hosted endpoint. The adapter depends on the Codex SDK, which installs and selects its matching app-server runtime. NeMo Fabric does not declare the runtime package directly or treat it as a user-installed command or adapter descriptor requirement. -A `codex` command on `PATH` is not selected implicitly. To override the -SDK-selected runtime intentionally, set -`harness.settings.codex_bin` to an app-server path that is absolute or relative -to the explicit `base_dir`. NeMo Fabric passes the resolved path through -`CodexConfig.codex_bin`; the SDK remains the execution driver. +A `codex` command on `PATH` is not selected implicitly. ## Execution Model @@ -89,8 +85,11 @@ Use normalized `FabricConfig` fields for portable configuration: - `models` selects the Codex model. The adapter supports the built-in `openai` provider and NVIDIA-hosted Responses-compatible models through the `nvidia` - provider. -- `environment.workspace` sets the working directory. + provider. `models..base_url` selects an explicit endpoint. +- `system_prompt` maps to Codex base instructions. +- `runtime.timeout_seconds` sets the Fabric invocation deadline. +- `environment.workspace` sets the working directory, and `environment.env` + supplies explicit harness-visible variables. - `mcp` maps stdio, HTTP, and streamable HTTP servers into the Codex thread's `mcp_servers` configuration. For stdio, NeMo Fabric parses `url` as a command plus arguments. @@ -109,18 +108,15 @@ Codex-specific controls belong in `harness.settings`: - `sandbox`: `read-only`, `workspace-write`, or `danger-full-access` - `approval_mode`: `auto_review` or `deny_all` -- `base_instructions` and `developer_instructions` -- `personality`, `reasoning_effort`, `service_name`, and `service_tier` +- `developer_instructions` +- `personality`, `reasoning_effort`, and `service_tier` - `output_schema` for SDK-native structured output -- `codex_bin` for an explicit Codex app-server runtime override - `config_overrides` as dotted Codex configuration keys applied when the SDK runtime starts, such as Codex-only MCP timeout or required-server options -- `timeout_seconds`, defaulting to 1800 -- `env` for variables explicitly forwarded to the Codex runtime -- `nemo_relay_command` for the optional external Relay gateway executable -Set model selection through `models` and the working directory through -`environment.workspace`. +Set model selection and endpoints through `models`, system instructions through +`system_prompt`, the invocation deadline through `runtime.timeout_seconds`, and +the working directory and explicit environment through `environment`. For `Fabric.start_runtime(...)`, the model provider, MCP configuration, skill roots, and `config_overrides` are fixed when the runtime starts and cannot vary @@ -130,7 +126,7 @@ same settings are scoped to that single invocation. The adapter filters the inherited environment. It retains portable OS and Codex state variables, the selected model's `api_key_env`, and explicit -`settings.env` values while clearing unrelated parent-process secrets. +`environment.env` values while clearing unrelated parent-process secrets. ## Relay Integration diff --git a/adapters/deepagents/README.md b/adapters/deepagents/README.md index 3b105e020..7d02d47b3 100644 --- a/adapters/deepagents/README.md +++ b/adapters/deepagents/README.md @@ -46,11 +46,10 @@ adapter resolution. NeMo Fabric maps the following into the harness: -- `models.default.model` / `harness.settings.model_name` selects the model. -- `models.default.provider` selects the client (`nvidia`/`openai` → OpenAI-compatible). -- `models.default.temperature` / `harness.settings.temperature` sets sampling. -- `harness.settings.base_url` overrides the model endpoint. -- `harness.settings.system_prompt` becomes the Deep Agents `system_prompt`. +- The selected `models` role supplies `model`, `provider`, `api_key_env`, + `base_url`, and `temperature`. +- Top-level `system_prompt` becomes the Deep Agents `system_prompt`. +- `runtime.timeout_seconds` sets the Fabric invocation deadline. - `environment.workspace` roots the Deep Agents filesystem backend (`FilesystemBackend(root_dir=..., virtual_mode=True)`). `virtual_mode` confines the agent to the workspace: absolute paths and `..` cannot escape @@ -72,6 +71,10 @@ NeMo Fabric maps the following into the harness: unsupported key is a normalized configuration failure rather than a silently dropped setting. +The legacy `harness.settings` aliases for model fields, `system_prompt`, +`workspace`, environment variables, and invocation timeout are rejected. Keep +only the Deep Agents-specific `deepagents` controls there. + ### Subagents Deep Agents can delegate to subagents through its built-in `task` tool. Subagents @@ -98,9 +101,8 @@ NeMo Fabric starts one local adapter host for every runtime. During runtime star the host compiles one Deep Agents graph, opens its async LangGraph checkpointer, and creates one thread ID. Every invocation reuses those native objects; later turns report `resumed` as `true`. The checkpointer lives under -`harness.settings.state_dir` (default the runtime artifacts directory) and is -closed during runtime stop. The live host owns the thread identity, and -LangGraph owns the transcript. +the Fabric artifact root, scoped by runtime ID, and is closed during runtime +stop. The live host owns the thread identity, and LangGraph owns the transcript. `Fabric.run(...)` is a convenience over that same lifecycle: it starts the runtime, invokes it once, and stops it. It does not use a separate adapter diff --git a/adapters/hermes/README.md b/adapters/hermes/README.md index 9cc99f68a..d1f315829 100644 --- a/adapters/hermes/README.md +++ b/adapters/hermes/README.md @@ -31,17 +31,24 @@ pip install "nemo-fabric[hermes, hermes-agent]" The adapter receives a normalized payload from NeMo Fabric and materializes a native Hermes Agent configuration for: -- model provider, model name, base URL, temperature, and token settings; -- workspace and terminal settings; +- selected model provider, model name, base URL, and temperature through + `models`; +- top-level `system_prompt` and `max_turns`; +- workspace and explicit environment variables through `environment`; +- invocation timeout through `runtime.timeout_seconds`; - NeMo Fabric skills as external skill directories for Hermes Agent; - NeMo Fabric MCP servers as Hermes Agent MCP server config; -- `tools.blocked` as disabled toolsets for Hermes Agent, unioned with - `harness.settings.disabled_toolsets`; +- `tools.toolsets.enabled` and `tools.toolsets.blocked` as Hermes toolset + selection and blocking policy; - optional NeMo Relay telemetry plugin configuration. -`hermes_home` configures a base directory. The adapter creates a child under -`runtimes/` so invocations in one NeMo Fabric runtime share Hermes Agent state -without sharing config or the session database with another runtime. +Per-tool `tools.blocked` is not accepted by Hermes because Hermes exposes +toolset-level policy at this boundary. Keep Hermes-specific controls such as +terminal timeout, reasoning configuration, and plugin configuration in +`harness.settings`. The adapter derives Hermes state from the Fabric artifact +root and creates a child under `runtimes/`, so invocations in one +NeMo Fabric runtime share state without sharing config or the session database +with another runtime. ## Execution Model diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index b5d0244c4..413426c3f 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -43,17 +43,15 @@ config = FabricConfig( harness=HarnessConfig( adapter_id="nvidia.fabric.hermes", resolution="preinstalled", - settings={ - "base_url": "https://integrate.api.nvidia.com/v1", - "max_iterations": 1, - }, ), + max_turns=1, models={ "default": ModelConfig( provider="nvidia", model="nvidia/nemotron-3-nano-30b-a3b", temperature=0.0, api_key_env="NVIDIA_API_KEY", + base_url="https://integrate.api.nvidia.com/v1", ) }, ) diff --git a/docs/integrations/claude.mdx b/docs/integrations/claude.mdx index deb7bca1d..6da788d48 100644 --- a/docs/integrations/claude.mdx +++ b/docs/integrations/claude.mdx @@ -8,11 +8,10 @@ SPDX-License-Identifier: Apache-2.0 */} The `nvidia.fabric.claude` adapter uses the Claude Agent SDK and its bundled Claude Code runtime. NeMo Fabric preserves Claude's native credential precedence and forwards only supported operating-system, configuration, and authentication -variables plus values explicitly configured in `harness.settings.env`. +variables plus values explicitly configured in `environment.env`. -The adapter pins `claude-agent-sdk==0.2.120`. The SDK owns its compatible Claude -Code runtime unless `harness.settings.cli_path` explicitly selects another -executable. +The adapter pins `claude-agent-sdk==0.2.120`. The SDK owns and selects its +compatible Claude Code runtime. ## Choose an Authentication Mode diff --git a/docs/integrations/codex.mdx b/docs/integrations/codex.mdx index fb2b3f6b0..e679f75af 100644 --- a/docs/integrations/codex.mdx +++ b/docs/integrations/codex.mdx @@ -48,23 +48,12 @@ mechanism that covers built-in, local, MCP, and hosted tools. NeMo Fabric reports normalized blocked-tool policy as unsupported instead of enforcing only part of the requested policy. -## Select a Codex Runtime +## Codex Runtime Ownership The Codex SDK installs and selects its matching app-server runtime. NeMo Fabric does not declare or select the runtime package separately. A `codex` command on `PATH` does not replace the SDK-owned runtime. -To override the SDK-selected app-server intentionally, set an absolute path or -a path relative to the NeMo Fabric config root in `harness.settings.codex_bin`: - -```python -config.harness.settings["codex_bin"] = "/path/to/codex" -``` - -NeMo Fabric passes this path to `CodexConfig.codex_bin`; it does not invoke the -command as a CLI adapter. Pin the override in reproducible environments because -the app-server protocol can change before a matching Python SDK is published. - ## Choose an Authentication Mode Codex supports the following OpenAI authentication modes for local work: @@ -117,7 +106,7 @@ runtime, invokes it once, and stops it, so the gateway is scoped to that single invocation. The SDK still obtains credentials from the selected Codex authentication mode. -Relay-enabled Codex runs require `models.default.provider` to be `openai`. +Relay-enabled Codex runs require the selected model provider to be `openai`. The custom `nvidia` provider uses its configured NVIDIA Responses endpoint and does not support the built-in provider redirect that Relay requires. Use the `nvidia` provider without Relay. diff --git a/docs/reference/api/python-library-reference/index.md b/docs/reference/api/python-library-reference/index.md index b4096061c..8ba32251b 100644 --- a/docs/reference/api/python-library-reference/index.md +++ b/docs/reference/api/python-library-reference/index.md @@ -46,6 +46,7 @@ SPDX-License-Identifier: Apache-2.0 */} - [`models.TelemetryConfig`](./nemo_fabric.models.md#class-telemetryconfig): Telemetry configuration. - [`models.TelemetryProviderConfig`](./nemo_fabric.models.md#class-telemetryproviderconfig): Provider-specific telemetry configuration. - [`models.ToolsConfig`](./nemo_fabric.models.md#class-toolsconfig): Harness-neutral tool capability configuration. +- [`models.ToolsetConfig`](./nemo_fabric.models.md#class-toolsetconfig): Harness-neutral toolset selection and blocking policy. - [`types.AdapterInfo`](./nemo_fabric.types.md#class-adapterinfo): Resolved adapter identity attached to a run plan. - [`types.ArtifactManifest`](./nemo_fabric.types.md#class-artifactmanifest): Normalized collection of artifacts produced by a run. - [`types.ArtifactRef`](./nemo_fabric.types.md#class-artifactref): Reference to one artifact produced by a run. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.client.md b/docs/reference/api/python-library-reference/nemo_fabric.client.md index f8fec00ec..c719a3ead 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.client.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.client.md @@ -1,7 +1,7 @@ --- title: "Client" slug: "/reference/api/python-library-reference/client" -description: "Resolve, plan, diagnose, and run agents with Fabric." +description: "Resolve, plan, diagnose, and run agents with NeMo Fabric." --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index ee2a955fd..b362e7fbd 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -1,7 +1,7 @@ --- title: "Models" slug: "/reference/api/python-library-reference/models" -description: "Pydantic authoring models for Fabric config and request inputs." +description: "Pydantic authoring models for NeMo Fabric config and request inputs." --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} @@ -1421,6 +1421,66 @@ Return a detached JSON-compatible mapping for Rust/core calls. --- +## class `ToolsetConfig` +Harness-neutral toolset selection and blocking policy. + + +--- + +### property extra_fields + +Return fields preserved by the extension point for this model. + +--- + +### property model_extra + +Get extra fields set during validation. + + + +**Returns:** + A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. + +--- + +### property model_fields_set + +Returns the set of fields that have been explicitly set on this model instance. + + + +**Returns:** + A set of strings representing the fields that have been set, i.e. that were not filled from defaults. + + + +--- + + +### classmethod `from_mapping` + +```python +from_mapping(value: 'Mapping[str, Any]') → Self +``` + +Validate a mapping using this Pydantic model. + +--- + + +### method `to_mapping` + +```python +to_mapping() → dict[str, Any] +``` + +Return a detached JSON-compatible mapping for Rust/core calls. + + +--- + + ## class `ToolsConfig` Harness-neutral tool capability configuration. @@ -1485,6 +1545,8 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `FabricConfig` SDK-facing typed Fabric agent configuration. +Fabric-owned fields apply uniformly. Adapter-translated fields are checked against the selected descriptor; see the normalized configuration compatibility table in ``docs/sdk/python.mdx``. + --- @@ -1553,7 +1615,21 @@ Add a skill path and return this config. block_tools(*tools: 'str') → Self ``` -Block adapter-native tool names or toolsets and return this config. +Block adapter-native tool names and return this config. + +--- + + +### method `configure_toolsets` + +```python +configure_toolsets( + enabled: 'Sequence[str] | None' = None, + blocked: 'Sequence[str]' = () +) → Self +``` + +Set adapter-native toolset selection and blocking policy. --- diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx new file mode 100644 index 000000000..7d99df503 --- /dev/null +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx @@ -0,0 +1,234 @@ +--- +title: "Enum Adapter Config Field" +sidebar-title: "AdapterConfigField" +description: "Adapter-translated normalized Fabric configuration fields." +position: 2 +--- +{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 */} + +Generated from `cargo doc --no-deps -p nemo-fabric-core`. + +```rust +pub enum AdapterConfigField { + Models, + ModelBaseUrl, + ModelTemperature, + SystemPrompt, + MaxTurns, + BlockedTools, + EnabledToolsets, + BlockedToolsets, + Mcp, + Skills, + Telemetry, +} +``` + +Adapter-translated normalized Fabric configuration fields. + +## Variants + +### `Models` + +
+ +Normalized model selection and credentials. + +### `ModelBaseUrl` + +
+ +Custom model endpoint. + +### `ModelTemperature` + +
+ +Model temperature. + +### `SystemPrompt` + +
+ +Portable system instructions. + +### `MaxTurns` + +
+ +Per-invocation harness turn limit. + +### `BlockedTools` + +
+ +Adapter-native tool names to block. + +### `EnabledToolsets` + +
+ +Harness-defined toolsets to expose. + +### `BlockedToolsets` + +
+ +Harness-defined toolsets to block. + +### `Mcp` + +
+ +Harness-native MCP servers. + +### `Skills` + +
+ +Harness-native skills. + +### `Telemetry` + +
+ +Typed telemetry configuration. Exact support remains declared by telemetry provider. + +## Trait Implementations + +### `impl Clone for AdapterConfigField` + +
Clone for AdapterConfigField"}} />
+ +#### `clone` + +
clone(&self) -> AdapterConfigField"}} />
+ +#### `clone_from` + +
clone_from(&mut self, source: &Self)"}} />
+ +### `impl Debug for AdapterConfigField` + +
Debug for AdapterConfigField"}} />
+ +#### `fmt` + +
fmt(&self, f: &mut Formatter<'_>) -> Result"}} />
+ +### `impl<'de> Deserialize<'de> for AdapterConfigField` + +
Deserialize<'de> for AdapterConfigField"}} />
+ +#### `deserialize` + +
deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where\n    __D: Deserializer<'de>,"}} />
+ +### `impl Hash for AdapterConfigField` + +
Hash for AdapterConfigField"}} />
+ +#### `hash` + +
hash<__H: Hasher>(&self, state: &mut __H)"}} />
+ +#### `hash_slice` + +
hash_slice<H>(data: &[Self], state: &mut H)where\n    H: Hasher,\n    Self: Sized,"}} />
+ +### `impl JsonSchema for AdapterConfigField` + +
AdapterConfigField"}} />
+ +#### `schema_name` + +
Cow<'static, str>"}} />
+ +#### `schema_id` + +
Cow<'static, str>"}} />
+ +#### `json_schema` + +
+ +#### `inline_schema` + +
bool"}} />
+ +### `impl Ord for AdapterConfigField` + +
Ord for AdapterConfigField"}} />
+ +#### `cmp` + +
cmp(&self, other: &AdapterConfigField) -> Ordering"}} />
+ +#### `max` + +
max(self, other: Self) -> Selfwhere\n    Self: Sized,"}} />
+ +#### `min` + +
min(self, other: Self) -> Selfwhere\n    Self: Sized,"}} />
+ +#### `clamp` + +
clamp(self, min: Self, max: Self) -> Selfwhere\n    Self: Sized,"}} />
+ +### `impl PartialEq for AdapterConfigField` + +
PartialEq for AdapterConfigField"}} />
+ +#### `eq` + +
eq(&self, other: &AdapterConfigField) -> bool"}} />
+ +#### `ne` + +
ne(&self, other: &Rhs) -> bool"}} />
+ +### `impl PartialOrd for AdapterConfigField` + +
PartialOrd for AdapterConfigField"}} />
+ +#### `partial_cmp` + +
partial_cmp(&self, other: &AdapterConfigField) -> Option<Ordering>"}} />
+ +#### `lt` + +
lt(&self, other: &Rhs) -> bool"}} />
+ +#### `le` + +
le(&self, other: &Rhs) -> bool"}} />
+ +#### `gt` + +
gt(&self, other: &Rhs) -> bool"}} />
+ +#### `ge` + +
ge(&self, other: &Rhs) -> bool"}} />
+ +### `impl Serialize for AdapterConfigField` + +
Serialize for AdapterConfigField"}} />
+ +#### `serialize` + +
serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>where\n    __S: Serializer,"}} />
+ +### `impl Copy for AdapterConfigField` + +
Copy for AdapterConfigField"}} />
+ +### `impl Eq for AdapterConfigField` + +
Eq for AdapterConfigField"}} />
+ +### `impl StructuralPartialEq for AdapterConfigField` + +
StructuralPartialEq for AdapterConfigField"}} />
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource.mdx index 57ada140b..3d35cda8f 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource.mdx @@ -2,7 +2,7 @@ title: "Enum Adapter Descriptor Source" sidebar-title: "AdapterDescriptorSource" description: "Where Fabric resolved an adapter descriptor from." -position: 4 +position: 5 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind.mdx index bdb4d02bc..402d09fe1 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind.mdx @@ -2,7 +2,7 @@ title: "Enum Adapter Kind" sidebar-title: "AdapterKind" description: "Adapter implementation kind." -position: 5 +position: 6 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx index b22fc853a..5b133e34d 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx @@ -2,7 +2,7 @@ title: "Enum Capability Kind" sidebar-title: "CapabilityKind" description: "Capability kind." -position: 38 +position: 40 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx index cfd744fbe..d51db854e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx @@ -2,7 +2,7 @@ title: "Enum Capability Target" sidebar-title: "CapabilityTarget" description: "Capability routing target." -position: 39 +position: 41 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-controllocation.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-controllocation.mdx index 84d27e315..b44503fea 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-controllocation.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-controllocation.mdx @@ -2,7 +2,7 @@ title: "Enum Control Location" sidebar-title: "ControlLocation" description: "Where Fabric control code runs relative to the environment." -position: 10 +position: 11 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-environmentownership.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-environmentownership.mdx index 962a0afdc..56f121751 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-environmentownership.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-environmentownership.mdx @@ -2,7 +2,7 @@ title: "Enum Environment Ownership" sidebar-title: "EnvironmentOwnership" description: "Whether Fabric owns the underlying environment resource." -position: 12 +position: 13 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx index b06236033..a75f60441 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx @@ -2,7 +2,7 @@ title: "Enum McpExposure" sidebar-title: "McpExposure" description: "MCP exposure strategy." -position: 17 +position: 18 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx index aed4582ca..59e753c33 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atif Storage Config" sidebar-title: "RelayAtifStorageConfig" description: "Relay ATIF remote storage configuration." -position: 43 +position: 45 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx index 6c6d8a7e9..348bca798 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Mode" sidebar-title: "RelayAtofMode" description: "Relay ATOF file mode." -position: 44 +position: 46 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx index 5af1bf71f..ad018342c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Sink Config" sidebar-title: "RelayAtofSinkConfig" description: "Relay ATOF sink configuration." -position: 45 +position: 47 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx index 9beca42d2..b5110d279 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Stream Field Name Policy" sidebar-title: "RelayAtofStreamFieldNamePolicy" description: "Relay ATOF stream field-name policy." -position: 46 +position: 48 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx index 4bf15bdef..b8784849e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Stream Transport" sidebar-title: "RelayAtofStreamTransport" description: "Relay ATOF stream transport." -position: 47 +position: 49 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx index fae226e2b..1f5857009 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Otlp Transport" sidebar-title: "RelayOtlpTransport" description: "Relay OTLP transport." -position: 48 +position: 50 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx index 34db2ad99..e8d549f6e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Unsupported Behavior" sidebar-title: "RelayUnsupportedBehavior" description: "Relay unsupported/unknown config handling." -position: 49 +position: 51 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx index 86197cf30..a85905a89 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx @@ -2,7 +2,7 @@ title: "Enum Resolution Strategy" sidebar-title: "ResolutionStrategy" description: "Adapter install or availability strategy." -position: 21 +position: 22 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx index 9cab3af61..9c578453a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx @@ -2,7 +2,7 @@ title: "Enum Telemetry Provider" sidebar-title: "TelemetryProvider" description: "Telemetry runtime provider." -position: 30 +position: 31 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx index 93dff2ba9..058809419 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx @@ -2,7 +2,7 @@ title: "Function load_adapter_descriptor" sidebar-title: "load_adapter_descriptor" description: "Load an adapter descriptor from JSON package metadata." -position: 32 +position: 35 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx index 830eb41f2..14a4e4844 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx @@ -2,7 +2,7 @@ title: "Function resolve_run_plan_from_config" sidebar-title: "resolve_run_plan_from_config" description: "Resolve a typed Fabric config into a runnable plan." -position: 33 +position: 36 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx index d82bf8c58..5fea806f4 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx @@ -2,7 +2,7 @@ title: "Module config" sidebar-title: "config" description: "Fabric config models and loading helpers." -position: 65 +position: 68 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} @@ -48,9 +48,11 @@ Fabric config models and loading helpers. - [TelemetryProviderConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig): Provider-specific telemetry configuration. - [ToolsConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig): Harness-neutral tool capability configuration. - [ToolsPlan](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan): Normalized tool policy for a run. +- [ToolsetConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig): Harness-neutral toolset capability configuration. ## Enums +- [AdapterConfigField](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield): Adapter-translated normalized Fabric configuration fields. - [AdapterDescriptorSource](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource): Where Fabric resolved an adapter descriptor from. - [AdapterKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind): Adapter implementation kind. - [CapabilityKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind): Capability kind. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx index 4e4a85f05..0cf693fa0 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx @@ -2,22 +2,22 @@ title: "Struct Adapter Config Support" sidebar-title: "AdapterConfigSupport" description: "Adapter config support." -position: 2 +position: 3 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Vec<String>,\n    pub generates: Vec<PathBuf>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
Vec<AdapterConfigField>,\n    pub generates: Vec<PathBuf>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Adapter config support. ## Fields -### `accepts: Vec` +### `accepts: Vec` -Fabric config areas or policy paths accepted by this adapter. +Normalized Fabric config fields accepted by this adapter. ### `generates: Vec` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterdescriptor.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterdescriptor.mdx index 2d1a3357d..52ea13c12 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterdescriptor.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterdescriptor.mdx @@ -2,7 +2,7 @@ title: "Struct Adapter Descriptor" sidebar-title: "AdapterDescriptor" description: "Language-neutral adapter descriptor for a harness integration." -position: 3 +position: 4 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterrequirements.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterrequirements.mdx index 128e05df3..a462ccfc7 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterrequirements.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterrequirements.mdx @@ -2,7 +2,7 @@ title: "Struct Adapter Requirements" sidebar-title: "AdapterRequirements" description: "Adapter runtime requirements." -position: 6 +position: 7 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetryprovidersupport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetryprovidersupport.mdx index 329db488c..e1b9ad7a9 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetryprovidersupport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetryprovidersupport.mdx @@ -2,7 +2,7 @@ title: "Struct Adapter Telemetry Provider Support" sidebar-title: "AdapterTelemetryProviderSupport" description: "Telemetry capabilities for one adapter-supported provider." -position: 7 +position: 8 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetrysupport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetrysupport.mdx index 455d860af..6e3baa017 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetrysupport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adaptertelemetrysupport.mdx @@ -2,7 +2,7 @@ title: "Struct Adapter Telemetry Support" sidebar-title: "AdapterTelemetrySupport" description: "Adapter telemetry support." -position: 8 +position: 9 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityplan.mdx index 8e98b7c73..50f1ae5d0 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityplan.mdx @@ -2,7 +2,7 @@ title: "Struct Capability Plan" sidebar-title: "CapabilityPlan" description: "Resolved capability configuration." -position: 9 +position: 10 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx index 97e9e0d19..aaf518089 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx @@ -2,14 +2,14 @@ title: "Struct Environment Config" sidebar-title: "EnvironmentConfig" description: "Execution environment configuration." -position: 11 +position: 12 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub control_location: ControlLocation,\n    pub ownership: EnvironmentOwnership,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub connection: Map<String, Value>,\n    pub metadata: Map<String, Value>,\n    pub settings: Map<String, Value>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub control_location: ControlLocation,\n    pub ownership: EnvironmentOwnership,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub env: BTreeMap<String, String>,\n    pub connection: Map<String, Value>,\n    pub metadata: Map<String, Value>,\n    pub settings: Map<String, Value>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Execution environment configuration. @@ -35,6 +35,10 @@ Workspace path inside or outside the provider. Artifact path inside or outside the provider. +### `env: BTreeMap` + +Environment variables visible to the harness and its tools. + ### `connection: Map` Provider connection metadata, such as server URL, credential reference, or namespace. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan.mdx index f0f664bc7..2e9e5d8bb 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan.mdx @@ -2,14 +2,14 @@ title: "Struct Environment Plan" sidebar-title: "EnvironmentPlan" description: "Resolved environment plan." -position: 13 +position: 14 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub control_location: ControlLocation,\n    pub ownership: EnvironmentOwnership,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub connection: Map<String, Value>,\n    pub metadata: Map<String, Value>,\n    pub settings: Map<String, Value>,\n}"}} />
+
String,\n    pub control_location: ControlLocation,\n    pub ownership: EnvironmentOwnership,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub env: BTreeMap<String, String>,\n    pub connection: Map<String, Value>,\n    pub metadata: Map<String, Value>,\n    pub settings: Map<String, Value>,\n}"}} />
Resolved environment plan. @@ -35,6 +35,10 @@ Resolved workspace path. Resolved artifact path. +### `env: BTreeMap` + +Environment variables visible to the harness and its tools. + ### `connection: Map` Provider connection metadata. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx index dca40ab9c..1347673f1 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx @@ -2,17 +2,19 @@ title: "Struct Fabric Config" sidebar-title: "FabricConfig" description: "Versioned Fabric agent config." -position: 14 +position: 15 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub metadata: MetadataConfig,\n    pub harness: HarnessConfig,\n    pub models: BTreeMap<String, ModelConfig>,\n    pub runtime: RuntimeConfig,\n    pub environment: Option<EnvironmentConfig>,\n    pub tools: Option<ToolsConfig>,\n    pub skills: Option<SkillConfig>,\n    pub mcp: Option<McpConfig>,\n    pub telemetry: Option<TelemetryConfig>,\n    pub relay: Option<RelayConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub metadata: MetadataConfig,\n    pub harness: HarnessConfig,\n    pub models: BTreeMap<String, ModelConfig>,\n    pub system_prompt: Option<String>,\n    pub max_turns: Option<u32>,\n    pub runtime: RuntimeConfig,\n    pub environment: Option<EnvironmentConfig>,\n    pub tools: Option<ToolsConfig>,\n    pub skills: Option<SkillConfig>,\n    pub mcp: Option<McpConfig>,\n    pub telemetry: Option<TelemetryConfig>,\n    pub relay: Option<RelayConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Versioned Fabric agent config. +Fabric-owned fields apply uniformly, while adapter-translated fields are validated against the selected adapter descriptor. See the configuration compatibility matrix. + ## Fields ### `schema_version: String` @@ -31,6 +33,14 @@ Harness selection and harness-specific settings. Model aliases. +### `system_prompt: Option` + +Portable system instructions for the selected harness. + +### `max_turns: Option` + +Maximum number of harness turns within one invocation. + ### `runtime: RuntimeConfig` Runtime input/output contract. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig.mdx index ce73c2a14..fd2177594 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Harness Config" sidebar-title: "HarnessConfig" description: "Harness selection." -position: 15 +position: 16 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx index 12e5c64c5..9c6b81189 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx @@ -2,7 +2,7 @@ title: "Struct McpConfig" sidebar-title: "McpConfig" description: "MCP capability configuration." -position: 16 +position: 17 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx index 6902533b3..8ab22f57e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx @@ -2,7 +2,7 @@ title: "Struct McpServer Plan" sidebar-title: "McpServerPlan" description: "Resolved MCP server exposure." -position: 18 +position: 19 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx index c9bfdec0d..f83f18a1c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Metadata Config" sidebar-title: "MetadataConfig" description: "Human-readable metadata." -position: 19 +position: 20 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx index 92033d79b..1b1389f14 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx @@ -2,14 +2,14 @@ title: "Struct Model Config" sidebar-title: "ModelConfig" description: "Model configuration." -position: 20 +position: 21 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub model: String,\n    pub temperature: Option<f64>,\n    pub api_key_env: Option<String>,\n    pub settings: Map<String, Value>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub model: String,\n    pub temperature: Option<f64>,\n    pub api_key_env: Option<String>,\n    pub base_url: Option<String>,\n    pub settings: Map<String, Value>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Model configuration. @@ -31,6 +31,10 @@ Optional temperature. Optional environment variable containing an API key. +### `base_url: Option` + +Optional provider endpoint URL. + ### `settings: Map` Provider-specific settings. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx index 1fd20085e..60f1e8808 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx @@ -2,7 +2,7 @@ title: "Struct Resolve Context" sidebar-title: "ResolveContext" description: "Source context used when resolving an in-memory Fabric config." -position: 22 +position: 23 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx index c1891a955..97724b25a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx @@ -2,7 +2,7 @@ title: "Struct Resolved Adapter Descriptor" sidebar-title: "ResolvedAdapterDescriptor" description: "Adapter descriptor selected for a run plan." -position: 23 +position: 24 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx index f042afb24..bf685ec07 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx @@ -2,7 +2,7 @@ title: "Struct RunPlan" sidebar-title: "RunPlan" description: "Resolved Fabric run plan." -position: 24 +position: 25 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx index 7ef3c1696..639ef5796 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx @@ -2,7 +2,7 @@ title: "Struct Runtime Capabilities" sidebar-title: "RuntimeCapabilities" description: "Lifecycle behavior implemented by a resolved runtime path." -position: 25 +position: 26 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx index 86641ac9b..6eb91b370 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx @@ -2,14 +2,14 @@ title: "Struct Runtime Config" sidebar-title: "RuntimeConfig" description: "Runtime input/output contract." -position: 26 +position: 27 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub output_schema: String,\n    pub artifacts: Option<PathBuf>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub output_schema: String,\n    pub artifacts: Option<PathBuf>,\n    pub timeout_seconds: Option<f64>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Runtime input/output contract. @@ -27,6 +27,10 @@ Output schema label. Artifact directory. +### `timeout_seconds: Option` + +Maximum duration of one invocation in seconds. + ### `extensions: BTreeMap` Additive normalized runtime fields. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx index 9572bbf71..1b3c1e1fe 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Skill Config" sidebar-title: "SkillConfig" description: "Skill capability configuration." -position: 27 +position: 28 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx index 5dd271a6f..c35e2db57 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Config" sidebar-title: "TelemetryConfig" description: "Telemetry configuration." -position: 28 +position: 29 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx index 04c79e8c9..767112630 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Plan" sidebar-title: "TelemetryPlan" description: "Resolved telemetry plan." -position: 29 +position: 30 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx index 21e9291ba..afa21e960 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Provider Config" sidebar-title: "TelemetryProviderConfig" description: "Provider-specific telemetry configuration." -position: 31 +position: 32 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx index 94985e2cc..0c5e1679c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx @@ -2,14 +2,14 @@ title: "Struct Tools Config" sidebar-title: "ToolsConfig" description: "Harness-neutral tool capability configuration." -position: 34 +position: 33 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Vec<String>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
Vec<String>,\n    pub toolsets: Option<ToolsetConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Harness-neutral tool capability configuration. @@ -17,7 +17,11 @@ Harness-neutral tool capability configuration. ### `blocked: Vec` -Adapter-native tool names or toolset names to block. +Adapter-native tool names to block. + +### `toolsets: Option` + +Adapter-native toolset selection and blocking policy. ### `extensions: BTreeMap` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx new file mode 100644 index 000000000..8fa31e77b --- /dev/null +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx @@ -0,0 +1,110 @@ +--- +title: "Struct Toolset Config" +sidebar-title: "ToolsetConfig" +description: "Harness-neutral toolset capability configuration." +position: 34 +--- +{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 */} + +Generated from `cargo doc --no-deps -p nemo-fabric-core`. + +
Option<Vec<String>>,\n    pub blocked: Vec<String>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+ +Harness-neutral toolset capability configuration. + +## Fields + +### `enabled: Option>` + +Toolsets to expose. `None` preserves the adapter default; an empty list exposes none. + +### `blocked: Vec` + +Toolsets to block. + +### `extensions: BTreeMap` + +Additive toolset configuration fields. + +## Trait Implementations + +### `impl Clone for ToolsetConfig` + +
Clone for ToolsetConfig"}} />
+ +#### `clone` + +
clone(&self) -> ToolsetConfig"}} />
+ +#### `clone_from` + +
clone_from(&mut self, source: &Self)"}} />
+ +### `impl Debug for ToolsetConfig` + +
Debug for ToolsetConfig"}} />
+ +#### `fmt` + +
fmt(&self, f: &mut Formatter<'_>) -> Result"}} />
+ +### `impl Default for ToolsetConfig` + +
Default for ToolsetConfig"}} />
+ +#### `default` + +
default() -> ToolsetConfig"}} />
+ +### `impl<'de> Deserialize<'de> for ToolsetConfig` + +
Deserialize<'de> for ToolsetConfig"}} />
+ +#### `deserialize` + +
deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where\n    __D: Deserializer<'de>,"}} />
+ +### `impl JsonSchema for ToolsetConfig` + +
ToolsetConfig"}} />
+ +#### `schema_name` + +
Cow<'static, str>"}} />
+ +#### `schema_id` + +
Cow<'static, str>"}} />
+ +#### `json_schema` + +
+ +#### `inline_schema` + +
bool"}} />
+ +### `impl PartialEq for ToolsetConfig` + +
PartialEq for ToolsetConfig"}} />
+ +#### `eq` + +
eq(&self, other: &ToolsetConfig) -> bool"}} />
+ +#### `ne` + +
ne(&self, other: &Rhs) -> bool"}} />
+ +### `impl Serialize for ToolsetConfig` + +
Serialize for ToolsetConfig"}} />
+ +#### `serialize` + +
serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>where\n    __S: Serializer,"}} />
+ +### `impl StructuralPartialEq for ToolsetConfig` + +
StructuralPartialEq for ToolsetConfig"}} />
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx index 204811185..7935c90af 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Vec<String>,\n}"}} />
+
Vec<String>,\n    pub toolsets: Option<ToolsetConfig>,\n}"}} />
Normalized tool policy for a run. @@ -17,7 +17,11 @@ Normalized tool policy for a run. ### `blocked: Vec` -Adapter-native tool names or toolset names to block. +Adapter-native tool names to block. + +### `toolsets: Option` + +Adapter-native toolset selection and blocking policy. ## Trait Implementations diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx index 0d424ce77..50a55eccd 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx @@ -2,7 +2,7 @@ title: "Enum Doctor Status" sidebar-title: "DoctorStatus" description: "Diagnostic status." -position: 36 +position: 39 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx index c25887030..8ddbe6d24 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx @@ -2,7 +2,7 @@ title: "Function doctor_plan" sidebar-title: "doctor_plan" description: "Inspect a resolved run plan without mutating the environment." -position: 37 +position: 40 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx index 5dff6b0b1..b33235362 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx @@ -2,7 +2,7 @@ title: "Module doctor" sidebar-title: "doctor" description: "Plan diagnostics for Fabric." -position: 66 +position: 69 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx index 75af40335..d3d3d0190 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx @@ -2,7 +2,7 @@ title: "Struct Doctor Check" sidebar-title: "DoctorCheck" description: "Diagnostic check result." -position: 34 +position: 37 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx index e7b635eb3..5f71661de 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx @@ -2,7 +2,7 @@ title: "Struct Doctor Report" sidebar-title: "DoctorReport" description: "Diagnostic report for a resolved run plan." -position: 35 +position: 38 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx index e0854e2fd..0068df5d8 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx @@ -2,14 +2,14 @@ title: "Enum Fabric Error" sidebar-title: "FabricError" description: "Errors raised by Fabric config loading and validation." -position: 38 +position: 41 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
PathBuf,\n        source: Error,\n    },\n    PathNotFound(PathBuf),\n    UnknownAdapter {\n        adapter_id: String,\n        available: Vec<String>,\n    },\n    AdapterDescriptorMismatch {\n        path: PathBuf,\n        field: &'static str,\n        expected: String,\n        actual: String,\n    },\n    AdapterDescriptorUnsupported {\n        adapter_id: String,\n        field: &'static str,\n        value: String,\n    },\n    InvalidAdapterDescriptor {\n        path: PathBuf,\n        message: String,\n    },\n    UnknownSchema {\n        schema: String,\n        available: Vec<String>,\n    },\n    UnsupportedRuntimeAdapter {\n        harness: String,\n        adapter_kind: AdapterKind,\n    },\n    AdapterLifecycleOperation {\n        operation: &'static str,\n        runtime_id: String,\n        code: String,\n        message: String,\n        diagnostics: String,\n    },\n    UnsupportedToolsPolicy {\n        harness: String,\n        reason: String,\n    },\n    RuntimeHandleMismatch {\n        field: &'static str,\n        expected: String,\n        actual: String,\n        runtime_id: String,\n    },\n    UnsupportedEnvironmentProvider {\n        provider: String,\n        adapter_kind: AdapterKind,\n    },\n    InvalidProcessSettings {\n        path: PathBuf,\n        source: Error,\n    },\n    InvalidPythonSettings {\n        path: PathBuf,\n        source: Error,\n    },\n    PythonInterpreterUnavailable {\n        path: PathBuf,\n        origin: String,\n        reason: String,\n    },\n    ProcessRunner {\n        command: String,\n        source: Error,\n    },\n    SerializeJson(Error),\n    Read {\n        path: PathBuf,\n        source: Error,\n    },\n    Write {\n        path: PathBuf,\n        source: Error,\n    },\n    ParseJson {\n        path: PathBuf,\n        source: Error,\n    },\n}"}} />
+
PathBuf,\n        source: Error,\n    },\n    PathNotFound(PathBuf),\n    UnknownAdapter {\n        adapter_id: String,\n        available: Vec<String>,\n    },\n    AdapterDescriptorMismatch {\n        path: PathBuf,\n        field: &'static str,\n        expected: String,\n        actual: String,\n    },\n    AdapterDescriptorUnsupported {\n        adapter_id: String,\n        field: &'static str,\n        value: String,\n    },\n    InvalidAdapterDescriptor {\n        path: PathBuf,\n        message: String,\n    },\n    InvalidConfig {\n        field: String,\n        reason: String,\n    },\n    AdapterCompatibility {\n        adapter_id: String,\n        field: String,\n        reason: String,\n    },\n    UnknownSchema {\n        schema: String,\n        available: Vec<String>,\n    },\n    UnsupportedRuntimeAdapter {\n        harness: String,\n        adapter_kind: AdapterKind,\n    },\n    AdapterLifecycleOperation {\n        operation: &'static str,\n        runtime_id: String,\n        code: String,\n        message: String,\n        diagnostics: String,\n    },\n    RuntimeHandleMismatch {\n        field: &'static str,\n        expected: String,\n        actual: String,\n        runtime_id: String,\n    },\n    UnsupportedEnvironmentProvider {\n        provider: String,\n        adapter_kind: AdapterKind,\n    },\n    InvalidProcessSettings {\n        path: PathBuf,\n        source: Error,\n    },\n    InvalidPythonSettings {\n        path: PathBuf,\n        source: Error,\n    },\n    PythonInterpreterUnavailable {\n        path: PathBuf,\n        origin: String,\n        reason: String,\n    },\n    ProcessRunner {\n        command: String,\n        source: Error,\n    },\n    SerializeJson(Error),\n    Read {\n        path: PathBuf,\n        source: Error,\n    },\n    Write {\n        path: PathBuf,\n        source: Error,\n    },\n    ParseJson {\n        path: PathBuf,\n        source: Error,\n    },\n}"}} />
Errors raised by Fabric config loading and validation. @@ -113,6 +113,42 @@ Adapter descriptor path. Validation message. +### `InvalidConfig` + +
+ +A normalized Fabric config field is invalid. + +#### Fields + +### `field: String` + +Canonical configuration path. + +### `reason: String` + +Validation failure. + +### `AdapterCompatibility` + +
+ +A valid normalized field cannot be implemented by the selected adapter. + +#### Fields + +### `adapter_id: String` + +Selected adapter id. + +### `field: String` + +Canonical configuration path. + +### `reason: String` + +Compatibility failure. + ### `UnknownSchema`
@@ -173,22 +209,6 @@ Human-readable failure message. Bounded adapter-host diagnostics. -### `UnsupportedToolsPolicy` - -
- -The selected harness cannot enforce the configured blocked-tools policy. - -#### Fields - -### `harness: String` - -Harness type. - -### `reason: String` - -Capability-routing explanation. - ### `RuntimeHandleMismatch`
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx index bc0a4a451..d8ddd4843 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx @@ -2,7 +2,7 @@ title: "Module error" sidebar-title: "error" description: "Error types for Fabric core." -position: 67 +position: 70 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx index 011b5b223..618862e5d 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx @@ -2,7 +2,7 @@ title: "Type Alias Result" sidebar-title: "Result" description: "Core Fabric result type." -position: 39 +position: 42 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx index b9f5b49fb..a3206a15a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx @@ -2,7 +2,7 @@ title: "Function version" sidebar-title: "version" description: "Returns the crate version compiled into this build." -position: 70 +position: 73 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx index 0f9030b11..6c12b8ae9 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx @@ -14,6 +14,7 @@ Core config and runtime contract for NeMo Fabric. ## Re-exports - `pub use config::ADAPTER_CONTRACT_VERSION;` +- `pub use config::AdapterConfigField;` - `pub use config::AdapterConfigSupport;` - `pub use config::AdapterDescriptor;` - `pub use config::AdapterDescriptorSource;` @@ -44,6 +45,8 @@ Core config and runtime contract for NeMo Fabric. - `pub use config::TelemetryPlan;` - `pub use config::TelemetryProvider;` - `pub use config::TelemetryProviderConfig;` +- `pub use config::ToolsConfig;` +- `pub use config::ToolsetConfig;` - `pub use config::load_adapter_descriptor;` - `pub use config::resolve_run_plan_from_config;` - `pub use doctor::DoctorCheck;` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx index aa347d621..e79978009 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx @@ -2,7 +2,7 @@ title: "Module runtime" sidebar-title: "runtime" description: "Runtime invocation helpers." -position: 68 +position: 71 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-environmenthandle.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-environmenthandle.mdx index 18dffa0be..c7ac84783 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-environmenthandle.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/struct-environmenthandle.mdx @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub provider: String,\n    pub control_location: ControlLocation,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub ownership: EnvironmentOwnership,\n    pub connection: BTreeMap<String, Value>,\n    pub metadata: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub provider: String,\n    pub control_location: ControlLocation,\n    pub workspace: Option<PathBuf>,\n    pub artifacts: Option<PathBuf>,\n    pub env: BTreeMap<String, String>,\n    pub ownership: EnvironmentOwnership,\n    pub connection: BTreeMap<String, Value>,\n    pub metadata: BTreeMap<String, Value>,\n}"}} />
Resolved execution environment context. @@ -35,6 +35,10 @@ Workspace visible to the harness runtime. Artifact root visible to the harness runtime. +### `env: BTreeMap` + +Environment variables visible to the harness and its tools. + ### `ownership: EnvironmentOwnership` Whether Fabric owns the environment resource. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx index a3475f92e..42b126261 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx @@ -2,7 +2,7 @@ title: "Module schema" sidebar-title: "schema" description: "JSON Schema generation for the public Fabric contract." -position: 69 +position: 72 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index a8cc4f7e8..4c05ffd40 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -138,16 +138,18 @@ use the descriptor contract values. | Adapter ID | Models | Tools / Blocked Tools | MCP | Skills | Subagents | Telemetry | Persistent Local Host | Remote Service | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `nvidia.fabric.claude` | Anthropic provider and Claude models | `allowed_tools` adapter setting / normalized `tools.blocked` | Normalized | Normalized | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway | Yes: connected `ClaudeSDKClient`, session, and optional Relay gateway | Not implemented | -| `nvidia.fabric.codex` | Built-in OpenAI and custom NVIDIA Responses providers with Codex-compatible models | Not normalized | Normalized: stdio, HTTP, and streamable HTTP | Normalized: `SKILL.md` directories | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway; native `otel` | Yes: `AsyncCodex` app-server client, thread, and optional Relay gateway | Not implemented | -| `nvidia.fabric.langchain.deepagents` | NVIDIA, OpenAI, OpenAI-compatible, and other LangChain providers | Built-ins and MCP / normalized middleware block list | Normalized | Normalized | Constrained: declarative local subagents inherit parent capabilities | Relay SDK: `atif`, `otel`, and `openinference`; native `otel` and `openinference` | Yes: compiled graph and async LangGraph checkpointer | Not implemented | -| `nvidia.fabric.hermes` | Normalized provider, model, and base URL | Toolsets / normalized disabled toolsets | Normalized | Normalized | Not exposed | Relay plugin: `atif`, `otel`, and `openinference` | Yes: `AIAgent`, `SessionDB`, and Relay plugin context | Not implemented | +| `nvidia.fabric.claude` | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | Per-tool blocking through Claude `disallowed_tools`; toolsets unsupported | Normalized | Normalized | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway | Yes: connected `ClaudeSDKClient`, session, and optional Relay gateway | Not implemented | +| `nvidia.fabric.codex` | OpenAI and NVIDIA-hosted Responses-compatible models | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized: `SKILL.md` directories | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway; native `otel` | Yes: `AsyncCodex` app-server client, thread, and optional Relay gateway | Not implemented | +| `nvidia.fabric.langchain.deepagents` | NVIDIA, OpenAI, OpenAI-compatible, and other LangChain providers | Per-tool blocking across built-ins, MCP, and local subagents; toolsets unsupported | Normalized | Normalized | Constrained: declarative local subagents inherit parent capabilities | Relay SDK: `atif`, `otel`, and `openinference`; native `otel` and `openinference` | Yes: compiled graph and async LangGraph checkpointer | Not implemented | +| `nvidia.fabric.hermes` | Configurable provider, model, and base URL | Hermes toolset selection and blocking; per-tool blocking unsupported | Normalized | Normalized | Not exposed | Relay plugin: `atif`, `otel`, and `openinference` | Yes: `AIAgent`, `SessionDB`, and Relay plugin context | Not implemented | "Normalized" means the adapter accepts the corresponding `FabricConfig` field and maps it to the harness. "Not normalized" does not mean that the underlying harness lacks the feature; it means that NeMo Fabric does not expose a portable configuration surface for it. NeMo Fabric currently normalizes -`tools.blocked`, not a portable tool definition catalog. Deep Agents supports +`tools.blocked`, not a portable tool definition catalog. `tools.toolsets` +selects harness-defined bundles and is intentionally distinct from individual +tool names. Deep Agents supports only JSON-shaped, in-process subagent definitions through `harness.settings.deepagents.subagents`; independently configured or remote subagent capabilities are not exposed. @@ -167,6 +169,57 @@ retries, worker scaling, and the number of runtimes to run. ## Configure Agents In Code +### Normalized Configuration Compatibility + +Fabric-owned fields such as workspace, environment variables, and the +invocation deadline apply to every local adapter. Adapter-translated fields are +validated against the selected adapter descriptor during planning. + +`Core` means Fabric owns the behavior and applies it uniformly before or around +adapter execution. `Yes` means the adapter translates the normalized field into +its harness. `No` means an explicitly configured value fails planning instead +of being ignored. Provider-specific Relay subfields and additive extension maps +are grouped because their support does not vary by adapter. + +| `FabricConfig` field | Claude | Codex | Deep Agents | Hermes Agent | +| --- | --- | --- | --- | --- | +| `schema_version` | Core | Core | Core | Core | +| `metadata.name`, `.description` | Core | Core | Core | Core | +| `harness.adapter_id`, `.resolution` | Core | Core | Core | Core | +| `harness.settings` | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | +| `models..provider` | `anthropic`, `nvidia` | `openai`, `nvidia` | Dynamic LangChain provider | Dynamic Hermes provider | +| `models..model` | Yes | Yes | Yes | Yes | +| `models..api_key_env` | Yes | Yes | Yes | Yes | +| `models..base_url` | Yes | Yes | Yes | Yes | +| `models..temperature` | No | No | Yes | Yes | +| `models..settings.` | No keys declared | No keys declared | No keys declared | No keys declared | +| `system_prompt` | Yes | Yes; maps to Codex base instructions | Yes | Yes | +| `max_turns` | Yes | No | No | Yes; maps to Hermes iterations | +| `runtime.input_schema`, `.output_schema` | Core | Core | Core | Core | +| `runtime.artifacts`, `.timeout_seconds` | Core | Core | Core | Core | +| `environment.provider`, `.control_location`, `.ownership` | Core | Core | Core | Core | +| `environment.workspace`, `.artifacts`, `.env` | Core | Core | Core | Core | +| `environment.connection`, `.metadata`, `.settings` | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | +| `tools.blocked` | Yes | No | Yes | No | +| `tools.toolsets.enabled`, `.blocked` | No | No | No | Yes | +| `skills.paths` | Yes | Yes | Yes | Yes | +| `mcp.servers..transport`, `.url` with `harness_native` exposure | Yes | Yes | Yes | Yes | +| `mcp.servers..exposure = "fabric_managed"` | No; not implemented | No; not implemented | No; not implemented | No; not implemented | +| `telemetry.providers.relay` | Yes | Yes | Yes | Yes | +| `telemetry.providers.native` | No | Yes; OpenTelemetry | Yes; OpenTelemetry and OpenInference | No | +| `telemetry.providers..config` | Declared-provider pass-through | Declared-provider pass-through | Declared-provider pass-through | Declared-provider pass-through | +| `relay.project`, `.output_dir`, `.observability` | Yes | Yes | Yes | Yes | +| `relay.components`, `.policy` | Yes | Yes | Yes | Yes | +| Additive `extensions` on typed config objects | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | Preserved; no portable adapter semantics | + +Model selection is deterministic: the `default` role wins; otherwise a single +named role is selected. More than one role without `default` fails planning. + +If a normalized field has no complete mapping, `plan(...)`, `doctor(...)`, and +runtime start fail with a configuration compatibility error naming the adapter +and field. NeMo Fabric does not silently drop the field or reinterpret a +per-tool name as a toolset. + Build the complete nested `FabricConfig` directly, or start with a base config and use helpers to add capabilities. For example, extend the config from the first example with skills, MCP, and telemetry: @@ -184,6 +237,12 @@ capability_config.enable_relay( project="fabric-review", output_dir="./artifacts/relay", ) +capability_config.block_tools("Bash") +# Toolsets are harness-defined bundles, not individual tool names. +capability_config.configure_toolsets( + enabled=["web", "terminal"], + blocked=["browser"], +) ``` Config helpers edit the typed config before planning or starting a runtime. They @@ -458,19 +517,23 @@ Runtime compatibility checks should validate: ## Custom Fields And Adapter Settings -Use normalized NeMo Fabric fields for portable behavior: models, runtime, -environment, skills, MCP, telemetry, tools, artifacts, and request context. +Use normalized NeMo Fabric fields for portable behavior: models, system +instructions, turn limit, runtime, environment, skills, MCP, telemetry, tools, +toolsets, artifacts, and request context. -Use `harness.settings` for adapter-owned configuration that the selected adapter -understands. Examples include Hermes Agent-specific launch options, Codex controls, -or adapter-specific launch paths. +Use `harness.settings` only for stable, harness-native behavior that the selected +adapter understands, such as Claude permission policy, Codex sandbox controls, +or Deep Agents subagent definitions. Executable paths, Relay command discovery, +state directories, and similar launch mechanics are runtime implementation +details rather than public adapter settings. Use `metadata` for caller-owned annotations that NeMo Fabric should preserve and echo back, but not interpret. Adapter settings are not portable by default. An adapter must explicitly read -and implement a setting before it affects runtime behavior. Use `doctor(...)` to -catch unsupported, ignored, or malformed adapter settings before launching a run. +and implement a setting before it affects runtime behavior. Planning and doctor +fail for normalized fields the selected adapter does not support. Validation of +all adapter-specific `harness.settings` keys remains adapter-owned. ## Errors diff --git a/examples/harbor/README.md b/examples/harbor/README.md index edb4dc495..9b6d22a71 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -77,7 +77,15 @@ container boundary: | `--mcp-config` | `mcp.servers` | | `--ak fabric_blocked_tools='[...]'` | `tools.blocked` | | `--ak fabric_telemetry=relay` | `telemetry` and Relay ATOF/ATIF configuration | -| `--ak fabric_harness_settings='{...}'` | `harness.settings` for adapter-specific runtime controls | +| `--ak fabric_model_base_url=` | Selected model endpoint | +| `--ak fabric_system_prompt=` | Normalized agent system instructions | +| `--ak fabric_max_turns=` | Normalized harness turn limit | +| `--ak fabric_runtime_timeout_seconds=` | Fabric invocation deadline | +| `--ak fabric_environment_env='{...}'` | Explicit harness-visible environment variables | +| `--ak fabric_blocked_tools='[...]'` | Individual tool names to block | +| `--ak fabric_enabled_toolsets='[...]'` | Harness-defined toolsets to enable | +| `--ak fabric_blocked_toolsets='[...]'` | Harness-defined toolsets to block | +| `--ak fabric_harness_settings='{...}'` | Adapter-specific controls only | The result is the complete `FabricConfig` uploaded with the `RunRequest` and task-local `base_dir`. The container-side runner deserializes that payload and diff --git a/examples/harbor/calculator/README.md b/examples/harbor/calculator/README.md index 26d48dbd2..df4f6d4a9 100644 --- a/examples/harbor/calculator/README.md +++ b/examples/harbor/calculator/README.md @@ -85,7 +85,8 @@ uv run --extra runtime --extra harbor harbor run \ --ak fabric_adapter_id=nvidia.fabric.hermes \ --ak fabric_config_base_dir=/opt/fabric-calculator \ --ak fabric_workspace=/app \ - --ak 'fabric_harness_settings={"base_url":"https://integrate.api.nvidia.com/v1","max_iterations":20}' \ + --ak fabric_model_base_url=https://integrate.api.nvidia.com/v1 \ + --ak fabric_max_turns=20 \ --ae "NVIDIA_API_KEY=$NVIDIA_API_KEY" \ --job-name fabric-hermes \ --jobs-dir "$RUNS_DIR" \ @@ -110,7 +111,9 @@ uv run --extra runtime --extra harbor harbor run \ --ak fabric_config_base_dir=/opt/fabric-calculator \ --ak fabric_workspace=/app \ --ak fabric_telemetry=relay \ - --ak 'fabric_harness_settings={"base_url":"https://integrate.api.nvidia.com/v1","max_iterations":4,"terminal_timeout":120}' \ + --ak fabric_model_base_url=https://integrate.api.nvidia.com/v1 \ + --ak fabric_max_turns=4 \ + --ak 'fabric_harness_settings={"terminal_timeout":120}' \ --ae "NVIDIA_API_KEY=$NVIDIA_API_KEY" \ --job-name fabric-hermes-relay \ --jobs-dir "$RUNS_DIR" \ @@ -149,7 +152,8 @@ uv run --extra runtime --extra harbor harbor run \ --ak fabric_adapter_id=nvidia.fabric.claude \ --ak fabric_config_base_dir=/opt/fabric-calculator \ --ak fabric_workspace=/app \ - --ak 'fabric_harness_settings={"max_turns":20,"timeout_seconds":600}' \ + --ak fabric_max_turns=20 \ + --ak fabric_runtime_timeout_seconds=600 \ --ae "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \ --job-name fabric-claude \ --jobs-dir "$RUNS_DIR" \ diff --git a/examples/harbor/swebench/README.md b/examples/harbor/swebench/README.md index 8102eb019..eddf99cbe 100644 --- a/examples/harbor/swebench/README.md +++ b/examples/harbor/swebench/README.md @@ -93,8 +93,8 @@ uv run --extra runtime --extra harbor harbor run \ --max-retries 1 ``` -For a self-hosted OpenAI-compatible model, change `--model` and add a -`base_url` to `fabric_harness_settings`. The server must support automatic tool +For a self-hosted OpenAI-compatible model, change `--model` and add +`--ak fabric_model_base_url=`. The server must support automatic tool calling; a successful plain chat completion is not sufficient for SWE-Bench. ## Run the Same Task with Claude @@ -113,9 +113,9 @@ uv run --extra runtime --extra harbor harbor run \ --ak fabric_adapter_id=nvidia.fabric.claude \ --ak fabric_config_bundle="$FABRIC_BUNDLE" \ --ak fabric_telemetry=relay \ - --ak 'fabric_harness_settings={"nemo_relay_command":"/tmp/nemo-fabric-config/.relay/bin/nemo-relay"}' \ --ak "fabric_package=$FABRIC_PACKAGE" \ --ae "PIP_FIND_LINKS=$FABRIC_FIND_LINKS" \ + --ae "PATH=/tmp/nemo-fabric-config/.relay/bin:$PATH" \ --ae "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \ --job-name django-13741-claude \ --jobs-dir "$RUNS_DIR" \ diff --git a/examples/notebooks/01_quickstart.ipynb b/examples/notebooks/01_quickstart.ipynb index 0a005f9bb..ac9b19b22 100644 --- a/examples/notebooks/01_quickstart.ipynb +++ b/examples/notebooks/01_quickstart.ipynb @@ -169,8 +169,8 @@ "with the meaning of each block explained inline:\n", "\n", "- `metadata` -- the agent's identity.\n", - "- `harness` -- which harness to use (`adapter_id`) plus that harness's own\n", - " `settings`, which NeMo Fabric passes through untouched.\n", + "- `harness` -- which harness to use (`adapter_id`) plus harness-specific\n", + " `settings`; portable behavior stays in typed Fabric fields.\n", "- `models` -- named model aliases; `api_key_env` names the credential.\n", "- `runtime` -- the input/output contract and where artifacts are written." ] @@ -183,12 +183,15 @@ "outputs": [], "source": [ "from nemo_fabric import (\n", + " EnvironmentConfig,\n", " Fabric,\n", " FabricConfig,\n", " HarnessConfig,\n", " MetadataConfig,\n", " ModelConfig,\n", " RuntimeConfig,\n", + " ToolsConfig,\n", + " ToolsetConfig,\n", ")\n", "\n", "WORKSPACE = REPO_ROOT / \"examples\" / \"code_review_agent\" / \"repos\" / \"my-service\"\n", @@ -201,19 +204,13 @@ " description=\"Reviews Python code for correctness issues.\",\n", " ),\n", " # Which harness runs it. `adapter_id` selects the harness integration;\n", - " # everything under `settings` is handed straight to that harness.\n", + " # only Hermes-specific options remain under `settings`.\n", " harness=HarnessConfig(\n", " adapter_id=\"nvidia.fabric.hermes\",\n", " resolution=\"preinstalled\",\n", " settings={\n", - " \"workspace\": str(WORKSPACE),\n", - " \"hermes_home\": str(ARTIFACTS / \"hermes-home\"),\n", - " \"base_url\": \"https://integrate.api.nvidia.com/v1\",\n", - " \"system_prompt\": \"You are a code reviewer. Point out correctness bugs and risks, concisely.\",\n", - " \"max_iterations\": 1,\n", " \"max_tokens\": 1024,\n", " \"reasoning_config\": {\"effort\": \"none\"},\n", - " \"enabled_toolsets\": [],\n", " },\n", " ),\n", " # Named model aliases. `api_key_env` names the environment variable that\n", @@ -222,10 +219,15 @@ " \"default\": ModelConfig(\n", " provider=\"nvidia\",\n", " model=\"nvidia/nemotron-3-nano-30b-a3b\",\n", + " base_url=\"https://integrate.api.nvidia.com/v1\",\n", " temperature=0.0,\n", " api_key_env=\"NVIDIA_API_KEY\",\n", " )\n", " },\n", + " system_prompt=\"You are a code reviewer. Point out correctness bugs and risks, concisely.\",\n", + " max_turns=1,\n", + " environment=EnvironmentConfig(provider=\"local\", workspace=WORKSPACE),\n", + " tools=ToolsConfig(toolsets=ToolsetConfig(enabled=[])),\n", " # The logical input/output contract and the artifact root for this agent.\n", " runtime=RuntimeConfig(\n", " input_schema=\"chat\",\n", diff --git a/skills/nemo-fabric-integrate/SKILL.md b/skills/nemo-fabric-integrate/SKILL.md index 5b326452f..e6be40272 100644 --- a/skills/nemo-fabric-integrate/SKILL.md +++ b/skills/nemo-fabric-integrate/SKILL.md @@ -70,6 +70,8 @@ from nemo_fabric import ( MetadataConfig, ModelConfig, RuntimeConfig, + ToolsetConfig, + ToolsConfig, ) @@ -82,9 +84,23 @@ def to_fabric_config(job) -> FabricConfig: provider=job.provider, model=job.model, api_key_env=job.api_key_env, + base_url=job.base_url, ) }, - runtime=RuntimeConfig(input_schema="chat", output_schema="message"), + system_prompt=job.system_prompt, + max_turns=job.max_turns, + runtime=RuntimeConfig( + input_schema="chat", + output_schema="message", + timeout_seconds=job.timeout_seconds, + ), + tools=ToolsConfig( + blocked=list(job.blocked_tools), + toolsets=ToolsetConfig( + enabled=job.enabled_toolsets, + blocked=list(job.blocked_toolsets), + ), + ), ) config.add_skill_path(job.skill_dir) config.add_mcp_server( @@ -96,7 +112,8 @@ def to_fabric_config(job) -> FabricConfig: return config ``` -- Shape capabilities with `add_skill_path`, `remove_skill_path`, +- Shape capabilities with `block_tools`, `configure_toolsets`, + `add_skill_path`, `remove_skill_path`, `add_mcp_server`, `remove_mcp_server`, and `enable_relay`. - Create deployment or evaluation variants with `model_copy(deep=True)` and ordinary Python functions; each copy plans and runs independently. diff --git a/skills/nemo-fabric-integrate/references/config-mapping.md b/skills/nemo-fabric-integrate/references/config-mapping.md index 9a93b6784..c256cf270 100644 --- a/skills/nemo-fabric-integrate/references/config-mapping.md +++ b/skills/nemo-fabric-integrate/references/config-mapping.md @@ -18,9 +18,10 @@ Import these from the top-level `nemo_fabric` package: | `FabricConfig` | Root config passed to every `Fabric` call. | | `MetadataConfig` | Agent name and description. | | `HarnessConfig` | `adapter_id`, `resolution`, and adapter-owned `settings`. | -| `ModelConfig` | Provider, model, credentials (`api_key_env`), and sampling. | -| `RuntimeConfig` | `input_schema`, `output_schema`, and artifact locations. | -| `EnvironmentConfig` | Execution environment (`local`, sandbox, control location). | +| `ModelConfig` | Provider, model, credentials (`api_key_env`), endpoint, and sampling. | +| `RuntimeConfig` | Input/output labels, artifact location, and invocation timeout. | +| `EnvironmentConfig` | Execution environment, workspace, and harness-visible variables. | +| `ToolsConfig` / `ToolsetConfig` | Per-tool blocking and harness-defined toolset policy. | | `McpConfig` / `McpServerConfig` | MCP servers and exposure. | | `SkillConfig` | Skill directories. | | `TelemetryConfig` | Telemetry providers. | @@ -39,13 +40,17 @@ methods that edit the typed config in place and return it: - `add_skill_path(path)` / `remove_skill_path(path)` - `add_mcp_server(name, *, transport, url, exposure, ...)` / `remove_mcp_server(name)` - `enable_relay(...)` for Relay observability in the `relay` block +- `block_tools(...)` for per-tool deny policy +- `configure_toolsets(enabled=..., blocked=...)` for toolset selection ```python config = FabricConfig( metadata=MetadataConfig(name=job.name), harness=HarnessConfig(adapter_id=job.adapter_id, resolution="preinstalled"), - models={"default": ModelConfig(provider=job.provider, model=job.model, api_key_env=job.api_key_env)}, - runtime=RuntimeConfig(input_schema="chat", output_schema="message"), + models={"default": ModelConfig(provider=job.provider, model=job.model, api_key_env=job.api_key_env, base_url=job.base_url)}, + system_prompt=job.system_prompt, + max_turns=job.max_turns, + runtime=RuntimeConfig(input_schema="chat", output_schema="message", timeout_seconds=job.timeout_seconds), ) config.add_skill_path(job.skill_dir) ``` @@ -78,13 +83,15 @@ package or job layout, so nothing depends on the process working directory. ## Adapter-Owned And Caller-Owned Data -- Use normalized fields for portable behavior: models, runtime, environment, - skills, MCP, telemetry, and request context. +- Use normalized fields for portable behavior: models, system prompt, turn + limit, runtime, environment, tools and toolsets, skills, MCP, telemetry, and + request context. - Use `harness.settings` for adapter-owned configuration the selected adapter - understands (for example Hermes Agent launch options or Codex settings). Adapter - settings are not portable, and `doctor(...)` does not validate their contents — - an unknown or misspelled key still passes and is silently ignored unless the - adapter reads it. Validate settings against the adapter's docs and your + understands (for example Claude permission policy, Codex sandbox controls, or + Deep Agents subagent definitions). Executable paths, state directories, and + Relay command discovery are runtime implementation details, not adapter + settings. Adapter settings are not portable, and `doctor(...)` does not yet + validate their contents. Validate them against the adapter's docs and your integration tests. - Use `metadata` and extension fields for caller-owned annotations NeMo Fabric carries but does not interpret. Config `metadata` is not echoed into From a8aa6edeedff8c9b1e753248482e0b2dd22eb632 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 09:01:53 -0700 Subject: [PATCH 03/21] fix: address normalized config review feedback Signed-off-by: Ajay Thorve --- adapters/README.md | 2 +- .../nemo_fabric_adapters/hermes/adapter.py | 2 +- crates/fabric-cli/src/examples.rs | 16 +++++++++- crates/fabric-core/src/config.rs | 27 ++++++++++------ crates/fabric-core/src/runtime.rs | 27 ++++++++++++++-- crates/fabric-core/src/schema.rs | 12 +++++++ .../nemo_fabric.client.md | 1 + .../nemo_fabric.errors.md | 6 ++++ .../nemo_fabric.models.md | 25 +++++++++++++++ .../nemo_fabric.runtime.md | 2 ++ .../nemo_fabric.types.md | 13 ++++++++ .../config/enum-adapterconfigfield.mdx | 4 +-- .../nemo-fabric-core/config/index.mdx | 4 +-- .../config/struct-adapterconfigsupport.mdx | 2 +- .../config/struct-fabricconfig.mdx | 6 ++-- docs/sdk/python.mdx | 31 ++++++++++++++----- examples/harbor/README.md | 19 ++++++------ python/src/nemo_fabric/models.py | 6 +++- schemas/adapter-descriptor.schema.json | 4 +-- schemas/agent.schema.json | 5 +-- schemas/run-plan.schema.json | 9 +++--- scripts/generate_api_docs.sh | 4 +-- tests/adapters/test_hermes_adapter.py | 10 +++++- tests/e2e/test_codex.py | 6 ++-- tests/python/test_sdk_contract.py | 7 ++++- 25 files changed, 195 insertions(+), 55 deletions(-) diff --git a/adapters/README.md b/adapters/README.md index 8fbdea124..4a74b59d6 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -61,7 +61,7 @@ capabilities. ### Complete `FabricConfig` Support -`Core` means Fabric owns the behavior and applies it uniformly before or around +`Core` means NeMo Fabric owns the behavior and applies it uniformly before or around adapter execution. `Yes` means the adapter translates the normalized field into its harness. `No` means an explicitly configured value fails planning instead of being ignored. Provider-specific Relay subfields and additive extension maps diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 9c6fdfb26..37c0179b4 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -47,7 +47,7 @@ REMOVED_SETTING_FIELDS = { "hermes_home": "runtime state is derived from the Fabric artifact root and runtime ID", "insert_reasoning": "the supported Hermes SDK does not consume this option", - "terminal_backend": "execution placement is owned by FabricConfig.environment", + "terminal_backend": "this adapter currently supports only local terminal execution", "toolset_platform": "the adapter owns its Hermes SDK lookup bridge", } PROVIDER_DEFAULT_API_KEY_ENV = { diff --git a/crates/fabric-cli/src/examples.rs b/crates/fabric-cli/src/examples.rs index 91c46949c..6a95926db 100644 --- a/crates/fabric-cli/src/examples.rs +++ b/crates/fabric-cli/src/examples.rs @@ -119,7 +119,7 @@ pub fn find(name: &str) -> Option { const EXAMPLES: [Example; 1] = [Example { name: "code-review", - description: "Review a small Python workspace using a maintained skill.", + description: "Review a small Python workspace with a deterministic default or a skill-capable harness.", default_variant: "scripted", variants: CODE_REVIEW_VARIANTS, }]; @@ -167,6 +167,20 @@ mod tests { .expect("select example"); assert!(selected.base_dir().join("repo/calculator.py").is_file()); assert!(selected.base_dir().join("skills/code-review.md").is_file()); + assert!( + selected.config.skills.is_none(), + "the scripted smoke variant does not declare skill support" + ); + assert!( + find("code-review") + .expect("code review example") + .select(Some("hermes")) + .expect("select Hermes example") + .config + .skills + .is_some(), + "maintained harness variants consume the staged skill" + ); let plan = resolve_run_plan_from_config( selected.config.clone(), ResolveContext::new(selected.base_dir()), diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 1c61eccc0..2fd1a2e6e 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -16,9 +16,9 @@ use crate::error::{FabricError, Result}; /// Adapter descriptor contract version supported by this core. pub const ADAPTER_CONTRACT_VERSION: &str = "fabric.adapter/v1alpha1"; -/// Versioned Fabric agent config. +/// Versioned NVIDIA NeMo Fabric agent config. /// -/// Fabric-owned fields apply uniformly, while adapter-translated fields are +/// NeMo Fabric-owned fields apply uniformly, while adapter-translated fields are /// validated against the selected adapter descriptor. See the /// [configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -37,6 +37,7 @@ pub struct FabricConfig { pub system_prompt: Option, /// Maximum number of harness turns within one invocation. #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] pub max_turns: Option, /// Runtime input/output contract. pub runtime: RuntimeConfig, @@ -355,7 +356,7 @@ pub struct AdapterRequirements { /// Adapter config support. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AdapterConfigSupport { - /// Normalized Fabric config fields accepted by this adapter. + /// Normalized NVIDIA NeMo Fabric config fields accepted by this adapter. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub accepts: Vec, /// Harness-native files generated by this adapter. @@ -366,7 +367,7 @@ pub struct AdapterConfigSupport { pub extensions: BTreeMap, } -/// Adapter-translated normalized Fabric configuration fields. +/// Adapter-translated normalized NeMo Fabric configuration fields. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] @@ -499,6 +500,7 @@ pub struct RuntimeConfig { pub artifacts: Option, /// Maximum duration of one invocation in seconds. #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(extend("exclusiveMinimum" = 0.0))] pub timeout_seconds: Option, /// Additive normalized runtime fields. #[serde(default, flatten)] @@ -1256,16 +1258,11 @@ fn validate_capability_plan_compatibility( else { return Ok(()); }; - let field = match route.kind { - CapabilityKind::Mcp => format!("mcp.servers.{}", route.name), - CapabilityKind::Skills => "skills".to_string(), - CapabilityKind::Tools => route.name.clone(), - }; Err(FabricError::AdapterCompatibility { adapter_id: descriptor .map(|descriptor| descriptor.adapter_id.clone()) .unwrap_or_else(|| "unknown".to_string()), - field, + field: route.config_field(), reason: route.reason.clone(), }) } @@ -1901,6 +1898,16 @@ pub struct CapabilityRoute { pub reason: String, } +impl CapabilityRoute { + pub(crate) fn config_field(&self) -> String { + match self.kind { + CapabilityKind::Mcp => format!("mcp.servers.{}", self.name), + CapabilityKind::Skills => "skills".to_string(), + CapabilityKind::Tools => self.name.clone(), + } + } +} + /// Capability kind. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index 506535430..0360d33fb 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -576,7 +576,7 @@ fn validate_adapter_compatibility(plan: &RunPlan) -> Result<()> { { return Err(FabricError::AdapterCompatibility { adapter_id: adapter_id(plan).unwrap_or_else(|| harness(plan)), - field: route.name.clone(), + field: route.config_field(), reason: route.reason.clone(), }); } @@ -2392,7 +2392,10 @@ mod tests { use std::fs; use super::*; - use crate::config::{ResolveContext, resolve_run_plan_from_config}; + use crate::config::{ + CapabilityKind, CapabilityRoute, CapabilityTarget, ResolveContext, + resolve_run_plan_from_config, + }; fn local_host_plan(mode: &str) -> (PathBuf, RunPlan) { local_host_plan_with_relay(mode, false) @@ -2547,6 +2550,26 @@ for line in sys.stdin: TEST_STOPPED_AGENTS.lock().expect("stop tracker").clone() } + #[test] + fn runtime_validation_reports_canonical_mcp_field() { + let (root, mut plan) = local_host_plan("success"); + plan.capability_plan.routes.push(CapabilityRoute { + kind: CapabilityKind::Mcp, + name: "github".to_string(), + target: CapabilityTarget::Unsupported, + reason: "unsupported test route".to_string(), + }); + + let error = start_runtime(&plan).expect_err("unsupported MCP route must fail"); + assert!(matches!( + error, + FabricError::AdapterCompatibility { field, .. } + if field == "mcp.servers.github" + )); + + let _ = fs::remove_dir_all(root); + } + #[test] fn local_host_reuses_one_process_and_stops_idempotently() { let (root, plan) = local_host_plan("success"); diff --git a/crates/fabric-core/src/schema.rs b/crates/fabric-core/src/schema.rs index 3ad59955e..0da0113ea 100644 --- a/crates/fabric-core/src/schema.rs +++ b/crates/fabric-core/src/schema.rs @@ -212,6 +212,18 @@ mod tests { } } + #[test] + fn agent_schema_enforces_positive_invocation_limits() { + let schema = generate_schema(SchemaName::Agent).expect("schema generation"); + + assert_eq!(schema["properties"]["max_turns"]["minimum"], 1); + assert_eq!( + schema["$defs"]["RuntimeConfig"]["properties"]["timeout_seconds"] + ["exclusiveMinimum"], + 0.0 + ); + } + #[test] fn adapter_descriptor_schema_rejects_empty_identifiers() { let schema = generate_schema(SchemaName::AdapterDescriptor).expect("schema generation"); diff --git a/docs/reference/api/python-library-reference/nemo_fabric.client.md b/docs/reference/api/python-library-reference/nemo_fabric.client.md index c719a3ead..87dd7cee9 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.client.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.client.md @@ -15,6 +15,7 @@ Native Python client for resolving and running NeMo Fabric agents. ## class `Fabric` + Primary Python entrypoint for NeMo Fabric. Every lifecycle method accepts a complete, typed ``FabricConfig`` plus an optional ``base_dir`` used to resolve relative paths. Compose variants in Python before calling the SDK. The ``doctor()``, ``plan()``, and ``run()`` results are typed, read-only mapping models. ``start_runtime()`` returns an active ``Runtime`` handle. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.errors.md b/docs/reference/api/python-library-reference/nemo_fabric.errors.md index 44bad6a22..b955cca5d 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.errors.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.errors.md @@ -15,6 +15,7 @@ Public exception hierarchy for the NeMo Fabric Python SDK. ## class `FabricError` + Base class for structured SDK-level Fabric errors. Catch this type to handle any SDK failure while preserving machine-readable stage, code, retryability, and detail fields. @@ -61,6 +62,7 @@ Initialize a structured Fabric exception. ## class `FabricConfigError` + Invalid SDK input, request shape, factory, or resolved config. @@ -96,6 +98,7 @@ Initialize a structured Fabric exception. ## class `FabricRuntimeError` + Failure while starting, invoking, stopping, or otherwise driving a runtime. @@ -131,6 +134,7 @@ Initialize a structured Fabric exception. ## class `FabricStateError` + Operation rejected because a local runtime is in the wrong state. @@ -166,6 +170,7 @@ Initialize a structured Fabric exception. ## class `FabricCapabilityError` + Operation rejected by resolved runtime capabilities or implementation status. @@ -201,6 +206,7 @@ Initialize a structured Fabric exception. ## class `FabricNativeUnavailableError` + SDK call requires the PyO3 extension, but it is not installed or importable. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index b362e7fbd..bd09c0d86 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -17,6 +17,7 @@ The Rust core remains the source of truth for persisted schema snapshots. These ## class `FabricBaseModel` + Base class for SDK-facing Pydantic models. @@ -77,6 +78,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `MetadataConfig` + Human-readable agent identity. @@ -137,6 +139,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `HarnessConfig` + Harness adapter selection plus adapter-owned settings. @@ -197,6 +200,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RuntimeConfig` + Runtime input/output contract. @@ -257,6 +261,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `EnvironmentConfig` + Execution environment configuration supplied by the consumer. ``provider`` selects the environment implementation. ``workspace`` is the path visible to the harness, while ``artifacts`` is the provider-specific output location. ``settings`` configures the selected provider; ``connection`` describes how Fabric reaches an existing environment; and ``metadata`` carries consumer-owned values that Fabric does not interpret. ``ownership`` identifies who tears the environment down, and ``control_location`` identifies whether Fabric control code runs inside or outside it. @@ -319,6 +324,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `ModelConfig` + Model alias configuration. @@ -379,6 +385,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `SkillConfig` + Skill capability configuration. @@ -461,6 +468,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `McpServerConfig` + MCP server configuration. @@ -521,6 +529,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `McpConfig` + MCP capability configuration. @@ -609,6 +618,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayConfigPolicy` + NeMo Relay config validation policy. @@ -669,6 +679,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofFileSinkConfig` + NeMo Relay ATOF file sink configuration. @@ -729,6 +740,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofStreamSinkConfig` + NeMo Relay ATOF stream sink configuration. @@ -789,6 +801,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofConfig` + NeMo Relay ATOF export configuration. @@ -849,6 +862,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayS3StorageConfig` + NeMo Relay ATIF S3 storage configuration. @@ -909,6 +923,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayHttpStorageConfig` + NeMo Relay ATIF HTTP storage configuration. @@ -969,6 +984,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtifConfig` + NeMo Relay ATIF export configuration. @@ -1029,6 +1045,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayOtlpConfig` + NeMo Relay OTLP export configuration for OpenTelemetry/OpenInference. @@ -1089,6 +1106,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayObservabilityConfig` + NeMo Relay observability component configuration. @@ -1149,6 +1167,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayComponentConfig` + Generic NeMo Relay plugin component configuration. @@ -1209,6 +1228,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayConfig` + First-class NeMo Relay integration configuration. @@ -1269,6 +1289,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `TelemetryProviderConfig` + Provider-specific telemetry configuration. @@ -1329,6 +1350,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `TelemetryConfig` + Telemetry configuration. @@ -1422,6 +1444,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `ToolsetConfig` + Harness-neutral toolset selection and blocking policy. @@ -1543,6 +1566,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `FabricConfig` + SDK-facing typed Fabric agent configuration. Fabric-owned fields apply uniformly. Adapter-translated fields are checked against the selected descriptor; see the normalized configuration compatibility table in ``docs/sdk/python.mdx``. @@ -1697,6 +1721,7 @@ Return a detached mapping matching the Rust ``FabricConfig`` schema. ## class `RunRequest` + One validated Fabric invocation request. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.runtime.md b/docs/reference/api/python-library-reference/nemo_fabric.runtime.md index e0e1474f2..751e48418 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.runtime.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.runtime.md @@ -15,6 +15,7 @@ Runtime lifecycle support for the Fabric Python SDK. ## class `RuntimeStatus` + Lifecycle state of a runtime. ``ACTIVE`` accepts invocations, ``STOPPED`` has released its runtime, and ``FAILED`` records a lifecycle failure that prevents further invocations but still permits cleanup. @@ -27,6 +28,7 @@ Lifecycle state of a runtime. ## class `Runtime` + One logical, stateful harness execution. Create runtimes with ``Fabric.start_runtime()`` rather than calling the constructor. A runtime serializes invocations and preserves adapter-owned harness state across turns. Use it as an asynchronous context manager to stop the runtime on exit. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.types.md b/docs/reference/api/python-library-reference/nemo_fabric.types.md index 5de0e451d..24931a35c 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.types.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.types.md @@ -15,6 +15,7 @@ Public data contracts for the NeMo Fabric Python SDK. ## class `AdapterInfo` + Resolved adapter identity attached to a run plan. @@ -84,6 +85,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RuntimeCapabilities` + Operations declared by the resolved runtime and adapter. Capabilities describe what the selected runtime can support; callers should still expect a capability-specific error when a transport is modeled but not implemented. @@ -156,6 +158,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunPlan` + Immutable execution plan produced before a runtime is started. @@ -226,6 +229,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `DoctorCheck` + One diagnostic check in a ``DoctorReport``. @@ -295,6 +299,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `DoctorReport` + Aggregate preflight diagnostics for a resolved run plan. @@ -363,6 +368,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ErrorInfo` + Structured failure returned inside a normalized ``RunResult``. @@ -433,6 +439,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ArtifactRef` + Reference to one artifact produced by a run. @@ -503,6 +510,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ArtifactManifest` + Normalized collection of artifacts produced by a run. @@ -570,6 +578,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `TelemetryRef` + Reference to external or persisted telemetry for a run. @@ -640,6 +649,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `FabricEvent` + One normalized lifecycle or invocation event. @@ -710,6 +720,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RuntimeHandle` + Opaque identity and binding for one started runtime. Applications should treat ``runtime_binding`` as opaque. Fabric validates the handle against the run plan before invocation or shutdown. @@ -784,6 +795,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunOutput` + Normalized adapter output. ``response`` is a known adapter response field whose value follows the core Fabric JSON contract. Other keys are adapter-specific extensions. @@ -852,6 +864,7 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunResult` + Normalized terminal result from one Fabric invocation. The model is both attribute-accessible and mapping-compatible. A harness failure can be represented by ``status`` and ``error`` without raising when the adapter successfully returns a normalized result. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx index 7d99df503..d14c06cc1 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx @@ -1,7 +1,7 @@ --- title: "Enum Adapter Config Field" sidebar-title: "AdapterConfigField" -description: "Adapter-translated normalized Fabric configuration fields." +description: "Adapter-translated normalized NeMo Fabric configuration fields." position: 2 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -25,7 +25,7 @@ pub enum AdapterConfigField { } ``` -Adapter-translated normalized Fabric configuration fields. +Adapter-translated normalized NeMo Fabric configuration fields. ## Variants diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx index 5fea806f4..66b7a1138 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx @@ -23,7 +23,7 @@ Fabric config models and loading helpers. - [CapabilityTargetPlan](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilitytargetplan): Capabilities routed to one target. - [EnvironmentConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig): Execution environment configuration. - [EnvironmentPlan](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentplan): Resolved environment plan. -- [FabricConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig): Versioned Fabric agent config. +- [FabricConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig): Versioned NVIDIA NeMo Fabric agent config. - [HarnessConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-harnessconfig): Harness selection. - [McpConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig): MCP capability configuration. - [McpServerConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig): MCP server configuration. @@ -52,7 +52,7 @@ Fabric config models and loading helpers. ## Enums -- [AdapterConfigField](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield): Adapter-translated normalized Fabric configuration fields. +- [AdapterConfigField](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield): Adapter-translated normalized NeMo Fabric configuration fields. - [AdapterDescriptorSource](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource): Where Fabric resolved an adapter descriptor from. - [AdapterKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind): Adapter implementation kind. - [CapabilityKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind): Capability kind. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx index 0cf693fa0..b432326ca 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-adapterconfigsupport.mdx @@ -17,7 +17,7 @@ Adapter config support. ### `accepts: Vec` -Normalized Fabric config fields accepted by this adapter. +Normalized NVIDIA NeMo Fabric config fields accepted by this adapter. ### `generates: Vec` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx index 1347673f1..95f888241 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx @@ -1,7 +1,7 @@ --- title: "Struct Fabric Config" sidebar-title: "FabricConfig" -description: "Versioned Fabric agent config." +description: "Versioned NVIDIA NeMo Fabric agent config." position: 15 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -11,9 +11,9 @@ Generated from `cargo doc --no-deps -p nemo-fabric-core`.
String,\n    pub metadata: MetadataConfig,\n    pub harness: HarnessConfig,\n    pub models: BTreeMap<String, ModelConfig>,\n    pub system_prompt: Option<String>,\n    pub max_turns: Option<u32>,\n    pub runtime: RuntimeConfig,\n    pub environment: Option<EnvironmentConfig>,\n    pub tools: Option<ToolsConfig>,\n    pub skills: Option<SkillConfig>,\n    pub mcp: Option<McpConfig>,\n    pub telemetry: Option<TelemetryConfig>,\n    pub relay: Option<RelayConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
-Versioned Fabric agent config. +Versioned NVIDIA NeMo Fabric agent config. -Fabric-owned fields apply uniformly, while adapter-translated fields are validated against the selected adapter descriptor. See the configuration compatibility matrix. +NeMo Fabric-owned fields apply uniformly, while adapter-translated fields are validated against the selected adapter descriptor. See the configuration compatibility matrix. ## Fields diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index 4c05ffd40..d4d6c6ce5 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -171,7 +171,7 @@ retries, worker scaling, and the number of runtimes to run. ### Normalized Configuration Compatibility -Fabric-owned fields such as workspace, environment variables, and the +NeMo Fabric-owned fields such as workspace, environment variables, and the invocation deadline apply to every local adapter. Adapter-translated fields are validated against the selected adapter descriptor during planning. @@ -237,12 +237,29 @@ capability_config.enable_relay( project="fabric-review", output_dir="./artifacts/relay", ) -capability_config.block_tools("Bash") -# Toolsets are harness-defined bundles, not individual tool names. -capability_config.configure_toolsets( - enabled=["web", "terminal"], - blocked=["browser"], -) +``` + +Tool names and toolsets remain separate because adapters expose different +native controls. Configure individual blocked tools only for Claude or Deep +Agents: + +```python +def with_blocked_tools(claude_or_deepagents_config): + blocked_tools_config = claude_or_deepagents_config.model_copy(deep=True) + blocked_tools_config.block_tools("Bash") + return blocked_tools_config +``` + +Configure harness-defined toolsets only for Hermes: + +```python +def with_toolsets(hermes_config): + toolset_config = hermes_config.model_copy(deep=True) + toolset_config.configure_toolsets( + enabled=["web", "terminal"], + blocked=["browser"], + ) + return toolset_config ``` Config helpers edit the typed config before planning or starting a runtime. They diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 9b6d22a71..96aa938bb 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -75,17 +75,16 @@ container boundary: | `--model` | `models.default` | | `--skill` | `skills.paths` | | `--mcp-config` | `mcp.servers` | +| `--ak fabric_telemetry=relay` | `telemetry.providers.relay` and `relay.observability` | +| `--ak fabric_model_base_url=` | `models.default.base_url` | +| `--ak fabric_system_prompt=` | `system_prompt` | +| `--ak fabric_max_turns=` | `max_turns` | +| `--ak fabric_runtime_timeout_seconds=` | `runtime.timeout_seconds` | +| `--ak fabric_environment_env='{...}'` | `environment.env` | | `--ak fabric_blocked_tools='[...]'` | `tools.blocked` | -| `--ak fabric_telemetry=relay` | `telemetry` and Relay ATOF/ATIF configuration | -| `--ak fabric_model_base_url=` | Selected model endpoint | -| `--ak fabric_system_prompt=` | Normalized agent system instructions | -| `--ak fabric_max_turns=` | Normalized harness turn limit | -| `--ak fabric_runtime_timeout_seconds=` | Fabric invocation deadline | -| `--ak fabric_environment_env='{...}'` | Explicit harness-visible environment variables | -| `--ak fabric_blocked_tools='[...]'` | Individual tool names to block | -| `--ak fabric_enabled_toolsets='[...]'` | Harness-defined toolsets to enable | -| `--ak fabric_blocked_toolsets='[...]'` | Harness-defined toolsets to block | -| `--ak fabric_harness_settings='{...}'` | Adapter-specific controls only | +| `--ak fabric_enabled_toolsets='[...]'` | `tools.toolsets.enabled` | +| `--ak fabric_blocked_toolsets='[...]'` | `tools.toolsets.blocked` | +| `--ak fabric_harness_settings='{...}'` | `harness.settings` | The result is the complete `FabricConfig` uploaded with the `RunRequest` and task-local `base_dir`. The container-side runner deserializes that payload and diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index 3e2abcfb9..d6b64c06d 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -111,7 +111,11 @@ class RuntimeConfig(FabricBaseModel): input_schema: str | None = None output_schema: str | None = None artifacts: str | Path | None = None - timeout_seconds: float | None = Field(default=None, gt=0) + timeout_seconds: float | None = Field( + default=None, + gt=0, + allow_inf_nan=False, + ) class EnvironmentConfig(FabricBaseModel): diff --git a/schemas/adapter-descriptor.schema.json b/schemas/adapter-descriptor.schema.json index 8188b1469..d8d3e06df 100644 --- a/schemas/adapter-descriptor.schema.json +++ b/schemas/adapter-descriptor.schema.json @@ -1,7 +1,7 @@ { "$defs": { "AdapterConfigField": { - "description": "Adapter-translated normalized Fabric configuration fields.", + "description": "Adapter-translated normalized NeMo Fabric configuration fields.", "oneOf": [ { "const": "models", @@ -65,7 +65,7 @@ "description": "Adapter config support.", "properties": { "accepts": { - "description": "Normalized Fabric config fields accepted by this adapter.", + "description": "Normalized NVIDIA NeMo Fabric config fields accepted by this adapter.", "items": { "$ref": "#/$defs/AdapterConfigField" }, diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 5f3c338ee..957f281e7 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -912,6 +912,7 @@ }, "timeout_seconds": { "description": "Maximum duration of one invocation in seconds.", + "exclusiveMinimum": 0.0, "format": "double", "type": [ "number", @@ -1011,7 +1012,7 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": true, - "description": "Versioned Fabric agent config.\n\nFabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", + "description": "Versioned NVIDIA NeMo Fabric agent config.\n\nNeMo Fabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", "properties": { "environment": { "anyOf": [ @@ -1031,7 +1032,7 @@ "max_turns": { "description": "Maximum number of harness turns within one invocation.", "format": "uint32", - "minimum": 0, + "minimum": 1, "type": [ "integer", "null" diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 6b25096e3..0079ebf1a 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -1,7 +1,7 @@ { "$defs": { "AdapterConfigField": { - "description": "Adapter-translated normalized Fabric configuration fields.", + "description": "Adapter-translated normalized NeMo Fabric configuration fields.", "oneOf": [ { "const": "models", @@ -65,7 +65,7 @@ "description": "Adapter config support.", "properties": { "accepts": { - "description": "Normalized Fabric config fields accepted by this adapter.", + "description": "Normalized NVIDIA NeMo Fabric config fields accepted by this adapter.", "items": { "$ref": "#/$defs/AdapterConfigField" }, @@ -562,7 +562,7 @@ }, "FabricConfig": { "additionalProperties": true, - "description": "Versioned Fabric agent config.\n\nFabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", + "description": "Versioned NVIDIA NeMo Fabric agent config.\n\nNeMo Fabric-owned fields apply uniformly, while adapter-translated fields are\nvalidated against the selected adapter descriptor. See the\n[configuration compatibility matrix](/sdk/python#normalized-configuration-compatibility).", "properties": { "environment": { "anyOf": [ @@ -582,7 +582,7 @@ "max_turns": { "description": "Maximum number of harness turns within one invocation.", "format": "uint32", - "minimum": 0, + "minimum": 1, "type": [ "integer", "null" @@ -1582,6 +1582,7 @@ }, "timeout_seconds": { "description": "Maximum duration of one invocation in seconds.", + "exclusiveMinimum": 0.0, "format": "double", "type": [ "number", diff --git a/scripts/generate_api_docs.sh b/scripts/generate_api_docs.sh index b8628f68f..9b25cc377 100755 --- a/scripts/generate_api_docs.sh +++ b/scripts/generate_api_docs.sh @@ -39,8 +39,8 @@ perl -0pi -e 's/\A\s+//' "$out"/*.md # lazydocs nests properties at h4 directly under h2 class sections. Flatten # those headings to h3 so generated pages satisfy markdown heading order. perl -pi -e 's/^#### (property<\/kbd>)/### $1/' "$out"/*.md -# lazydocs emits the ToolsConfig class heading without a separating blank line. -perl -0pi -e 's/(^## class<\/kbd> `ToolsConfig`\n)(?!\n)/$1\n/m' "$out/nemo_fabric.models.md" +# lazydocs emits some class headings without a separating blank line. +perl -0pi -e 's/(^## class<\/kbd> `[^`]+`\n)(?!\n)/$1\n/gm' "$out"/*.md add_frontmatter() { local file="$1" diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index bc97bd8df..2c2f837b7 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -659,13 +659,21 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( @pytest.mark.parametrize( "setting", - ["hermes_home", "insert_reasoning", "terminal_backend", "toolset_platform"], + ["hermes_home", "insert_reasoning", "toolset_platform"], ) def test_removed_runtime_settings_are_rejected(setting): with pytest.raises(ValueError, match=rf"harness\.settings\.{setting}"): adapter._validate_settings_boundary({setting: "value"}) +def test_terminal_backend_reports_local_only_support(): + with pytest.raises( + ValueError, + match=r"harness\.settings\.terminal_backend.*only local terminal execution", + ): + adapter._validate_settings_boundary({"terminal_backend": "ssh"}) + + def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/e2e/test_codex.py b/tests/e2e/test_codex.py index da0c9039b..023ef7e2e 100644 --- a/tests/e2e/test_codex.py +++ b/tests/e2e/test_codex.py @@ -39,7 +39,7 @@ async def test_codex_sdk_with_relay(): ) if relay_command is None: pytest.fail("the nemo-relay CLI is required") - await _run_relay() + await _run_relay(str(relay_command)) async def _run() -> None: @@ -80,11 +80,13 @@ async def _run() -> None: assert second["output"]["usage"] is not None, second.to_mapping() -async def _run_relay() -> None: +async def _run_relay(relay_command: str) -> None: from examples.code_review_agent import BASE_DIR, codex_config, with_relay from nemo_fabric import Fabric config = with_relay(codex_config()) + assert config.environment is not None + config.environment.env["FABRIC_TEST_NEMO_RELAY_COMMAND"] = relay_command client = Fabric() result = await client.run( config, diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index f60c703f3..502552005 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -244,6 +244,8 @@ def test_typed_config_serializes_normalized_execution_fields(): with pytest.raises(ValidationError, match="greater than 0"): RuntimeConfig(timeout_seconds=0) + with pytest.raises(ValidationError, match="finite number"): + RuntimeConfig(timeout_seconds=float("inf")) with pytest.raises(ValidationError, match="greater than 0"): FabricConfig( metadata=MetadataConfig(name="demo"), @@ -252,7 +254,10 @@ def test_typed_config_serializes_normalized_execution_fields(): ) with pytest.raises(ValidationError, match="both enabled and blocked"): ToolsetConfig(enabled=["browser"], blocked=["browser"]) - with pytest.raises(ValidationError, match="models..base_url"): + with pytest.raises( + ValidationError, + match=r"models\.\.settings\.base_url", + ): FabricConfig( metadata=MetadataConfig(name="demo"), harness=HarnessConfig(adapter_id="test.fabric.shim"), From c3891cd5b02803f9d4dd1d5feadacdedd1257c4c Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 09:08:23 -0700 Subject: [PATCH 04/21] style: format schema contract assertion Signed-off-by: Ajay Thorve --- crates/fabric-core/src/schema.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/fabric-core/src/schema.rs b/crates/fabric-core/src/schema.rs index 0da0113ea..dffbab222 100644 --- a/crates/fabric-core/src/schema.rs +++ b/crates/fabric-core/src/schema.rs @@ -218,8 +218,7 @@ mod tests { assert_eq!(schema["properties"]["max_turns"]["minimum"], 1); assert_eq!( - schema["$defs"]["RuntimeConfig"]["properties"]["timeout_seconds"] - ["exclusiveMinimum"], + schema["$defs"]["RuntimeConfig"]["properties"]["timeout_seconds"]["exclusiveMinimum"], 0.0 ); } From 4405a5fe6772932d236d8a83c1597971c20449ec Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 09:41:56 -0700 Subject: [PATCH 05/21] docs: qualify normalized config product name Signed-off-by: Ajay Thorve --- crates/fabric-core/src/config.rs | 2 +- .../nemo-fabric-core/config/enum-adapterconfigfield.mdx | 4 ++-- .../rust-library-reference/nemo-fabric-core/config/index.mdx | 2 +- schemas/adapter-descriptor.schema.json | 2 +- schemas/run-plan.schema.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 2fd1a2e6e..974fc9e1a 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -367,7 +367,7 @@ pub struct AdapterConfigSupport { pub extensions: BTreeMap, } -/// Adapter-translated normalized NeMo Fabric configuration fields. +/// Adapter-translated normalized NVIDIA NeMo Fabric configuration fields. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, )] diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx index d14c06cc1..641bb4e86 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx @@ -1,7 +1,7 @@ --- title: "Enum Adapter Config Field" sidebar-title: "AdapterConfigField" -description: "Adapter-translated normalized NeMo Fabric configuration fields." +description: "Adapter-translated normalized NVIDIA NeMo Fabric configuration fields." position: 2 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -25,7 +25,7 @@ pub enum AdapterConfigField { } ``` -Adapter-translated normalized NeMo Fabric configuration fields. +Adapter-translated normalized NVIDIA NeMo Fabric configuration fields. ## Variants diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx index 66b7a1138..dbe36e0d1 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx @@ -52,7 +52,7 @@ Fabric config models and loading helpers. ## Enums -- [AdapterConfigField](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield): Adapter-translated normalized NeMo Fabric configuration fields. +- [AdapterConfigField](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield): Adapter-translated normalized NVIDIA NeMo Fabric configuration fields. - [AdapterDescriptorSource](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterdescriptorsource): Where Fabric resolved an adapter descriptor from. - [AdapterKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterkind): Adapter implementation kind. - [CapabilityKind](/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind): Capability kind. diff --git a/schemas/adapter-descriptor.schema.json b/schemas/adapter-descriptor.schema.json index d8d3e06df..78f2d01e1 100644 --- a/schemas/adapter-descriptor.schema.json +++ b/schemas/adapter-descriptor.schema.json @@ -1,7 +1,7 @@ { "$defs": { "AdapterConfigField": { - "description": "Adapter-translated normalized NeMo Fabric configuration fields.", + "description": "Adapter-translated normalized NVIDIA NeMo Fabric configuration fields.", "oneOf": [ { "const": "models", diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 0079ebf1a..c03909c96 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -1,7 +1,7 @@ { "$defs": { "AdapterConfigField": { - "description": "Adapter-translated normalized NeMo Fabric configuration fields.", + "description": "Adapter-translated normalized NVIDIA NeMo Fabric configuration fields.", "oneOf": [ { "const": "models", From b479537a5e38868c877254b84a60ae42523278cf Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 10:11:12 -0700 Subject: [PATCH 06/21] refactor: simplify normalized configuration Signed-off-by: Ajay Thorve --- adapters/claude/README.md | 5 -- adapters/claude/fabric-adapter.json | 3 +- .../nemo_fabric_adapters/claude/adapter.py | 33 ------------ adapters/codex/fabric-adapter.json | 3 +- .../src/nemo_fabric_adapters/codex/adapter.py | 51 +------------------ .../src/nemo_fabric_adapters/common/utils.py | 5 +- adapters/deepagents/README.md | 6 +-- adapters/deepagents/fabric-adapter.json | 3 +- .../deepagents/adapter.py | 30 ----------- adapters/hermes/fabric-adapter.json | 3 +- .../nemo_fabric_adapters/hermes/adapter.py | 33 ------------ .../adapters/claude/fabric-adapter.json | 3 +- .../assets/adapters/codex/fabric-adapter.json | 3 +- .../adapters/deepagents/fabric-adapter.json | 3 +- .../adapters/hermes/fabric-adapter.json | 3 +- crates/fabric-core/src/config.rs | 42 +-------------- crates/fabric-core/src/runtime.rs | 25 +-------- .../api/python-library-reference/index.md | 2 +- .../nemo_fabric.client.md | 1 - .../nemo_fabric.errors.md | 6 --- .../nemo_fabric.models.md | 26 +--------- .../nemo_fabric.runtime.md | 2 - .../nemo_fabric.types.md | 13 ----- .../config/enum-adapterconfigfield.mdx | 7 --- .../config/struct-fabricconfig.mdx | 2 +- .../adapters/claude/fabric-adapter.json | 3 +- .../adapters/hermes/fabric-adapter.json | 3 +- examples/notebooks/01_quickstart.ipynb | 4 +- python/src/nemo_fabric/models.py | 12 +---- schemas/adapter-descriptor.schema.json | 5 -- schemas/agent.schema.json | 2 +- schemas/run-plan.schema.json | 7 +-- scripts/generate_api_docs.sh | 4 +- tests/adapters/test_adapaters_common_utils.py | 16 ------ tests/adapters/test_claude_adapter.py | 43 +--------------- tests/adapters/test_codex_adapter.py | 51 ------------------- tests/adapters/test_deepagents.py | 27 +--------- tests/adapters/test_hermes_adapter.py | 51 +------------------ .../adapters/hermes-shim/fabric-adapter.json | 3 +- tests/python/test_sdk_contract.py | 17 ------- tests/python/test_typed_config.py | 4 -- 41 files changed, 29 insertions(+), 536 deletions(-) diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 5a6837cf9..42d5b924f 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -103,11 +103,6 @@ Only Claude-specific controls belong in `harness.settings`: - `max_budget_usd` - `setting_sources` (defaults to `[]` for deterministic isolation) -Putting `model_name`, `base_url`, `cwd`, `system_prompt`, `max_turns`, -`timeout_seconds`, `env`, `tools`, `disallowed_tools`, `mcp_servers`, or `skills` -in `harness.settings` is an error. Use the corresponding normalized field so -the same consumer configuration can compose with other adapters. - The adapter filters the inherited environment before launching Claude Code. It retains portable OS/config variables, the selected model's `api_key_env`, and explicitly configured `environment.env` values. Raw Claude stderr is consumed diff --git a/adapters/claude/fabric-adapter.json b/adapters/claude/fabric-adapter.json index 5ea209bb2..688f06586 100644 --- a/adapters/claude/fabric-adapter.json +++ b/adapters/claude/fabric-adapter.json @@ -14,8 +14,7 @@ "max_turns", "tools.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py index ea0d4a50e..21f043ccc 100644 --- a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py +++ b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py @@ -46,23 +46,6 @@ "auto", } SETTING_SOURCES = {"user", "project", "local"} -REMOVED_SETTING_FIELDS = { - "cli_path": "the Claude Agent SDK owns its bundled runtime", - "nemo_relay_command": "NeMo Relay is resolved from PATH", -} -NORMALIZED_SETTING_FIELDS = { - "model_name": "FabricConfig.models", - "base_url": "FabricConfig.models..base_url", - "cwd": "FabricConfig.environment.workspace", - "env": "FabricConfig.environment.env", - "system_prompt": "FabricConfig.system_prompt", - "max_turns": "FabricConfig.max_turns", - "timeout_seconds": "FabricConfig.runtime.timeout_seconds", - "tools": "FabricConfig.tools", - "disallowed_tools": "FabricConfig.tools.blocked", - "mcp_servers": "FabricConfig.mcp", - "skills": "FabricConfig.skills", -} INHERITED_ENV_NAMES = { "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", @@ -199,21 +182,6 @@ def _settings(payload: dict[str, Any]) -> dict[str, Any]: return _mapping(common_utils.settings_payload(payload), name="harness.settings") -def _validate_settings_boundary(settings: dict[str, Any]) -> None: - for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - "claude_invalid_configuration", - f"harness.settings.{name} is not supported; use {normalized_field}", - ) - for name, reason in REMOVED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - "claude_invalid_configuration", - f"harness.settings.{name} is not supported; {reason}", - ) - - def _selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: return _mapping( common_utils.selected_model_config(payload), @@ -503,7 +471,6 @@ def build_options( relay: ClaudeRelaySettings | None = None, ) -> ClaudeAgentOptions: settings = _settings(payload) - _validate_settings_boundary(settings) permission_mode = settings.get("permission_mode") if permission_mode is not None and permission_mode not in PERMISSION_MODES: raise AdapterConfigError( diff --git a/adapters/codex/fabric-adapter.json b/adapters/codex/fabric-adapter.json index 27eac7877..be5ffbbe2 100644 --- a/adapters/codex/fabric-adapter.json +++ b/adapters/codex/fabric-adapter.json @@ -12,8 +12,7 @@ "models.base_url", "system_prompt", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py index 471712b80..ae549899a 100644 --- a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py +++ b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py @@ -78,30 +78,6 @@ "https_proxy", "no_proxy", } -REMOVED_CLI_SETTINGS = { - "codex_args", - "codex_command", - "codex_profile", - "codex_state_dir", - "skip_git_repo_check", -} -REMOVED_SETTING_FIELDS = { - "codex_bin": "the Codex SDK owns its pinned app-server runtime", - "nemo_relay_command": "NeMo Relay is resolved from PATH", - "service_name": "the Codex app-server service label is not portable consumer intent", -} -NORMALIZED_SETTING_FIELDS = { - "model": "FabricConfig.models", - "base_instructions": "FabricConfig.system_prompt", - "system_prompt": "FabricConfig.system_prompt", - "cwd": "FabricConfig.environment.workspace", - "env": "FabricConfig.environment.env", - "base_url": "FabricConfig.models..base_url", - "timeout_seconds": "FabricConfig.runtime.timeout_seconds", - "mcp_servers": "FabricConfig.mcp", - "model_name": "FabricConfig.models", - "skills": "FabricConfig.skills", -} LOGGER = logging.getLogger(__name__) @@ -155,28 +131,6 @@ def _settings(payload: dict[str, Any]) -> dict[str, Any]: return _mapping(common_utils.settings_payload(payload), name="harness.settings") -def _validate_settings_boundary(settings: dict[str, Any]) -> None: - removed = sorted(REMOVED_CLI_SETTINGS.intersection(settings)) - if removed: - names = ", ".join(f"harness.settings.{name}" for name in removed) - raise AdapterConfigError( - "codex_invalid_configuration", - f"Codex CLI-only settings are not supported by the SDK adapter: {names}", - ) - for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - "codex_invalid_configuration", - f"harness.settings.{name} is not supported; use {normalized_field}", - ) - for name, reason in REMOVED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - "codex_invalid_configuration", - f"harness.settings.{name} is not supported; {reason}", - ) - - def runtime_id(payload: dict[str, Any]) -> str: value = common_utils.runtime_context(payload).get("runtime_id") if not isinstance(value, str) or not value: @@ -616,9 +570,7 @@ def prepare_codex_relay(payload: dict[str, Any]) -> CodexRelaySettings | None: relay_contract = relay_gateway.relay_cli_contract(executable) plugin_config = common_utils.load_relay_plugin_config(payload) config_path, plugin_config_path = common_utils.write_relay_configs( - # The SDK owns Codex execution. Relay needs only gateway defaults and - # the sibling plugins.toml; configuring an agent command would retain - # a misleading dependency on the removed Codex CLI launch path. + # Codex execution remains SDK-owned; Relay runs only as a gateway. relay_config={}, plugin_config=plugin_config, observability_version=relay_contract.observability_version, @@ -745,7 +697,6 @@ def validate_runtime_payload(payload: dict[str, Any]) -> str: """Validate runtime-owned configuration before starting SDK or Relay processes.""" settings = _settings(payload) - _validate_settings_boundary(settings) _native_skill_paths(payload) fabric_runtime_id = runtime_id(payload) resolve_cwd(payload) diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index e81c59cea..c717bd4b4 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -91,10 +91,7 @@ def runtime_state_directory(base: str | Path, payload: dict[str, Any]) -> Path: def environment_payload(payload: dict[str, Any]) -> dict[str, Any]: return ( - runtime_context(payload).get("environment") - or fabric_config(payload).get("environment") - or payload.get("environment") - or {} + runtime_context(payload).get("environment") or payload.get("environment") or {} ) diff --git a/adapters/deepagents/README.md b/adapters/deepagents/README.md index 7d02d47b3..3c39a893d 100644 --- a/adapters/deepagents/README.md +++ b/adapters/deepagents/README.md @@ -68,13 +68,9 @@ NeMo Fabric maps the following into the harness: instances, and Python callables cannot cross the boundary. NeMo Fabric-owned arguments (`model`, `tools`, `backend`, `skills`, `system_prompt`, `middleware`, `checkpointer`) cannot be overridden through this passthrough, and an unknown or - unsupported key is a normalized configuration failure rather than a silently + unsupported key is an adapter configuration failure rather than a silently dropped setting. -The legacy `harness.settings` aliases for model fields, `system_prompt`, -`workspace`, environment variables, and invocation timeout are rejected. Keep -only the Deep Agents-specific `deepagents` controls there. - ### Subagents Deep Agents can delegate to subagents through its built-in `task` tool. Subagents diff --git a/adapters/deepagents/fabric-adapter.json b/adapters/deepagents/fabric-adapter.json index 5d649557d..02f50f8fc 100644 --- a/adapters/deepagents/fabric-adapter.json +++ b/adapters/deepagents/fabric-adapter.json @@ -15,8 +15,7 @@ "system_prompt", "tools.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py index baa9e4e7e..aa53cbb15 100644 --- a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py +++ b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py @@ -57,21 +57,6 @@ # through harness.settings.deepagents. Executable objects (AgentMiddleware, BaseTool, # Python callables) cannot cross the SDK->JSON->payload boundary and are excluded. DEEPAGENTS_PASSTHROUGH_KEYS = frozenset({"subagents", "interrupt_on"}) -NORMALIZED_SETTING_FIELDS = { - "model": "FabricConfig.models", - "model_name": "FabricConfig.models..model", - "provider": "FabricConfig.models..provider", - "api_key_env": "FabricConfig.models..api_key_env", - "base_url": "FabricConfig.models..base_url", - "temperature": "FabricConfig.models..temperature", - "system_prompt": "FabricConfig.system_prompt", - "workspace": "FabricConfig.environment.workspace", - "env": "FabricConfig.environment.env", - "timeout_seconds": "FabricConfig.runtime.timeout_seconds", -} -REMOVED_SETTING_FIELDS = { - "state_dir": "runtime state is derived from the Fabric artifact root and runtime ID", -} class AdapterConfigError(RuntimeError): @@ -108,19 +93,6 @@ def wrap_tool_call(self, request: Any, handler: Any) -> Any: return handler(request) -def _validate_settings_boundary(settings: dict[str, Any]) -> None: - for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - f"harness.settings.{name} is not supported; use {normalized_field}" - ) - for name, reason in REMOVED_SETTING_FIELDS.items(): - if name in settings: - raise AdapterConfigError( - f"harness.settings.{name} is not supported; {reason}" - ) - - def resolve_api_key_env(model_config: dict[str, Any]) -> str: """Resolve the credential env var, defaulting per provider. @@ -164,8 +136,6 @@ def preflight_check(payload: dict[str, Any]) -> None: "it with the 'deepagents' extra (pip install nemo-fabric-adapters-deepagents)." ) - settings = common_utils.settings_payload(payload) - _validate_settings_boundary(settings) model_config = selected_model_config(payload) api_key_env = resolve_api_key_env(model_config) if api_key_env not in os.environ: diff --git a/adapters/hermes/fabric-adapter.json b/adapters/hermes/fabric-adapter.json index 40927fb41..804acb622 100644 --- a/adapters/hermes/fabric-adapter.json +++ b/adapters/hermes/fabric-adapter.json @@ -21,8 +21,7 @@ "tools.toolsets.enabled", "tools.toolsets.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 37c0179b4..89520a477 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -29,27 +29,6 @@ # answering while the trial still reports success). See FABRIC-85. DEFAULT_MAX_ITERATIONS: int = 90 LOGGER = logging.getLogger(__name__) -NORMALIZED_SETTING_FIELDS = { - "model": "FabricConfig.models", - "model_name": "FabricConfig.models..model", - "provider": "FabricConfig.models..provider", - "api_key_env": "FabricConfig.models..api_key_env", - "base_url": "FabricConfig.models..base_url", - "temperature": "FabricConfig.models..temperature", - "system_prompt": "FabricConfig.system_prompt", - "max_iterations": "FabricConfig.max_turns", - "max_turns": "FabricConfig.max_turns", - "workspace": "FabricConfig.environment.workspace", - "env": "FabricConfig.environment.env", - "enabled_toolsets": "FabricConfig.tools.toolsets.enabled", - "disabled_toolsets": "FabricConfig.tools.toolsets.blocked", -} -REMOVED_SETTING_FIELDS = { - "hermes_home": "runtime state is derived from the Fabric artifact root and runtime ID", - "insert_reasoning": "the supported Hermes SDK does not consume this option", - "terminal_backend": "this adapter currently supports only local terminal execution", - "toolset_platform": "the adapter owns its Hermes SDK lookup bridge", -} PROVIDER_DEFAULT_API_KEY_ENV = { "": "NVIDIA_API_KEY", "anthropic": "ANTHROPIC_API_KEY", @@ -59,17 +38,6 @@ } -def _validate_settings_boundary(settings: dict[str, Any]) -> None: - for name, normalized_field in NORMALIZED_SETTING_FIELDS.items(): - if name in settings: - raise ValueError( - f"harness.settings.{name} is not supported; use {normalized_field}" - ) - for name, reason in REMOVED_SETTING_FIELDS.items(): - if name in settings: - raise ValueError(f"harness.settings.{name} is not supported; {reason}") - - def _api_key_env(model_config: dict[str, Any]) -> str: explicit = model_config.get("api_key_env") if isinstance(explicit, str) and explicit: @@ -261,7 +229,6 @@ async def start(self, payload: dict[str, Any]) -> None: self._relay_finalize_hook_invoked = False validate_hermes_telemetry_provider(payload) self._settings = common_utils.settings_payload(payload) - _validate_settings_boundary(self._settings) self._model_config = common_utils.selected_model_config(payload) self._runtime_id = common_utils.runtime_id(payload) self._hermes_home = common_utils.runtime_state_directory( diff --git a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json index 5ea209bb2..688f06586 100644 --- a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json @@ -14,8 +14,7 @@ "max_turns", "tools.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json index 27eac7877..be5ffbbe2 100644 --- a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json @@ -12,8 +12,7 @@ "models.base_url", "system_prompt", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json index 5d649557d..02f50f8fc 100644 --- a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json @@ -15,8 +15,7 @@ "system_prompt", "tools.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json index 40927fb41..804acb622 100644 --- a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json @@ -21,8 +21,7 @@ "tools.toolsets.enabled", "tools.toolsets.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 974fc9e1a..4496f902e 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -29,7 +29,7 @@ pub struct FabricConfig { pub metadata: MetadataConfig, /// Harness selection and harness-specific settings. pub harness: HarnessConfig, - /// Model aliases. + /// Named model roles. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub models: BTreeMap, /// Portable system instructions for the selected harness. @@ -402,9 +402,6 @@ pub enum AdapterConfigField { /// Harness-native skills. #[serde(rename = "skills")] Skills, - /// Typed telemetry configuration. Exact support remains declared by telemetry provider. - #[serde(rename = "telemetry")] - Telemetry, } /// Adapter telemetry support. @@ -1124,12 +1121,6 @@ fn validate_config(config: &FabricConfig) -> Result<()> { "must be a non-empty string", ); } - if model.settings.contains_key("base_url") { - return invalid_config( - format!("models.{role}.settings.base_url"), - "is not supported; use models..base_url", - ); - } } if let Some(environment) = &config.environment { for name in environment.env.keys() { @@ -2233,37 +2224,6 @@ mod tests { } } - #[test] - fn model_settings_rejects_normalized_base_url_alias() { - let mut config = typed_config("nvidia.fabric.hermes"); - config.models.insert( - "default".to_string(), - ModelConfig { - provider: "nvidia".to_string(), - model: "nvidia/test".to_string(), - temperature: None, - api_key_env: None, - base_url: None, - settings: serde_json::Map::from_iter([( - "base_url".to_string(), - Value::String("https://legacy.example/v1".to_string()), - )]), - extensions: BTreeMap::new(), - }, - ); - - let error = - resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-model-alias")) - .expect_err("normalized aliases must be rejected"); - - assert!(matches!( - error, - FabricError::InvalidConfig { field, reason } - if field == "models.default.settings.base_url" - && reason.contains("models..base_url") - )); - } - #[test] fn sole_named_model_is_selected_without_forcing_default_role() { let mut config = typed_config("nvidia.fabric.claude"); diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index 0360d33fb..12775817a 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -2392,10 +2392,7 @@ mod tests { use std::fs; use super::*; - use crate::config::{ - CapabilityKind, CapabilityRoute, CapabilityTarget, ResolveContext, - resolve_run_plan_from_config, - }; + use crate::config::{ResolveContext, resolve_run_plan_from_config}; fn local_host_plan(mode: &str) -> (PathBuf, RunPlan) { local_host_plan_with_relay(mode, false) @@ -2550,26 +2547,6 @@ for line in sys.stdin: TEST_STOPPED_AGENTS.lock().expect("stop tracker").clone() } - #[test] - fn runtime_validation_reports_canonical_mcp_field() { - let (root, mut plan) = local_host_plan("success"); - plan.capability_plan.routes.push(CapabilityRoute { - kind: CapabilityKind::Mcp, - name: "github".to_string(), - target: CapabilityTarget::Unsupported, - reason: "unsupported test route".to_string(), - }); - - let error = start_runtime(&plan).expect_err("unsupported MCP route must fail"); - assert!(matches!( - error, - FabricError::AdapterCompatibility { field, .. } - if field == "mcp.servers.github" - )); - - let _ = fs::remove_dir_all(root); - } - #[test] fn local_host_reuses_one_process_and_stops_idempotently() { let (root, plan) = local_host_plan("success"); diff --git a/docs/reference/api/python-library-reference/index.md b/docs/reference/api/python-library-reference/index.md index 8ba32251b..cc5cd1ff6 100644 --- a/docs/reference/api/python-library-reference/index.md +++ b/docs/reference/api/python-library-reference/index.md @@ -28,7 +28,7 @@ SPDX-License-Identifier: Apache-2.0 */} - [`models.McpConfig`](./nemo_fabric.models.md#class-mcpconfig): MCP capability configuration. - [`models.McpServerConfig`](./nemo_fabric.models.md#class-mcpserverconfig): MCP server configuration. - [`models.MetadataConfig`](./nemo_fabric.models.md#class-metadataconfig): Human-readable agent identity. -- [`models.ModelConfig`](./nemo_fabric.models.md#class-modelconfig): Model alias configuration. +- [`models.ModelConfig`](./nemo_fabric.models.md#class-modelconfig): Configuration for one model role. - [`models.RelayAtifConfig`](./nemo_fabric.models.md#class-relayatifconfig): NeMo Relay ATIF export configuration. - [`models.RelayAtofConfig`](./nemo_fabric.models.md#class-relayatofconfig): NeMo Relay ATOF export configuration. - [`models.RelayAtofFileSinkConfig`](./nemo_fabric.models.md#class-relayatoffilesinkconfig): NeMo Relay ATOF file sink configuration. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.client.md b/docs/reference/api/python-library-reference/nemo_fabric.client.md index 87dd7cee9..c719a3ead 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.client.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.client.md @@ -15,7 +15,6 @@ Native Python client for resolving and running NeMo Fabric agents. ## class `Fabric` - Primary Python entrypoint for NeMo Fabric. Every lifecycle method accepts a complete, typed ``FabricConfig`` plus an optional ``base_dir`` used to resolve relative paths. Compose variants in Python before calling the SDK. The ``doctor()``, ``plan()``, and ``run()`` results are typed, read-only mapping models. ``start_runtime()`` returns an active ``Runtime`` handle. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.errors.md b/docs/reference/api/python-library-reference/nemo_fabric.errors.md index b955cca5d..44bad6a22 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.errors.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.errors.md @@ -15,7 +15,6 @@ Public exception hierarchy for the NeMo Fabric Python SDK. ## class `FabricError` - Base class for structured SDK-level Fabric errors. Catch this type to handle any SDK failure while preserving machine-readable stage, code, retryability, and detail fields. @@ -62,7 +61,6 @@ Initialize a structured Fabric exception. ## class `FabricConfigError` - Invalid SDK input, request shape, factory, or resolved config. @@ -98,7 +96,6 @@ Initialize a structured Fabric exception. ## class `FabricRuntimeError` - Failure while starting, invoking, stopping, or otherwise driving a runtime. @@ -134,7 +131,6 @@ Initialize a structured Fabric exception. ## class `FabricStateError` - Operation rejected because a local runtime is in the wrong state. @@ -170,7 +166,6 @@ Initialize a structured Fabric exception. ## class `FabricCapabilityError` - Operation rejected by resolved runtime capabilities or implementation status. @@ -206,7 +201,6 @@ Initialize a structured Fabric exception. ## class `FabricNativeUnavailableError` - SDK call requires the PyO3 extension, but it is not installed or importable. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index bd09c0d86..e705b75a4 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -17,7 +17,6 @@ The Rust core remains the source of truth for persisted schema snapshots. These ## class `FabricBaseModel` - Base class for SDK-facing Pydantic models. @@ -78,7 +77,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `MetadataConfig` - Human-readable agent identity. @@ -139,7 +137,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `HarnessConfig` - Harness adapter selection plus adapter-owned settings. @@ -200,7 +197,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RuntimeConfig` - Runtime input/output contract. @@ -261,7 +257,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `EnvironmentConfig` - Execution environment configuration supplied by the consumer. ``provider`` selects the environment implementation. ``workspace`` is the path visible to the harness, while ``artifacts`` is the provider-specific output location. ``settings`` configures the selected provider; ``connection`` describes how Fabric reaches an existing environment; and ``metadata`` carries consumer-owned values that Fabric does not interpret. ``ownership`` identifies who tears the environment down, and ``control_location`` identifies whether Fabric control code runs inside or outside it. @@ -324,8 +319,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `ModelConfig` - -Model alias configuration. +Configuration for one model role. --- @@ -385,7 +379,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `SkillConfig` - Skill capability configuration. @@ -468,7 +461,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `McpServerConfig` - MCP server configuration. @@ -529,7 +521,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `McpConfig` - MCP capability configuration. @@ -618,7 +609,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayConfigPolicy` - NeMo Relay config validation policy. @@ -679,7 +669,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofFileSinkConfig` - NeMo Relay ATOF file sink configuration. @@ -740,7 +729,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofStreamSinkConfig` - NeMo Relay ATOF stream sink configuration. @@ -801,7 +789,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtofConfig` - NeMo Relay ATOF export configuration. @@ -862,7 +849,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayS3StorageConfig` - NeMo Relay ATIF S3 storage configuration. @@ -923,7 +909,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayHttpStorageConfig` - NeMo Relay ATIF HTTP storage configuration. @@ -984,7 +969,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayAtifConfig` - NeMo Relay ATIF export configuration. @@ -1045,7 +1029,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayOtlpConfig` - NeMo Relay OTLP export configuration for OpenTelemetry/OpenInference. @@ -1106,7 +1089,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayObservabilityConfig` - NeMo Relay observability component configuration. @@ -1167,7 +1149,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayComponentConfig` - Generic NeMo Relay plugin component configuration. @@ -1228,7 +1209,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RelayConfig` - First-class NeMo Relay integration configuration. @@ -1289,7 +1269,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `TelemetryProviderConfig` - Provider-specific telemetry configuration. @@ -1350,7 +1329,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `TelemetryConfig` - Telemetry configuration. @@ -1566,7 +1544,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `FabricConfig` - SDK-facing typed Fabric agent configuration. Fabric-owned fields apply uniformly. Adapter-translated fields are checked against the selected descriptor; see the normalized configuration compatibility table in ``docs/sdk/python.mdx``. @@ -1721,7 +1698,6 @@ Return a detached mapping matching the Rust ``FabricConfig`` schema. ## class `RunRequest` - One validated Fabric invocation request. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.runtime.md b/docs/reference/api/python-library-reference/nemo_fabric.runtime.md index 751e48418..e0e1474f2 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.runtime.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.runtime.md @@ -15,7 +15,6 @@ Runtime lifecycle support for the Fabric Python SDK. ## class `RuntimeStatus` - Lifecycle state of a runtime. ``ACTIVE`` accepts invocations, ``STOPPED`` has released its runtime, and ``FAILED`` records a lifecycle failure that prevents further invocations but still permits cleanup. @@ -28,7 +27,6 @@ Lifecycle state of a runtime. ## class `Runtime` - One logical, stateful harness execution. Create runtimes with ``Fabric.start_runtime()`` rather than calling the constructor. A runtime serializes invocations and preserves adapter-owned harness state across turns. Use it as an asynchronous context manager to stop the runtime on exit. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.types.md b/docs/reference/api/python-library-reference/nemo_fabric.types.md index 24931a35c..5de0e451d 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.types.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.types.md @@ -15,7 +15,6 @@ Public data contracts for the NeMo Fabric Python SDK. ## class `AdapterInfo` - Resolved adapter identity attached to a run plan. @@ -85,7 +84,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RuntimeCapabilities` - Operations declared by the resolved runtime and adapter. Capabilities describe what the selected runtime can support; callers should still expect a capability-specific error when a transport is modeled but not implemented. @@ -158,7 +156,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunPlan` - Immutable execution plan produced before a runtime is started. @@ -229,7 +226,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `DoctorCheck` - One diagnostic check in a ``DoctorReport``. @@ -299,7 +295,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `DoctorReport` - Aggregate preflight diagnostics for a resolved run plan. @@ -368,7 +363,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ErrorInfo` - Structured failure returned inside a normalized ``RunResult``. @@ -439,7 +433,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ArtifactRef` - Reference to one artifact produced by a run. @@ -510,7 +503,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `ArtifactManifest` - Normalized collection of artifacts produced by a run. @@ -578,7 +570,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `TelemetryRef` - Reference to external or persisted telemetry for a run. @@ -649,7 +640,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `FabricEvent` - One normalized lifecycle or invocation event. @@ -720,7 +710,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RuntimeHandle` - Opaque identity and binding for one started runtime. Applications should treat ``runtime_binding`` as opaque. Fabric validates the handle against the run plan before invocation or shutdown. @@ -795,7 +784,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunOutput` - Normalized adapter output. ``response`` is a known adapter response field whose value follows the core Fabric JSON contract. Other keys are adapter-specific extensions. @@ -864,7 +852,6 @@ Return a detached, JSON-compatible mapping for serialization. ## class `RunResult` - Normalized terminal result from one Fabric invocation. The model is both attribute-accessible and mapping-compatible. A harness failure can be represented by ``status`` and ``error`` without raising when the adapter successfully returns a normalized result. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx index 641bb4e86..1ab490455 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx @@ -21,7 +21,6 @@ pub enum AdapterConfigField { BlockedToolsets, Mcp, Skills, - Telemetry, } ``` @@ -89,12 +88,6 @@ Harness-native MCP servers. Harness-native skills. -### `Telemetry` - -
- -Typed telemetry configuration. Exact support remains declared by telemetry provider. - ## Trait Implementations ### `impl Clone for AdapterConfigField` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx index 95f888241..3b779c7e6 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx @@ -31,7 +31,7 @@ Harness selection and harness-specific settings. ### `models: BTreeMap` -Model aliases. +Named model roles. ### `system_prompt: Option` diff --git a/examples/harbor/swebench/adapters/claude/fabric-adapter.json b/examples/harbor/swebench/adapters/claude/fabric-adapter.json index 5ea209bb2..688f06586 100644 --- a/examples/harbor/swebench/adapters/claude/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/claude/fabric-adapter.json @@ -14,8 +14,7 @@ "max_turns", "tools.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json index 40927fb41..804acb622 100644 --- a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json @@ -21,8 +21,7 @@ "tools.toolsets.enabled", "tools.toolsets.blocked", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/examples/notebooks/01_quickstart.ipynb b/examples/notebooks/01_quickstart.ipynb index ac9b19b22..c8fbed9a6 100644 --- a/examples/notebooks/01_quickstart.ipynb +++ b/examples/notebooks/01_quickstart.ipynb @@ -171,7 +171,7 @@ "- `metadata` -- the agent's identity.\n", "- `harness` -- which harness to use (`adapter_id`) plus harness-specific\n", " `settings`; portable behavior stays in typed Fabric fields.\n", - "- `models` -- named model aliases; `api_key_env` names the credential.\n", + "- `models` -- named model roles; `api_key_env` names the credential.\n", "- `runtime` -- the input/output contract and where artifacts are written." ] }, @@ -213,7 +213,7 @@ " \"reasoning_config\": {\"effort\": \"none\"},\n", " },\n", " ),\n", - " # Named model aliases. `api_key_env` names the environment variable that\n", + " # Named model roles. `api_key_env` names the environment variable that\n", " # holds the credential, so the key itself never lives in the config.\n", " models={\n", " \"default\": ModelConfig(\n", diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index d6b64c06d..9f589f485 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -178,7 +178,7 @@ def _validate_env_names(cls, value: dict[str, str]) -> dict[str, str]: class ModelConfig(FabricBaseModel): - """Model alias configuration.""" + """Configuration for one model role.""" provider: str = Field(min_length=1) model: str = Field(min_length=1) @@ -208,16 +208,6 @@ def _validate_base_url(cls, value: str | None) -> str | None: raise ValueError("base_url must be a non-empty string") return value - @field_validator("settings") - @classmethod - def _reject_normalized_aliases(cls, value: dict[str, Any]) -> dict[str, Any]: - if "base_url" in value: - raise ValueError( - "models..settings.base_url is not supported; use " - "models..base_url" - ) - return value - class SkillConfig(FabricBaseModel): """Skill capability configuration.""" diff --git a/schemas/adapter-descriptor.schema.json b/schemas/adapter-descriptor.schema.json index 78f2d01e1..85cb0bae4 100644 --- a/schemas/adapter-descriptor.schema.json +++ b/schemas/adapter-descriptor.schema.json @@ -52,11 +52,6 @@ "const": "skills", "description": "Harness-native skills.", "type": "string" - }, - { - "const": "telemetry", - "description": "Typed telemetry configuration. Exact support remains declared by telemetry provider.", - "type": "string" } ] }, diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 957f281e7..17cd86c47 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -1057,7 +1057,7 @@ "additionalProperties": { "$ref": "#/$defs/ModelConfig" }, - "description": "Model aliases.", + "description": "Named model roles.", "type": "object" }, "relay": { diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index c03909c96..7ef900933 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -52,11 +52,6 @@ "const": "skills", "description": "Harness-native skills.", "type": "string" - }, - { - "const": "telemetry", - "description": "Typed telemetry configuration. Exact support remains declared by telemetry provider.", - "type": "string" } ] }, @@ -607,7 +602,7 @@ "additionalProperties": { "$ref": "#/$defs/ModelConfig" }, - "description": "Model aliases.", + "description": "Named model roles.", "type": "object" }, "relay": { diff --git a/scripts/generate_api_docs.sh b/scripts/generate_api_docs.sh index 9b25cc377..685a61adb 100755 --- a/scripts/generate_api_docs.sh +++ b/scripts/generate_api_docs.sh @@ -39,8 +39,8 @@ perl -0pi -e 's/\A\s+//' "$out"/*.md # lazydocs nests properties at h4 directly under h2 class sections. Flatten # those headings to h3 so generated pages satisfy markdown heading order. perl -pi -e 's/^#### (property<\/kbd>)/### $1/' "$out"/*.md -# lazydocs emits some class headings without a separating blank line. -perl -0pi -e 's/(^## class<\/kbd> `[^`]+`\n)(?!\n)/$1\n/gm' "$out"/*.md +# lazydocs emits these class headings without a separating blank line. +perl -0pi -e 's/(^## class<\/kbd> `(?:ToolsConfig|ToolsetConfig)`\n)(?!\n)/$1\n/gm' "$out/nemo_fabric.models.md" add_frontmatter() { local file="$1" diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index 1c4561667..ee0d51250 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -169,22 +169,6 @@ def test_normalized_agent_and_toolset_accessors(): assert common_utils.blocked_toolsets(payload) == ["browser"] -def test_environment_payload_uses_normalized_config_without_runtime_context(): - payload = { - "config": { - "environment": { - "workspace": "/config-workspace", - "env": {"VISIBLE": "yes"}, - } - } - } - - assert common_utils.environment_payload(payload) == { - "workspace": "/config-workspace", - "env": {"VISIBLE": "yes"}, - } - - def test_payload_accessors_use_canonical_plan_fields(tmp_path): base_dir = str(tmp_path / "outer") payload = { diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index 27efcfaac..777b2b861 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -95,7 +95,6 @@ def test_claude_descriptor_is_narrow_and_versioned(): "tools.blocked", "mcp", "skills", - "telemetry", ], }, "telemetry": { @@ -374,43 +373,6 @@ def test_build_options_maps_blocked_tools_to_disallowed_tools(claude_payload): assert options.disallowed_tools == ["Bash", "WebFetch"] -@pytest.mark.parametrize( - ("name", "normalized_field"), - [ - ("model_name", "FabricConfig.models"), - ("base_url", "FabricConfig.models..base_url"), - ("cwd", "FabricConfig.environment.workspace"), - ("env", "FabricConfig.environment.env"), - ("system_prompt", "FabricConfig.system_prompt"), - ("max_turns", "FabricConfig.max_turns"), - ("timeout_seconds", "FabricConfig.runtime.timeout_seconds"), - ("tools", "FabricConfig.tools"), - ("disallowed_tools", "FabricConfig.tools.blocked"), - ("mcp_servers", "FabricConfig.mcp"), - ("skills", "FabricConfig.skills"), - ], -) -def test_build_options_rejects_normalized_capabilities_in_harness_settings( - claude_payload, name, normalized_field -): - claude_payload["config"]["harness"]["settings"][name] = [] - - with pytest.raises( - adapter.AdapterConfigError, match=normalized_field.replace(".", r"\.") - ): - adapter.build_options(claude_payload) - - -@pytest.mark.parametrize("setting", ["cli_path", "nemo_relay_command"]) -def test_build_options_rejects_removed_runtime_settings(claude_payload, setting): - claude_payload["config"]["harness"]["settings"][setting] = "custom" - - with pytest.raises( - adapter.AdapterConfigError, match=rf"harness\.settings\.{setting}" - ): - adapter.build_options(claude_payload) - - def test_build_options_rejects_skill_path_without_skill_manifest(claude_payload): skill_path = Path(claude_payload["capability_plan"]["native"]["skill_paths"][0]) (skill_path / "SKILL.md").unlink() @@ -457,10 +419,7 @@ def test_build_options_uses_nvidia_provider_endpoint_and_default_credential( options = adapter.build_options(claude_payload) - assert ( - options.env["ANTHROPIC_BASE_URL"] - == "https://integrate.api.nvidia.com" - ) + assert options.env["ANTHROPIC_BASE_URL"] == "https://integrate.api.nvidia.com" assert options.env["ANTHROPIC_API_KEY"] == "nvidia-secret" diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index 84604dd3d..32d1f36fc 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -926,56 +926,6 @@ async def block(): assert client.closed is True -@pytest.mark.parametrize( - "setting", ["codex_command", "codex_args", "codex_profile", "skip_git_repo_check"] -) -def test_cli_only_settings_are_rejected(codex_payload, setting): - codex_payload["config"]["harness"]["settings"][setting] = "legacy" - - error = runtime_start_error(codex_payload) - - assert error.code == "codex_invalid_configuration" - assert setting in error.message - - -@pytest.mark.parametrize( - ("setting", "normalized_field"), - [ - ("model", "FabricConfig.models"), - ("base_url", "FabricConfig.models..base_url"), - ("env", "FabricConfig.environment.env"), - ("timeout_seconds", "FabricConfig.runtime.timeout_seconds"), - ("mcp_servers", "FabricConfig.mcp"), - ("skills", "FabricConfig.skills"), - ], -) -def test_normalized_capabilities_reject_harness_settings( - codex_payload, mock_codex, setting, normalized_field -): - codex_payload["config"]["harness"]["settings"][setting] = {} - - error = runtime_start_error(codex_payload) - - assert error.code == "codex_invalid_configuration" - assert normalized_field in error.message - mock_codex.assert_not_called() - - -@pytest.mark.parametrize( - "setting", ["codex_bin", "nemo_relay_command", "service_name"] -) -def test_removed_runtime_settings_are_rejected( - codex_payload, mock_codex, setting -): - codex_payload["config"]["harness"]["settings"][setting] = "custom" - - error = runtime_start_error(codex_payload) - - assert error.code == "codex_invalid_configuration" - assert f"harness.settings.{setting}" in error.message - mock_codex.assert_not_called() - - def test_adapter_rejects_structured_input(codex_payload): codex_payload["request"]["input"] = { "messages": [{"role": "user", "content": "Inspect the change."}] @@ -1003,7 +953,6 @@ def test_descriptor_has_no_codex_binary_requirement(): "system_prompt", "mcp", "skills", - "telemetry", ] assert "requirements" not in descriptor diff --git a/tests/adapters/test_deepagents.py b/tests/adapters/test_deepagents.py index 9e5313468..2b72ee3c9 100644 --- a/tests/adapters/test_deepagents.py +++ b/tests/adapters/test_deepagents.py @@ -13,7 +13,6 @@ import importlib.machinery import os -import re import sys import types from collections.abc import AsyncIterator @@ -411,9 +410,7 @@ async def test_native_telemetry_exports_without_artifacts( assert fake_relay["wrapped"] assert fake_relay["plugin_open"] - assert fake_relay["plugin_configs"] == [ - payload["telemetry_plan"]["native_config"] - ] + assert fake_relay["plugin_configs"] == [payload["telemetry_plan"]["native_config"]] assert output["telemetry"] == { "enabled": True, "provider": "native", @@ -1097,28 +1094,6 @@ async def test_openai_compatible_provider_requires_api_key_env(tmp_path, make_pa await adapter.DeepAgentsRuntime().start(lifecycle_start_payload(payload)) -def test_removed_state_dir_setting_is_rejected(): - with pytest.raises( - adapter.AdapterConfigError, match=r"harness\.settings\.state_dir" - ): - adapter._validate_settings_boundary({"state_dir": "./state"}) - - -@pytest.mark.parametrize( - ("setting", "target"), - sorted(adapter.NORMALIZED_SETTING_FIELDS.items()), -) -def test_normalized_fields_are_rejected_in_harness_settings( - setting: str, - target: str, -): - with pytest.raises( - adapter.AdapterConfigError, - match=rf"harness\.settings\.{setting}.*{re.escape(target)}", - ): - adapter._validate_settings_boundary({setting: "legacy"}) - - def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 2c2f837b7..acef2c446 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -9,7 +9,6 @@ import inspect import json import os -import re import sys from pathlib import Path from types import ModuleType, SimpleNamespace @@ -250,35 +249,6 @@ def test_default_max_iterations_matches_hermes_library_default(): assert adapter.DEFAULT_MAX_ITERATIONS == hermes_default -@pytest.mark.parametrize( - ("setting", "target"), - [ - ("api_key_env", "FabricConfig.models..api_key_env"), - ("base_url", "FabricConfig.models..base_url"), - ("disabled_toolsets", "FabricConfig.tools.toolsets.blocked"), - ("enabled_toolsets", "FabricConfig.tools.toolsets.enabled"), - ("env", "FabricConfig.environment.env"), - ("max_iterations", "FabricConfig.max_turns"), - ("max_turns", "FabricConfig.max_turns"), - ("model", "FabricConfig.models"), - ("model_name", "FabricConfig.models..model"), - ("provider", "FabricConfig.models..provider"), - ("system_prompt", "FabricConfig.system_prompt"), - ("temperature", "FabricConfig.models..temperature"), - ("workspace", "FabricConfig.environment.workspace"), - ], -) -def test_normalized_fields_are_rejected_in_harness_settings( - setting: str, - target: str, -): - with pytest.raises( - ValueError, - match=rf"harness\.settings\.{setting}.*{re.escape(target)}", - ): - adapter._validate_settings_boundary({setting: "legacy"}) - - def test_build_hermes_config_omits_max_turns_when_fabric_limit_unset(): # When max_turns is unset the config layer must leave agent.max_turns # absent so Hermes applies its own default rather than a starving override. @@ -366,9 +336,7 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( "agent_name": "matrix-agent", "base_dir": str(tmp_path), "config": { - "harness": { - "settings": {} - }, + "harness": {"settings": {}}, "tools": {"toolsets": {"enabled": ["git", "shell"]}}, "models": { "review": { @@ -657,23 +625,6 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( ) -@pytest.mark.parametrize( - "setting", - ["hermes_home", "insert_reasoning", "toolset_platform"], -) -def test_removed_runtime_settings_are_rejected(setting): - with pytest.raises(ValueError, match=rf"harness\.settings\.{setting}"): - adapter._validate_settings_boundary({setting: "value"}) - - -def test_terminal_backend_reports_local_only_support(): - with pytest.raises( - ValueError, - match=r"harness\.settings\.terminal_backend.*only local terminal execution", - ): - adapter._validate_settings_boundary({"terminal_backend": "ssh"}) - - def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json index 118d54ba9..e485a2de8 100644 --- a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json +++ b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/fabric-adapter.json @@ -20,8 +20,7 @@ "models", "models.temperature", "mcp", - "skills", - "telemetry" + "skills" ] }, "telemetry": { diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index 502552005..84d9a27f8 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -254,23 +254,6 @@ def test_typed_config_serializes_normalized_execution_fields(): ) with pytest.raises(ValidationError, match="both enabled and blocked"): ToolsetConfig(enabled=["browser"], blocked=["browser"]) - with pytest.raises( - ValidationError, - match=r"models\.\.settings\.base_url", - ): - FabricConfig( - metadata=MetadataConfig(name="demo"), - harness=HarnessConfig(adapter_id="test.fabric.shim"), - models={ - "default": { - "provider": "nvidia", - "model": "nvidia/test", - "settings": { - "base_url": "https://legacy.example/v1", - }, - } - }, - ) def test_run_plan_config_block_tools_emits_canonical_shape(): diff --git a/tests/python/test_typed_config.py b/tests/python/test_typed_config.py index 82f5f8e2d..9a63617e7 100644 --- a/tests/python/test_typed_config.py +++ b/tests/python/test_typed_config.py @@ -19,11 +19,8 @@ from pathlib import Path from shutil import copytree -import pytest - from nemo_fabric import Fabric from nemo_fabric import FabricConfig -from nemo_fabric import FabricConfigError from nemo_fabric import RunRequest from nemo_fabric import RunResult @@ -72,7 +69,6 @@ def _shim_adapter_config() -> FabricConfig: config["harness"] = { "adapter_id": "test.fabric.hermes_shim", "resolution": "preinstalled", - "settings": {}, } config["models"] = { "default": {"provider": "test", "model": "test-model", "temperature": 0.0} From 7efee3d5b2c6d2c05150c6bc8caff9b31815bba9 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 12:44:12 -0700 Subject: [PATCH 07/21] fix: make adapter provider routing explicit Signed-off-by: Ajay Thorve --- README.md | 4 +- adapters/README.md | 6 +- adapters/claude/README.md | 24 ++-- .../nemo_fabric_adapters/claude/adapter.py | 63 ++++++--- adapters/codex/README.md | 25 ++-- .../src/nemo_fabric_adapters/codex/adapter.py | 71 ++++++---- .../common/relay_gateway.py | 21 ++- .../src/nemo_fabric_adapters/common/utils.py | 23 ++- adapters/deepagents/README.md | 30 ++-- .../deepagents/adapter.py | 47 ++++--- adapters/hermes/fabric-adapter.json | 6 +- .../nemo_fabric_adapters/hermes/adapter.py | 6 +- .../adapters/hermes/fabric-adapter.json | 6 +- crates/fabric-core/src/config.rs | 14 +- docs/integrations/claude.mdx | 15 +- docs/integrations/codex.mdx | 29 ++-- .../api/python-library-reference/index.md | 6 +- .../nemo_fabric.client.md | 2 +- .../nemo_fabric.models.md | 10 +- .../nemo-fabric-core/config/index.mdx | 2 +- .../config/struct-environmentconfig.mdx | 2 + .../config/struct-toolsconfig.mdx | 2 +- .../config/struct-toolsetconfig.mdx | 8 +- .../config/struct-toolsplan.mdx | 2 +- docs/sdk/python.mdx | 11 +- .../adapters/hermes/fabric-adapter.json | 6 +- python/src/nemo_fabric/client.py | 6 +- python/src/nemo_fabric/models.py | 66 +++++++-- schemas/agent.schema.json | 10 +- schemas/run-plan.schema.json | 12 +- skills/nemo-fabric-integrate/SKILL.md | 38 ++++- .../references/config-mapping.md | 5 +- tests/adapters/test_adapaters_common_utils.py | 69 ++++----- .../test_adapters_common_relay_gateway.py | 18 +-- tests/adapters/test_claude_adapter.py | 97 ++++++++++--- tests/adapters/test_codex_adapter.py | 132 +++++++++++------- tests/adapters/test_deepagents.py | 14 ++ tests/adapters/test_hermes_adapter.py | 28 ++++ .../hermes_shim/adapter.py | 5 +- 39 files changed, 601 insertions(+), 340 deletions(-) diff --git a/README.md b/README.md index 841111af4..8e78f9059 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,8 @@ references. | Agent harness | Choose it for | Model ecosystem | Key capabilities | Observability | | --- | --- | --- | --- | --- | -| Claude | Claude-native coding and tool-use workflows | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | Tool guardrails, MCP, skills, and persistent Claude sessions | NeMo Relay | -| Codex | Codex-native coding workflows | OpenAI and NVIDIA-hosted Responses-compatible models | MCP, skills, and persistent Codex threads | NeMo Relay and native OpenTelemetry | +| Claude | Claude-native coding and tool-use workflows | Native Anthropic or a configured Anthropic Messages-compatible provider | Tool guardrails, MCP, skills, and persistent Claude sessions | NeMo Relay | +| Codex | Codex-native coding workflows | Native OpenAI or a configured Responses-compatible provider | MCP, skills, and persistent Codex threads | NeMo Relay and native OpenTelemetry | | LangChain Deep Agents | Composable LangChain and LangGraph agents | LangChain model providers | Built-in and MCP tools, guardrails, skills, and local subagents | NeMo Relay and native OpenTelemetry/OpenInference | | Hermes Agent | Hermes Agent workflows with custom model endpoints | Configurable provider, model, and base URL | Toolsets, guardrails, MCP, skills, and persistent conversation history | NeMo Relay | diff --git a/adapters/README.md b/adapters/README.md index 4a74b59d6..930d8636e 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -45,8 +45,8 @@ provider should expose more precise provenance. | Agent Harness | Models | Tool Policy | MCP | Skills | Subagents | | --- | --- | --- | --- | --- | --- | -| [Claude](claude/README.md) | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | `tools.blocked` maps to Claude `disallowed_tools`; toolsets unsupported | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | -| [Codex](codex/README.md) | OpenAI and NVIDIA-hosted Responses-compatible models | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | +| [Claude](claude/README.md) | Native Anthropic or a configured Anthropic Messages-compatible provider | `tools.blocked` maps to Claude `disallowed_tools`; toolsets unsupported | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | +| [Codex](codex/README.md) | Native OpenAI or a configured Responses-compatible provider | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | | [LangChain Deep Agents](deepagents/README.md) | LangChain model providers | `tools.blocked` middleware covers built-ins, MCP, and local subagents; toolsets unsupported | Normalized through `langchain-mcp-adapters` | Normalized | Constrained declarative local delegation | | [Hermes Agent](hermes/README.md) | Configurable provider, model, and base URL | `tools.toolsets` maps to Hermes toolset controls; per-tool blocking unsupported | Normalized | Normalized | Not exposed | @@ -73,7 +73,7 @@ are grouped because their support does not vary by adapter. | `metadata.name`, `.description` | Core | Core | Core | Core | | `harness.adapter_id`, `.resolution` | Core | Core | Core | Core | | `harness.settings` | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | -| `models..provider` | `anthropic`, `nvidia` | `openai`, `nvidia` | Dynamic LangChain provider | Dynamic Hermes provider | +| `models..provider` | `anthropic` uses native auth; custom names require an Anthropic Messages-compatible `base_url` and `api_key_env` | `openai` uses native auth; custom names require a Responses-compatible `base_url` and `api_key_env` | Dynamic LangChain provider; custom OpenAI-compatible endpoints require `base_url` and `api_key_env` | Dynamic Hermes provider | | `models..model` | Yes | Yes | Yes | Yes | | `models..api_key_env` | Yes | Yes | Yes | Yes | | `models..base_url` | Yes | Yes | Yes | Yes | diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 42d5b924f..b5396b5f1 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -34,12 +34,13 @@ bearer credential, `ANTHROPIC_API_KEY` for a static API credential, or Anthropic Workload Identity Federation (WIF) for production and CI workloads that should not store a long-lived API key. -When the selected model provider is `nvidia`, the adapter reads the selected -model's credential from `api_key_env` (default: `NVIDIA_API_KEY`) and translates -the configured NVIDIA `/v1` endpoint into the host URL expected by Claude Code. -`models..base_url` overrides the public NVIDIA API Catalog endpoint. Set -it explicitly for a frontier or self-hosted endpoint. This request-scoped -mapping does not change the parent environment. +The native `anthropic` provider can use any Claude authentication mode above +without an explicit endpoint. For another provider name, configure both +`models..api_key_env` and `models..base_url`. The endpoint must +implement the Anthropic Messages protocol; the adapter maps the named +credential and endpoint into the environment expected by Claude Code. Provider +names identify configuration; the adapter does not maintain a provider +allowlist. The runtime-scoped mapping does not change the parent environment. The adapter forwards the Anthropic profile and federation environment variables that Claude Code and the Claude Agent SDK consume. This includes @@ -82,9 +83,9 @@ hosting is adapter-declared; consumers do not configure a runtime strategy in Configure portable capabilities through the normalized `FabricConfig` fields: -- `models` selects the Claude model. The adapter accepts the native `anthropic` - provider and NVIDIA-hosted Anthropic Messages-compatible models through the - `nvidia` provider. `models..base_url` selects an explicit endpoint. +- `models` selects the Claude model. The native `anthropic` provider retains + Claude authentication and endpoint discovery. Any other provider name must + configure an Anthropic Messages-compatible `base_url` and `api_key_env`. - `system_prompt` supplies the Claude system instructions. - `max_turns` sets the Claude turn limit. - `runtime.timeout_seconds` sets the Fabric invocation deadline. @@ -122,8 +123,9 @@ config.enable_relay( For each Relay-enabled Claude runtime, NeMo Fabric starts one `nemo-relay` gateway, waits for its health endpoint, and stops it with the runtime. NeMo Fabric passes the gateway URL to the connected Claude Code process through `ANTHROPIC_BASE_URL` -and `NEMO_RELAY_GATEWAY_URL`. It also stages a runtime-scoped Claude plugin that -forwards lifecycle hooks with `nemo-relay hook-forward claude`. +and `NEMO_RELAY_GATEWAY_URL`, and passes the selected explicit model endpoint to +the gateway as its Anthropic upstream. It also stages a runtime-scoped Claude +plugin that forwards lifecycle hooks with `nemo-relay hook-forward claude`. `Fabric.run(...)` starts the same runtime, invokes it once, and stops it, so the gateway has the same lifecycle as that single invocation. diff --git a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py index 21f043ccc..06effaf4e 100644 --- a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py +++ b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py @@ -208,10 +208,10 @@ def selected_model(payload: dict[str, Any]) -> str | None: if value is None: return None provider = model_config.get("provider") - if provider not in {"anthropic", "nvidia"}: + if not isinstance(provider, str) or not provider: raise AdapterConfigError( "claude_invalid_configuration", - "selected model provider must be anthropic or nvidia for the Claude adapter", + "selected model provider must be a non-empty string", ) if not isinstance(value, str) or not value: raise AdapterConfigError( @@ -220,14 +220,29 @@ def selected_model(payload: dict[str, Any]) -> str | None: return value.removeprefix("anthropic/") if provider == "anthropic" else value +def _anthropic_base_url(model: dict[str, Any]) -> str | None: + base_url = common_utils.get_base_url(model) + if base_url is None: + return None + if not isinstance(base_url, str) or not base_url: + raise AdapterConfigError( + "claude_invalid_configuration", + "selected model base_url must be a non-empty string", + ) + base_url = base_url.rstrip("/") + return ( + base_url.removesuffix("/v1") + if model.get("provider") != "anthropic" + else base_url + ) + + def _model_environment( payload: dict[str, Any], environment: dict[str, str] ) -> dict[str, str]: model = _selected_model_config(payload) provider = model.get("provider") api_key_env = model.get("api_key_env") - if provider == "nvidia" and api_key_env is None: - api_key_env = "NVIDIA_API_KEY" if not isinstance(api_key_env, str) or not api_key_env: if api_key_env is None: api_key = None @@ -238,29 +253,30 @@ def _model_environment( ) else: api_key = environment.get(api_key_env) or os.environ.get(api_key_env) + if provider != "anthropic" and api_key_env is None: + raise AdapterConfigError( + "claude_invalid_configuration", + "selected model api_key_env is required for a custom " + "Anthropic Messages-compatible provider", + ) if api_key_env is not None and not api_key: raise AdapterConfigError( "claude_invalid_configuration", f"{api_key_env} is required for the selected model provider", ) - base_url = common_utils.get_base_url(model) - if provider == "nvidia" and not base_url: + base_url = _anthropic_base_url(model) + if provider != "anthropic" and not base_url: raise AdapterConfigError( "claude_invalid_configuration", - "selected model base_url is required for the NVIDIA model provider", + "selected model base_url is required for a custom " + "Anthropic Messages-compatible provider", ) values: dict[str, str] = {} if api_key: values["ANTHROPIC_API_KEY"] = api_key if base_url: - # Claude Code appends the Anthropic API version path itself, while - # Fabric's NVIDIA endpoint includes it for OpenAI-compatible clients. - values["ANTHROPIC_BASE_URL"] = ( - base_url.rstrip("/").removesuffix("/v1") - if provider == "nvidia" - else base_url.rstrip("/") - ) - if provider == "nvidia": + values["ANTHROPIC_BASE_URL"] = base_url + if provider != "anthropic": values["ANTHROPIC_AUTH_TOKEN"] = "" return values @@ -444,6 +460,7 @@ def prepare_claude_relay(payload: dict[str, Any]) -> ClaudeRelaySettings | None: bind=gateway_bind, url=f"http://{gateway_bind}", log_path=config_path.parent / "gateway.log", + anthropic_base_url=_anthropic_base_url(_selected_model_config(payload)), ) plugin_path = config_path.parent / "claude-plugin" try: @@ -644,7 +661,21 @@ def child_environment( values[api_key_env] = os.environ[api_key_env] configured = common_utils.environment_env(payload) values.update(configured) - values.update(_model_environment(payload, values)) + model_environment = _model_environment(payload, values) + conflicts = sorted( + name + for name, value in model_environment.items() + if name in configured and configured[name] != value + ) + if conflicts: + fields = ", ".join(f"environment.env.{name}" for name in conflicts) + raise AdapterConfigError( + "claude_invalid_configuration", + f"{fields} conflicts with the selected model configuration; " + "configure model credentials and endpoints through models., " + "or remove the duplicate environment.env values", + ) + values.update(model_environment) if relay_gateway_url is not None: values["NEMO_RELAY_GATEWAY_URL"] = relay_gateway_url values["ANTHROPIC_BASE_URL"] = relay_gateway_url diff --git a/adapters/codex/README.md b/adapters/codex/README.md index bfd5ce0ac..f9a17107b 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -49,15 +49,14 @@ the SDK runtime. The current real-agent acceptance path validates an existing Codex login; it does not yet claim a raw environment variable as a complete login flow. -When the selected model provider is `nvidia`, the adapter defines a Codex model -provider for the configured NVIDIA Responses endpoint. `Fabric.run(...)` owns -that provider for one invocation, while `Fabric.start_runtime(...)` fixes it for -the lifetime of the persistent runtime. The adapter reads the credential from -`api_key_env` (default: `NVIDIA_API_KEY`) and isolates Codex state under the -NeMo Fabric artifact root, so execution does not depend on or modify a user's Codex -login. Set the endpoint in -`models..base_url`; it defaults to the public NVIDIA API Catalog endpoint. -Set it explicitly for a frontier or self-hosted endpoint. +The native `openai` provider retains Codex authentication and endpoint +discovery. For another provider name, configure both +`models..api_key_env` and `models..base_url`. The endpoint must +implement the OpenAI Responses protocol. The adapter defines a runtime-scoped +Codex model provider with that name and isolates its Codex state under the +NeMo Fabric artifact root, so execution does not depend on or modify a user's +Codex login. Provider names identify configuration; the adapter does not +maintain a provider allowlist. The adapter depends on the Codex SDK, which installs and selects its matching app-server runtime. NeMo Fabric does not declare the runtime package directly or @@ -83,9 +82,9 @@ return codes, stdout, or stderr. Use normalized `FabricConfig` fields for portable configuration: -- `models` selects the Codex model. The adapter supports the built-in `openai` - provider and NVIDIA-hosted Responses-compatible models through the `nvidia` - provider. `models..base_url` selects an explicit endpoint. +- `models` selects the Codex model. The native `openai` provider retains Codex + authentication and endpoint discovery. Any other provider name must configure + a Responses-compatible `base_url` and `api_key_env`. - `system_prompt` maps to Codex base instructions. - `runtime.timeout_seconds` sets the Fabric invocation deadline. - `environment.workspace` sets the working directory, and `environment.env` @@ -132,3 +131,5 @@ Codex state variables, the selected model's `api_key_env`, and explicit Relay-enabled runs also require the external `nemo-relay` CLI. Refer to the [NeMo Relay CLI](https://docs.nvidia.com/nemo/fabric/getting-started/install#nemo-relay-cli) install guide for instructions on installing the CLI tool. +NeMo Fabric routes the selected Responses-compatible provider through the +gateway and passes its explicit `base_url` to Relay as the upstream endpoint. diff --git a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py index ae549899a..8ed8c9a1d 100644 --- a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py +++ b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py @@ -291,10 +291,10 @@ def selected_model(payload: dict[str, Any]) -> str | None: if value is None: return None provider = model_config.get("provider") - if provider not in {"openai", "nvidia"}: + if not isinstance(provider, str) or not provider: raise AdapterConfigError( "codex_invalid_configuration", - "selected model provider must be openai or nvidia for the Codex adapter", + "selected model provider must be a non-empty string", ) if not isinstance(value, str) or not value: raise AdapterConfigError( @@ -304,18 +304,26 @@ def selected_model(payload: dict[str, Any]) -> str | None: def selected_model_provider(payload: dict[str, Any]) -> str: - return str(_selected_model_config(payload).get("provider") or "openai") + provider = _selected_model_config(payload).get("provider") + if not isinstance(provider, str) or not provider: + raise AdapterConfigError( + "codex_invalid_configuration", + "selected model provider must be a non-empty string", + ) + return provider -def nvidia_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: +def custom_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: model_config = _selected_model_config(payload) - if model_config.get("provider") != "nvidia": + provider = selected_model_provider(payload) + if provider == "openai": return {} - api_key_env = model_config.get("api_key_env") or "NVIDIA_API_KEY" + api_key_env = model_config.get("api_key_env") if not isinstance(api_key_env, str) or not api_key_env: raise AdapterConfigError( "codex_invalid_configuration", - "models.default.api_key_env must be a non-empty string", + "selected model api_key_env is required for a custom " + "Responses-compatible provider", ) if not ( common_utils.environment_env(payload).get(api_key_env) @@ -323,18 +331,19 @@ def nvidia_model_provider_config(payload: dict[str, Any]) -> dict[str, Any]: ): raise AdapterConfigError( "codex_invalid_configuration", - f"{api_key_env} is required for the NVIDIA model provider", + f"{api_key_env} is required for the selected model provider", ) base_url = common_utils.get_base_url(model_config) if not isinstance(base_url, str) or not base_url: raise AdapterConfigError( "codex_invalid_configuration", - "selected model base_url is required for the NVIDIA model provider", + "selected model base_url is required for a custom " + "Responses-compatible provider", ) return { "model_providers": { - "nvidia": { - "name": "NVIDIA", + provider: { + "name": provider, "base_url": base_url.rstrip("/"), "env_key": api_key_env, "wire_api": "responses", @@ -437,8 +446,8 @@ def child_environment( and api_key_env in values ): values["OPENAI_API_KEY"] = values[api_key_env] - if selected_model_provider(payload) == "nvidia": - codex_home = state_dir(payload) / "nvidia-home" + if selected_model_provider(payload) != "openai": + codex_home = state_dir(payload) / "custom-provider-home" values["CODEX_HOME"] = str(codex_home) # The SDK overlays this mapping on the parent environment. An empty # originator is still treated as an override by Codex and produces invalid @@ -586,6 +595,12 @@ def prepare_codex_relay(payload: dict[str, Any]) -> CodexRelaySettings | None: "NeMo Relay runtime configuration is unavailable", ) + base_url = common_utils.get_base_url(_selected_model_config(payload)) + if base_url is not None and (not isinstance(base_url, str) or not base_url): + raise AdapterConfigError( + "codex_invalid_configuration", + "selected model base_url must be a non-empty string", + ) port = relay_gateway.find_available_tcp_port() bind = f"127.0.0.1:{port}" return CodexRelaySettings( @@ -595,6 +610,7 @@ def prepare_codex_relay(payload: dict[str, Any]) -> CodexRelaySettings | None: bind=bind, url=f"http://{bind}", log_path=config_path.parent / "gateway.log", + openai_base_url=base_url.rstrip("/") if base_url else None, ), plugin_config=plugin_config, ) @@ -606,7 +622,7 @@ def thread_config( """Build request-scoped Codex config without writing a user profile.""" config = native_codex_telemetry_config(payload) - _merge_config(config, nvidia_model_provider_config(payload)) + _merge_config(config, custom_model_provider_config(payload)) _merge_config(config, openai_model_provider_config(payload)) mcp_servers = _native_mcp_servers(payload) if mcp_servers: @@ -617,13 +633,22 @@ def thread_config( ) _apply_config_overrides(config, overrides) if relay is not None: + provider = selected_model_provider(payload) + transport_config = ( + {"openai_base_url": relay.gateway.url} + if provider == "openai" + else { + "model_providers": { + provider: { + "base_url": relay.gateway.url, + } + } + } + ) _merge_config( config, { - # Keep the SDK-selected built-in provider so Codex retains its - # native API-key and ChatGPT authentication behavior. Relay is - # only the transport endpoint for this invocation. - "openai_base_url": relay.gateway.url, + **transport_config, "features": { "hooks": True, # Relay disables delegated multi-agent execution because @@ -713,14 +738,6 @@ def validate_runtime_payload(payload: dict[str, Any]) -> str: _personality(payload) _reasoning_effort(payload) _output_schema(payload) - if ( - common_utils.relay_enabled(payload) - and selected_model_provider(payload) != "openai" - ): - raise AdapterConfigError( - "codex_invalid_configuration", - "NeMo Relay requires the built-in openai model provider", - ) child_environment(payload) thread_config(payload, None) return fabric_runtime_id @@ -992,7 +1009,7 @@ async def start(self, payload: dict[str, Any]) -> None: self._relay = relay self._gateway_process = _start_relay_gateway(payload, relay) client_config = sdk_config(payload, relay) - if selected_model_provider(payload) == "nvidia": + if selected_model_provider(payload) != "openai": await asyncio.to_thread( Path(client_config.env["CODEX_HOME"]).mkdir, parents=True, diff --git a/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py b/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py index a292fafb5..c8777e858 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py +++ b/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py @@ -37,6 +37,8 @@ class RelayGatewayLaunch: bind: str url: str log_path: Path + openai_base_url: str | None = None + anthropic_base_url: str | None = None @dataclass(frozen=True) @@ -155,16 +157,21 @@ def start_relay_gateway( if not launch.config_path.is_file(): raise RelayGatewayError("NeMo Relay gateway configuration was not generated") launch.log_path.parent.mkdir(parents=True, exist_ok=True) + command = [ + str(launch.executable), + "--config", + str(launch.config_path), + "--bind", + launch.bind, + ] + if launch.openai_base_url is not None: + command.extend(["--openai-base-url", launch.openai_base_url]) + if launch.anthropic_base_url is not None: + command.extend(["--anthropic-base-url", launch.anthropic_base_url]) try: with launch.log_path.open("wb") as log_stream: process = subprocess.Popen( - [ - str(launch.executable), - "--config", - str(launch.config_path), - "--bind", - launch.bind, - ], + command, cwd=cwd, stdout=log_stream, stderr=subprocess.STDOUT, diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index c717bd4b4..bc809028d 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -104,16 +104,10 @@ def models_payload(payload: dict[str, Any]) -> dict[str, Any]: return fabric_config(payload).get("models") or payload.get("models") or {} -def default_base_url(provider: str | None) -> str | None: - if provider == "nvidia": - return "https://integrate.api.nvidia.com/v1" - return None - - def get_base_url(model_config: dict[str, Any]) -> str | None: - """Return the canonical model endpoint, including provider defaults.""" + """Return the explicitly configured model endpoint.""" - return model_config.get("base_url") or default_base_url(model_config.get("provider")) + return model_config.get("base_url") def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: @@ -153,7 +147,9 @@ def environment_env(payload: dict[str, Any]) -> dict[str, str]: def telemetry_payload(payload: dict[str, Any]) -> dict[str, Any]: - telemetry = fabric_config(payload).get("telemetry") or payload.get("telemetry") or {} + telemetry = ( + fabric_config(payload).get("telemetry") or payload.get("telemetry") or {} + ) return telemetry if isinstance(telemetry, dict) else {} @@ -263,7 +259,9 @@ def load_relay_plugin_config(payload: dict[str, Any]) -> dict[str, Any]: return plugin_config -def normalize_relay_output_dirs(plugin_config: dict[str, Any], payload: dict[str, Any]) -> None: +def normalize_relay_output_dirs( + plugin_config: dict[str, Any], payload: dict[str, Any] +) -> None: base = Path(base_dir(payload)).resolve() runtime_id = runtime_context(payload)["runtime_id"] for component in plugin_config.get("components", []): @@ -352,7 +350,9 @@ def write_relay_configs( 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") + raise RuntimeError( + "FABRIC_RELAY_CONFIG_PATH is required when Relay is enabled" + ) config_path = Path(config_path) config_dir = config_path.parent / "relay-config" @@ -380,6 +380,5 @@ def write_relay_configs( raise RuntimeError("tomli_w is not installed") from e - def relay_model_name(payload: dict[str, Any]) -> str: return selected_model_config(payload).get("model") or "unknown" diff --git a/adapters/deepagents/README.md b/adapters/deepagents/README.md index 3c39a893d..bf0be5fd6 100644 --- a/adapters/deepagents/README.md +++ b/adapters/deepagents/README.md @@ -23,22 +23,20 @@ pip install "nemo-fabric[deepagents, runtime]" ## Model and Authentication -The adapter builds a LangChain chat model from NeMo Fabric's `models.default` config. -For `nvidia` (or an unspecified provider) it targets NVIDIA-hosted, -OpenAI-compatible endpoints (`https://integrate.api.nvidia.com/v1`) via -`ChatOpenAI`; `openai` and `openai-compatible` also use `ChatOpenAI` with the -provider's own default endpoint. Any other provider is constructed through -`langchain.chat_models.init_chat_model`, so additional backends can be added -without changing the adapter. - -`models.default.api_key_env` names the environment variable holding the API key, -and defaults **per provider** — `NVIDIA_API_KEY` for `nvidia` (or an unspecified -provider) and `OPENAI_API_KEY` for `openai`. Every other provider — including -`openai-compatible` and any `init_chat_model` backend — must set `api_key_env` -explicitly (a missing one is a normalized configuration failure), so a key is -never sent to the wrong endpoint. - -Because `models.default.api_key_env` is provider-specific, the adapter declares no +The adapter builds a LangChain chat model from the selected NeMo Fabric model +role: `models.default`, or the sole configured role when `default` is absent. +The `openai`, `nvidia`, and `openai-compatible` providers use `ChatOpenAI`; +`nvidia` and `openai-compatible` require an explicit compatible `base_url`. +Any other provider is constructed through +`langchain.chat_models.init_chat_model`, so LangChain-supported backends do not +require adapter-specific branches. + +`models..api_key_env` names the environment variable holding the API key, +and defaults to `OPENAI_API_KEY` only for the native `openai` provider. Every +other provider must set `api_key_env` explicitly (a missing one is a normalized +configuration failure), so a key is never sent to the wrong endpoint. + +Because `models..api_key_env` is provider-specific, the adapter declares no static env requirement; a runtime **preflight** verifies that the `deepagents` package is importable and the configured credential is set. A failed preflight fails runtime start with a stable lifecycle error. `fabric doctor` validates diff --git a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py index aa53cbb15..ae0d7b157 100644 --- a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py +++ b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py @@ -29,13 +29,7 @@ HARNESS = "deepagents" # Providers we serve through the OpenAI-compatible ``ChatOpenAI`` client. -OPENAI_COMPATIBLE_PROVIDERS = {"", "nvidia", "openai", "openai-compatible"} -# Conventional credential env var per provider; others must set api_key_env. -PROVIDER_DEFAULT_API_KEY_ENV = { - "": "NVIDIA_API_KEY", - "nvidia": "NVIDIA_API_KEY", - "openai": "OPENAI_API_KEY", -} +OPENAI_COMPATIBLE_PROVIDERS = {"nvidia", "openai", "openai-compatible"} # MCP transports langchain-mcp-adapters accepts (after normalization). VALID_MCP_TRANSPORTS = {"stdio", "sse", "streamable_http", "websocket"} # create_deep_agent arguments Fabric derives from normalized config; the @@ -94,23 +88,25 @@ def wrap_tool_call(self, request: Any, handler: Any) -> Any: def resolve_api_key_env(model_config: dict[str, Any]) -> str: - """Resolve the credential env var, defaulting per provider. + """Resolve the credential env var. - An explicit ``api_key_env`` always wins. Otherwise nvidia/unspecified default - to ``NVIDIA_API_KEY`` and openai to ``OPENAI_API_KEY``; any other provider must - set ``api_key_env`` explicitly so a key is never sent to the wrong endpoint. + OpenAI retains its conventional environment variable. Other providers must + name the credential explicitly so a key is never sent to the wrong endpoint. """ explicit = model_config.get("api_key_env") - if explicit: - return str(explicit) - provider = str(model_config.get("provider") or "").lower() - default = PROVIDER_DEFAULT_API_KEY_ENV.get(provider) - if default is None: + if isinstance(explicit, str) and explicit: + return explicit + if explicit is not None: raise AdapterConfigError( - f"models.default.api_key_env is required for provider '{provider}'." + "models.default.api_key_env must be a non-empty string." ) - return default + provider = str(model_config.get("provider") or "").lower() + if provider == "openai": + return "OPENAI_API_KEY" + raise AdapterConfigError( + f"models.default.api_key_env is required for provider '{provider}'." + ) def main() -> None: @@ -157,10 +153,8 @@ def resolve_base_url(model_config: dict[str, Any]) -> str | None: def build_chat_model(payload: dict[str, Any]) -> tuple[Any, str, str | None]: """Build a LangChain chat model from Fabric model config. - The default path targets NVIDIA-hosted OpenAI-compatible endpoints. A generic - hook falls back to ``langchain.chat_models.init_chat_model`` for any provider - that is not OpenAI-compatible, so other backends can be added without - reworking the adapter. + Known OpenAI-compatible providers use ``ChatOpenAI``. Other providers are + delegated to ``langchain.chat_models.init_chat_model``. """ model_config = selected_model_config(payload) @@ -175,10 +169,17 @@ def build_chat_model(payload: dict[str, Any]) -> tuple[Any, str, str | None]: if not api_key: raise RuntimeError(f"{api_key_env} is required for the Deep Agents adapter") - provider = str(model_config.get("provider") or "nvidia").lower() + provider = str(model_config.get("provider") or "").lower() + if not provider: + raise AdapterConfigError("models.default.provider is required.") base_url = resolve_base_url(model_config) temperature = model_config.get("temperature") + if provider in OPENAI_COMPATIBLE_PROVIDERS - {"openai"} and not base_url: + raise AdapterConfigError( + f"models.default.base_url is required for provider '{provider}'." + ) + if provider not in OPENAI_COMPATIBLE_PROVIDERS: # Generic provider hook: honor an explicit non-OpenAI-compatible provider. from langchain.chat_models import init_chat_model diff --git a/adapters/hermes/fabric-adapter.json b/adapters/hermes/fabric-adapter.json index 804acb622..53ff125e8 100644 --- a/adapters/hermes/fabric-adapter.json +++ b/adapters/hermes/fabric-adapter.json @@ -6,11 +6,7 @@ "runner": { "module": "nemo_fabric_adapters.hermes.adapter" }, - "requirements": { - "env": [ - "NVIDIA_API_KEY" - ] - }, + "requirements": {}, "config": { "accepts": [ "models", diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 89520a477..b298312bb 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -30,7 +30,6 @@ DEFAULT_MAX_ITERATIONS: int = 90 LOGGER = logging.getLogger(__name__) PROVIDER_DEFAULT_API_KEY_ENV = { - "": "NVIDIA_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "nvidia": "NVIDIA_API_KEY", "openai": "OPENAI_API_KEY", @@ -240,10 +239,7 @@ async def start(self, payload: dict[str, Any]) -> None: os.environ.setdefault("HERMES_YOLO_MODE", "1") os.environ.setdefault("HERMES_ACCEPT_HOOKS", "1") os.environ["HERMES_SESSION_SOURCE"] = "fabric" - os.environ.setdefault( - "TERMINAL_ENV", - "local", - ) + os.environ["TERMINAL_ENV"] = "local" os.environ.setdefault( "TERMINAL_TIMEOUT", str(self._settings.get("terminal_timeout", 60)), diff --git a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json index 804acb622..53ff125e8 100644 --- a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json @@ -6,11 +6,7 @@ "runner": { "module": "nemo_fabric_adapters.hermes.adapter" }, - "requirements": { - "env": [ - "NVIDIA_API_KEY" - ] - }, + "requirements": {}, "config": { "accepts": [ "models", diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 4496f902e..66f4af0c8 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -70,7 +70,7 @@ pub struct ToolsConfig { /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, - /// Adapter-native toolset selection and blocking policy. + /// Harness-defined toolset selection and blocking policy. #[serde(default, skip_serializing_if = "Option::is_none")] pub toolsets: Option, /// Additive tool configuration fields. @@ -78,13 +78,13 @@ pub struct ToolsConfig { pub extensions: BTreeMap, } -/// Harness-neutral toolset capability configuration. +/// Harness-defined toolset capability configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ToolsetConfig { - /// Toolsets to expose. `None` preserves the adapter default; an empty list exposes none. + /// Toolsets to expose. `None` preserves the harness default; an empty list exposes none. #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option>, - /// Toolsets to block. + /// Toolsets to exclude from the enabled or default harness toolset set. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, /// Additive toolset configuration fields. @@ -530,6 +530,10 @@ pub struct EnvironmentConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub artifacts: Option, /// Environment variables visible to the harness and its tools. + /// + /// Values are serialized into the run plan and can appear wherever configs + /// or plans are logged or persisted. Prefer `api_key_env`-style + /// environment-variable-name indirection for credentials. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub env: BTreeMap, /// Provider connection metadata, such as server URL, credential reference, or namespace. @@ -1857,7 +1861,7 @@ pub struct ToolsPlan { /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, - /// Adapter-native toolset selection and blocking policy. + /// Harness-defined toolset selection and blocking policy. #[serde(default, skip_serializing_if = "Option::is_none")] pub toolsets: Option, } diff --git a/docs/integrations/claude.mdx b/docs/integrations/claude.mdx index 6da788d48..005f9a0ed 100644 --- a/docs/integrations/claude.mdx +++ b/docs/integrations/claude.mdx @@ -51,6 +51,14 @@ export ANTHROPIC_API_KEY=sk-ant-api03-example The adapter also forwards the selected model's `api_key_env` when the model configuration names a different environment variable. +## Use a Compatible Custom Provider + +Use `provider="anthropic"` for Claude's native authentication and endpoint +discovery. For another provider name, set both `api_key_env` and `base_url`. +The configured endpoint must implement the Anthropic Messages protocol. NeMo +Fabric maps the named credential and endpoint into Claude Code's environment; +it does not maintain a provider-name allowlist or infer provider endpoints. + ## Use Workload Identity Federation For a named WIF profile, set the profile and optional nondefault configuration @@ -86,7 +94,8 @@ variables take precedence over federation even when their value is empty. NeMo Relay does not authenticate Claude. A Relay-enabled NeMo Fabric runtime starts one gateway as a supervised sidecar, sets `ANTHROPIC_BASE_URL` for the -Claude runtime, and reuses the gateway across ordered invocations. +Claude runtime, passes an explicit selected model endpoint to the gateway as its +Anthropic upstream, and reuses the gateway across ordered invocations. `Fabric.run(...)` starts the same runtime, invokes it once, and stops it, so the gateway is scoped to that single invocation. Claude still resolves its credential through the selected mode, and NeMo Fabric does not write authentication values @@ -94,5 +103,5 @@ to Relay configuration or artifacts. NeMo Fabric supports the external NeMo Relay CLI from `0.6.0` up to, but not including, `0.7.0`. The Python package named `nemo-relay` does not install this -CLI. NeMo Fabric owns sidecar supervision and Claude configuration; Relay owns the -gateway transport and semantic observability pipeline. +CLI. NeMo Fabric owns sidecar supervision, Claude configuration, and upstream +selection. Relay owns the gateway transport and semantic observability pipeline. diff --git a/docs/integrations/codex.mdx b/docs/integrations/codex.mdx index e679f75af..bb5d76c42 100644 --- a/docs/integrations/codex.mdx +++ b/docs/integrations/codex.mdx @@ -96,20 +96,25 @@ the current real-agent acceptance path validates cached Codex authentication. Treat `CODEX_HOME/auth.json` as a secret when Codex uses file-based credential storage. Do not commit or copy it into NeMo Fabric configuration or artifacts. +## Use a Compatible Custom Provider + +Use `provider="openai"` for Codex's native authentication and endpoint +discovery. For another provider name, set both `api_key_env` and `base_url`. +The configured endpoint must implement the OpenAI Responses protocol. NeMo +Fabric defines a runtime-scoped Codex model provider with the configured name; +it does not maintain a provider-name allowlist or reuse the native Codex login +for that provider. + ## Use Authentication with Relay -NeMo Relay does not replace OpenAI authentication. A Relay-enabled NeMo Fabric -runtime starts one gateway as a supervised sidecar and directs the Codex SDK's -built-in OpenAI provider through that gateway. The runtime reuses the gateway -and SDK client across ordered invocations. `Fabric.run(...)` starts the same -runtime, invokes it once, and stops it, so the gateway is scoped to that single -invocation. The SDK still obtains credentials from the selected Codex -authentication mode. - -Relay-enabled Codex runs require the selected model provider to be `openai`. -The custom `nvidia` provider uses its configured NVIDIA Responses endpoint and -does not support the built-in provider redirect that Relay requires. Use the -`nvidia` provider without Relay. +NeMo Relay does not replace model-provider authentication. A Relay-enabled NeMo +Fabric runtime starts one gateway as a supervised sidecar and directs the +selected Responses-compatible provider through it. For a configured custom +provider, NeMo Fabric passes its explicit `base_url` to the gateway as the +OpenAI-compatible upstream. The runtime reuses the gateway and SDK client across +ordered invocations. `Fabric.run(...)` starts the same runtime, invokes it once, +and stops it, so the gateway is scoped to that single invocation. The SDK still +obtains credentials from the selected provider configuration. NeMo Fabric supplies runtime-scoped Relay configuration to the SDK. It does not copy the Codex credential store into Relay configuration or persist credentials diff --git a/docs/reference/api/python-library-reference/index.md b/docs/reference/api/python-library-reference/index.md index cc5cd1ff6..ec17f4107 100644 --- a/docs/reference/api/python-library-reference/index.md +++ b/docs/reference/api/python-library-reference/index.md @@ -10,9 +10,9 @@ SPDX-License-Identifier: Apache-2.0 */} ## Modules -- [`nemo_fabric.client`](./nemo_fabric.client.md#module-nemo_fabricclient): Native Python client for resolving and running NeMo Fabric agents. +- [`nemo_fabric.client`](./nemo_fabric.client.md#module-nemo_fabricclient): Native Python client for resolving and running NVIDIA NeMo Fabric agents. - [`nemo_fabric.runtime`](./nemo_fabric.runtime.md#module-nemo_fabricruntime): Runtime lifecycle support for the Fabric Python SDK. -- [`nemo_fabric.models`](./nemo_fabric.models.md#module-nemo_fabricmodels): Pydantic SDK models for NeMo Fabric configuration and requests. +- [`nemo_fabric.models`](./nemo_fabric.models.md#module-nemo_fabricmodels): Pydantic SDK models for NVIDIA NeMo Fabric configuration and requests. - [`nemo_fabric.types`](./nemo_fabric.types.md#module-nemo_fabrictypes): Public data contracts for the NeMo Fabric Python SDK. - [`nemo_fabric.errors`](./nemo_fabric.errors.md#module-nemo_fabricerrors): Public exception hierarchy for the NeMo Fabric Python SDK. @@ -46,7 +46,7 @@ SPDX-License-Identifier: Apache-2.0 */} - [`models.TelemetryConfig`](./nemo_fabric.models.md#class-telemetryconfig): Telemetry configuration. - [`models.TelemetryProviderConfig`](./nemo_fabric.models.md#class-telemetryproviderconfig): Provider-specific telemetry configuration. - [`models.ToolsConfig`](./nemo_fabric.models.md#class-toolsconfig): Harness-neutral tool capability configuration. -- [`models.ToolsetConfig`](./nemo_fabric.models.md#class-toolsetconfig): Harness-neutral toolset selection and blocking policy. +- [`models.ToolsetConfig`](./nemo_fabric.models.md#class-toolsetconfig): Harness-defined toolset selection and blocking policy. - [`types.AdapterInfo`](./nemo_fabric.types.md#class-adapterinfo): Resolved adapter identity attached to a run plan. - [`types.ArtifactManifest`](./nemo_fabric.types.md#class-artifactmanifest): Normalized collection of artifacts produced by a run. - [`types.ArtifactRef`](./nemo_fabric.types.md#class-artifactref): Reference to one artifact produced by a run. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.client.md b/docs/reference/api/python-library-reference/nemo_fabric.client.md index c719a3ead..dbb27a993 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.client.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.client.md @@ -7,7 +7,7 @@ description: "Resolve, plan, diagnose, and run agents with NeMo Fabric." SPDX-License-Identifier: Apache-2.0 */} # module `nemo_fabric.client` -Native Python client for resolving and running NeMo Fabric agents. +Native Python client for resolving and running NVIDIA NeMo Fabric agents. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index e705b75a4..68e16d764 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -7,7 +7,7 @@ description: "Pydantic authoring models for NeMo Fabric config and request input SPDX-License-Identifier: Apache-2.0 */} # module `nemo_fabric.models` -Pydantic SDK models for NeMo Fabric configuration and requests. +Pydantic SDK models for NVIDIA NeMo Fabric configuration and requests. The Rust core remains the source of truth for persisted schema snapshots. These models provide the Python SDK's typed authoring surface and intentionally keep extension fields so consumers can carry adapter- or application-owned data without waiting for a schema release. @@ -1423,7 +1423,9 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `ToolsetConfig` -Harness-neutral toolset selection and blocking policy. +Harness-defined toolset selection and blocking policy. + +``enabled=None`` preserves the selected harness default, while an empty ``enabled`` list exposes no toolsets. ``blocked`` excludes toolsets from either the enabled list or the harness default. --- @@ -1546,7 +1548,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `FabricConfig` SDK-facing typed Fabric agent configuration. -Fabric-owned fields apply uniformly. Adapter-translated fields are checked against the selected descriptor; see the normalized configuration compatibility table in ``docs/sdk/python.mdx``. +Fabric-owned fields apply uniformly. Adapter-translated fields are checked against the selected descriptor; refer to the [normalized configuration compatibility table](/nemo/fabric/sdk/python-sdk#normalized-configuration-compatibility). --- @@ -1630,7 +1632,7 @@ configure_toolsets( ) → Self ``` -Set adapter-native toolset selection and blocking policy. +Set harness-defined toolset selection and blocking policy. --- diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx index dbe36e0d1..93094359a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx @@ -48,7 +48,7 @@ Fabric config models and loading helpers. - [TelemetryProviderConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig): Provider-specific telemetry configuration. - [ToolsConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig): Harness-neutral tool capability configuration. - [ToolsPlan](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan): Normalized tool policy for a run. -- [ToolsetConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig): Harness-neutral toolset capability configuration. +- [ToolsetConfig](/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig): Harness-defined toolset capability configuration. ## Enums diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx index aaf518089..d12af9628 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-environmentconfig.mdx @@ -39,6 +39,8 @@ Artifact path inside or outside the provider. Environment variables visible to the harness and its tools. +Values are serialized into the run plan and can appear wherever configs or plans are logged or persisted. Prefer `api_key_env`-style environment-variable-name indirection for credentials. + ### `connection: Map` Provider connection metadata, such as server URL, credential reference, or namespace. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx index 0c5e1679c..95a956059 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx @@ -21,7 +21,7 @@ Adapter-native tool names to block. ### `toolsets: Option` -Adapter-native toolset selection and blocking policy. +Harness-defined toolset selection and blocking policy. ### `extensions: BTreeMap` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx index 8fa31e77b..9990a46e4 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx @@ -1,7 +1,7 @@ --- title: "Struct Toolset Config" sidebar-title: "ToolsetConfig" -description: "Harness-neutral toolset capability configuration." +description: "Harness-defined toolset capability configuration." position: 34 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -11,17 +11,17 @@ Generated from `cargo doc --no-deps -p nemo-fabric-core`.
Option<Vec<String>>,\n    pub blocked: Vec<String>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
-Harness-neutral toolset capability configuration. +Harness-defined toolset capability configuration. ## Fields ### `enabled: Option>` -Toolsets to expose. `None` preserves the adapter default; an empty list exposes none. +Toolsets to expose. `None` preserves the harness default; an empty list exposes none. ### `blocked: Vec` -Toolsets to block. +Toolsets to exclude from the enabled or default harness toolset set. ### `extensions: BTreeMap` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx index 7935c90af..4b4490815 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx @@ -21,7 +21,7 @@ Adapter-native tool names to block. ### `toolsets: Option` -Adapter-native toolset selection and blocking policy. +Harness-defined toolset selection and blocking policy. ## Trait Implementations diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index d4d6c6ce5..016c44dfc 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -138,8 +138,8 @@ use the descriptor contract values. | Adapter ID | Models | Tools / Blocked Tools | MCP | Skills | Subagents | Telemetry | Persistent Local Host | Remote Service | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `nvidia.fabric.claude` | Anthropic and NVIDIA-hosted Anthropic Messages-compatible models | Per-tool blocking through Claude `disallowed_tools`; toolsets unsupported | Normalized | Normalized | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway | Yes: connected `ClaudeSDKClient`, session, and optional Relay gateway | Not implemented | -| `nvidia.fabric.codex` | OpenAI and NVIDIA-hosted Responses-compatible models | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized: `SKILL.md` directories | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway; native `otel` | Yes: `AsyncCodex` app-server client, thread, and optional Relay gateway | Not implemented | +| `nvidia.fabric.claude` | Native Anthropic or a configured Anthropic Messages-compatible provider | Per-tool blocking through Claude `disallowed_tools`; toolsets unsupported | Normalized | Normalized | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway | Yes: connected `ClaudeSDKClient`, session, and optional Relay gateway | Not implemented | +| `nvidia.fabric.codex` | Native OpenAI or a configured Responses-compatible provider | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized: `SKILL.md` directories | Not exposed | Relay: `atif`, `otel`, and `openinference` through hooks and gateway; native `otel` | Yes: `AsyncCodex` app-server client, thread, and optional Relay gateway | Not implemented | | `nvidia.fabric.langchain.deepagents` | NVIDIA, OpenAI, OpenAI-compatible, and other LangChain providers | Per-tool blocking across built-ins, MCP, and local subagents; toolsets unsupported | Normalized | Normalized | Constrained: declarative local subagents inherit parent capabilities | Relay SDK: `atif`, `otel`, and `openinference`; native `otel` and `openinference` | Yes: compiled graph and async LangGraph checkpointer | Not implemented | | `nvidia.fabric.hermes` | Configurable provider, model, and base URL | Hermes toolset selection and blocking; per-tool blocking unsupported | Normalized | Normalized | Not exposed | Relay plugin: `atif`, `otel`, and `openinference` | Yes: `AIAgent`, `SessionDB`, and Relay plugin context | Not implemented | @@ -187,7 +187,7 @@ are grouped because their support does not vary by adapter. | `metadata.name`, `.description` | Core | Core | Core | Core | | `harness.adapter_id`, `.resolution` | Core | Core | Core | Core | | `harness.settings` | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | Adapter-owned escape hatch | -| `models..provider` | `anthropic`, `nvidia` | `openai`, `nvidia` | Dynamic LangChain provider | Dynamic Hermes provider | +| `models..provider` | `anthropic` uses native auth; custom names require an Anthropic Messages-compatible `base_url` and `api_key_env` | `openai` uses native auth; custom names require a Responses-compatible `base_url` and `api_key_env` | Dynamic LangChain provider; custom OpenAI-compatible endpoints require `base_url` and `api_key_env` | Dynamic Hermes provider | | `models..model` | Yes | Yes | Yes | Yes | | `models..api_key_env` | Yes | Yes | Yes | Yes | | `models..base_url` | Yes | Yes | Yes | Yes | @@ -536,7 +536,10 @@ Runtime compatibility checks should validate: Use normalized NeMo Fabric fields for portable behavior: models, system instructions, turn limit, runtime, environment, skills, MCP, telemetry, tools, -toolsets, artifacts, and request context. +toolsets, and artifacts. + +Supply request context through `RunRequest.context` for each invocation. Request +context is not part of `FabricConfig`. Use `harness.settings` only for stable, harness-native behavior that the selected adapter understands, such as Claude permission policy, Codex sandbox controls, diff --git a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json index 804acb622..53ff125e8 100644 --- a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json @@ -6,11 +6,7 @@ "runner": { "module": "nemo_fabric_adapters.hermes.adapter" }, - "requirements": { - "env": [ - "NVIDIA_API_KEY" - ] - }, + "requirements": {}, "config": { "accepts": [ "models", diff --git a/python/src/nemo_fabric/client.py b/python/src/nemo_fabric/client.py index f6d23c10a..2c1c382bb 100644 --- a/python/src/nemo_fabric/client.py +++ b/python/src/nemo_fabric/client.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Native Python client for resolving and running NeMo Fabric agents.""" +"""Native Python client for resolving and running NVIDIA NeMo Fabric agents.""" from __future__ import annotations @@ -214,7 +214,9 @@ async def start_runtime( def start() -> dict[str, Any]: nonlocal started_runtime - started_runtime = json.loads(native.start_runtime(json.dumps(plan.to_mapping()))) + started_runtime = json.loads( + native.start_runtime(json.dumps(plan.to_mapping())) + ) return started_runtime try: diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index 9f589f485..b2f6a0d82 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Pydantic SDK models for NeMo Fabric configuration and requests. +"""Pydantic SDK models for NVIDIA NeMo Fabric configuration and requests. The Rust core remains the source of truth for persisted schema snapshots. These models provide the Python SDK's typed authoring surface and intentionally keep @@ -146,7 +146,11 @@ class EnvironmentConfig(FabricBaseModel): ) env: dict[str, str] = Field( default_factory=dict, - description="Environment variables visible to the harness and its tools.", + description=( + "Environment variables visible to the harness and its tools. Values are " + "serialized into configuration and run plans; prefer api_key_env-style " + "environment-variable-name indirection for credentials." + ), ) settings: dict[str, Any] = Field( default_factory=dict, @@ -293,8 +297,12 @@ class RelayAtofStreamSinkConfig(FabricBaseModel): type: Literal["stream"] = "stream" url: str transport: Literal["http_post", "websocket", "ndjson"] = "http_post" - headers: dict[str, str] = Field(default_factory=dict, exclude_if=lambda value: not value) - header_env: dict[str, str] = Field(default_factory=dict, exclude_if=lambda value: not value) + headers: dict[str, str] = Field( + default_factory=dict, exclude_if=lambda value: not value + ) + header_env: dict[str, str] = Field( + default_factory=dict, exclude_if=lambda value: not value + ) timeout_millis: int = 3000 field_name_policy: Literal["preserve", "replace_dots"] = "preserve" name: str | None = None @@ -403,7 +411,9 @@ class RelayConfig(FabricBaseModel): project: str | None = None output_dir: str | Path | None = None observability: RelayObservabilityConfig | dict[str, Any] | None = None - components: list[RelayComponentConfig | dict[str, Any]] = Field(default_factory=list) + components: list[RelayComponentConfig | dict[str, Any]] = Field( + default_factory=list + ) policy: RelayConfigPolicy | dict[str, Any] | None = None @@ -416,7 +426,9 @@ class TelemetryProviderConfig(FabricBaseModel): class TelemetryConfig(FabricBaseModel): """Telemetry configuration.""" - providers: dict[Literal["relay", "native"], TelemetryProviderConfig | dict[str, Any]] = Field(default_factory=dict) + providers: dict[ + Literal["relay", "native"], TelemetryProviderConfig | dict[str, Any] + ] = Field(default_factory=dict) def enable_relay( self, @@ -445,10 +457,26 @@ def remove_provider(self, provider: Literal["relay", "native"]) -> Self: class ToolsetConfig(FabricBaseModel): - """Harness-neutral toolset selection and blocking policy.""" + """Harness-defined toolset selection and blocking policy. - enabled: list[str] | None = None - blocked: list[str] = Field(default_factory=list) + ``enabled=None`` preserves the selected harness default, while an empty + ``enabled`` list exposes no toolsets. ``blocked`` excludes toolsets from + either the enabled list or the harness default. + """ + + enabled: list[str] | None = Field( + default=None, + description=( + "Toolsets to expose. None preserves the harness default; an empty list " + "exposes no toolsets." + ), + ) + blocked: list[str] = Field( + default_factory=list, + description=( + "Toolsets to exclude from the enabled or default harness toolset set." + ), + ) @model_validator(mode="after") def _validate_policy(self) -> Self: @@ -484,8 +512,9 @@ class FabricConfig(FabricBaseModel): """SDK-facing typed Fabric agent configuration. Fabric-owned fields apply uniformly. Adapter-translated fields are checked - against the selected descriptor; see the normalized configuration - compatibility table in ``docs/sdk/python.mdx``. + against the selected descriptor; refer to the + [normalized configuration compatibility + table](/nemo/fabric/sdk/python-sdk#normalized-configuration-compatibility). """ schema_version: str = "fabric.agent/v1alpha1" @@ -582,7 +611,7 @@ def configure_toolsets( enabled: Sequence[str] | None = None, blocked: Sequence[str] = (), ) -> Self: - """Set adapter-native toolset selection and blocking policy.""" + """Set harness-defined toolset selection and blocking policy.""" if self.tools is None: self.tools = ToolsConfig() @@ -618,12 +647,19 @@ def enable_relay( relay.output_dir = output_dir if observability is not None: relay.observability = ( - observability if isinstance(observability, RelayObservabilityConfig) else dict(observability) + observability + if isinstance(observability, RelayObservabilityConfig) + else dict(observability) ) if components is not None: - relay.components = [item if isinstance(item, RelayComponentConfig) else dict(item) for item in components] + relay.components = [ + item if isinstance(item, RelayComponentConfig) else dict(item) + for item in components + ] if policy is not None: - relay.policy = policy if isinstance(policy, RelayConfigPolicy) else dict(policy) + relay.policy = ( + policy if isinstance(policy, RelayConfigPolicy) else dict(policy) + ) self.relay = relay return self diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 17cd86c47..3a8247f90 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -40,7 +40,7 @@ "additionalProperties": { "type": "string" }, - "description": "Environment variables visible to the harness and its tools.", + "description": "Environment variables visible to the harness and its tools.\n\nValues are serialized into the run plan and can appear wherever configs\nor plans are logged or persisted. Prefer `api_key_env`-style\nenvironment-variable-name indirection for credentials.", "type": "object" }, "metadata": { @@ -980,24 +980,24 @@ "type": "null" } ], - "description": "Adapter-native toolset selection and blocking policy." + "description": "Harness-defined toolset selection and blocking policy." } }, "type": "object" }, "ToolsetConfig": { "additionalProperties": true, - "description": "Harness-neutral toolset capability configuration.", + "description": "Harness-defined toolset capability configuration.", "properties": { "blocked": { - "description": "Toolsets to block.", + "description": "Toolsets to exclude from the enabled or default harness toolset set.", "items": { "type": "string" }, "type": "array" }, "enabled": { - "description": "Toolsets to expose. `None` preserves the adapter default; an empty list exposes none.", + "description": "Toolsets to expose. `None` preserves the harness default; an empty list exposes none.", "items": { "type": "string" }, diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 7ef900933..5e01b6491 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -446,7 +446,7 @@ "additionalProperties": { "type": "string" }, - "description": "Environment variables visible to the harness and its tools.", + "description": "Environment variables visible to the harness and its tools.\n\nValues are serialized into the run plan and can appear wherever configs\nor plans are logged or persisted. Prefer `api_key_env`-style\nenvironment-variable-name indirection for credentials.", "type": "object" }, "metadata": { @@ -1708,7 +1708,7 @@ "type": "null" } ], - "description": "Adapter-native toolset selection and blocking policy." + "description": "Harness-defined toolset selection and blocking policy." } }, "type": "object" @@ -1732,24 +1732,24 @@ "type": "null" } ], - "description": "Adapter-native toolset selection and blocking policy." + "description": "Harness-defined toolset selection and blocking policy." } }, "type": "object" }, "ToolsetConfig": { "additionalProperties": true, - "description": "Harness-neutral toolset capability configuration.", + "description": "Harness-defined toolset capability configuration.", "properties": { "blocked": { - "description": "Toolsets to block.", + "description": "Toolsets to exclude from the enabled or default harness toolset set.", "items": { "type": "string" }, "type": "array" }, "enabled": { - "description": "Toolsets to expose. `None` preserves the adapter default; an empty list exposes none.", + "description": "Toolsets to expose. `None` preserves the harness default; an empty list exposes none.", "items": { "type": "string" }, diff --git a/skills/nemo-fabric-integrate/SKILL.md b/skills/nemo-fabric-integrate/SKILL.md index e6be40272..e684ffd96 100644 --- a/skills/nemo-fabric-integrate/SKILL.md +++ b/skills/nemo-fabric-integrate/SKILL.md @@ -75,6 +75,36 @@ from nemo_fabric import ( ) +def to_tools_config(job) -> ToolsConfig | None: + blocked_tools = list(job.blocked_tools) + enabled_toolsets = job.enabled_toolsets + blocked_toolsets = list(job.blocked_toolsets) + + if job.adapter_id in { + "nvidia.fabric.claude", + "nvidia.fabric.langchain.deepagents", + }: + if enabled_toolsets is not None or blocked_toolsets: + raise ValueError(f"{job.adapter_id} does not support toolset policy") + return ToolsConfig(blocked=blocked_tools) if blocked_tools else None + + if job.adapter_id == "nvidia.fabric.hermes": + if blocked_tools: + raise ValueError("nvidia.fabric.hermes does not support per-tool blocking") + if enabled_toolsets is None and not blocked_toolsets: + return None + return ToolsConfig( + toolsets=ToolsetConfig( + enabled=enabled_toolsets, + blocked=blocked_toolsets, + ) + ) + + if blocked_tools or enabled_toolsets is not None or blocked_toolsets: + raise ValueError(f"{job.adapter_id} does not support normalized tool policy") + return None + + def to_fabric_config(job) -> FabricConfig: config = FabricConfig( metadata=MetadataConfig(name=job.name), @@ -94,13 +124,7 @@ def to_fabric_config(job) -> FabricConfig: output_schema="message", timeout_seconds=job.timeout_seconds, ), - tools=ToolsConfig( - blocked=list(job.blocked_tools), - toolsets=ToolsetConfig( - enabled=job.enabled_toolsets, - blocked=list(job.blocked_toolsets), - ), - ), + tools=to_tools_config(job), ) config.add_skill_path(job.skill_dir) config.add_mcp_server( diff --git a/skills/nemo-fabric-integrate/references/config-mapping.md b/skills/nemo-fabric-integrate/references/config-mapping.md index c256cf270..4d9640d3d 100644 --- a/skills/nemo-fabric-integrate/references/config-mapping.md +++ b/skills/nemo-fabric-integrate/references/config-mapping.md @@ -84,8 +84,9 @@ package or job layout, so nothing depends on the process working directory. ## Adapter-Owned And Caller-Owned Data - Use normalized fields for portable behavior: models, system prompt, turn - limit, runtime, environment, tools and toolsets, skills, MCP, telemetry, and - request context. + limit, runtime, environment, tools and toolsets, skills, MCP, and telemetry. +- Supply request context through `RunRequest.context` for each invocation; + request context is not part of `FabricConfig`. - Use `harness.settings` for adapter-owned configuration the selected adapter understands (for example Claude permission policy, Codex sandbox controls, or Deep Agents subagent definitions). Executable paths, state directories, and diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index ee0d51250..9a668ec2f 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -71,25 +71,12 @@ def test_virtualenv_subprocess_env_preserves_environment_outside_virtualenv( def test_request_payload(): - assert common_utils.request_payload({"request": {"input": "hello"}}) == {"input": "hello"} + assert common_utils.request_payload({"request": {"input": "hello"}}) == { + "input": "hello" + } assert common_utils.request_payload({}) == {} -@pytest.mark.parametrize( - ("provider", "expected"), - [ - ("nvidia", "https://integrate.api.nvidia.com/v1"), - ("openai", None), - (None, None), - ], -) -def test_default_base_url( - provider: str | None, - expected: str | None, -): - assert common_utils.default_base_url(provider) == expected - - @pytest.mark.parametrize( ("model_config", "expected"), [ @@ -97,8 +84,11 @@ def test_default_base_url( {"provider": "nvidia", "base_url": "https://model.example/v1"}, "https://model.example/v1", ), - ({"provider": "openai", "base_url": "https://model.example/v1"}, "https://model.example/v1"), - ({"provider": "nvidia"}, "https://integrate.api.nvidia.com/v1"), + ( + {"provider": "openai", "base_url": "https://model.example/v1"}, + "https://model.example/v1", + ), + ({"provider": "nvidia"}, None), ({"provider": "other"}, None), ], ) @@ -193,10 +183,14 @@ def test_payload_accessors_use_canonical_plan_fields(tmp_path): assert common_utils.agent_name(payload) == "outer-agent" assert common_utils.base_dir(payload) == base_dir assert common_utils.runtime_context(payload) == payload["runtime_context"] - assert common_utils.environment_payload(payload) == {"workspace": "/runtime-workspace"} + assert common_utils.environment_payload(payload) == { + "workspace": "/runtime-workspace" + } assert common_utils.settings_payload(payload) == {"inner": True} assert common_utils.models_payload(payload) == {"inner": {"model": "inner-model"}} - assert common_utils.capability_plan(payload) == {"native": {"skill_paths": ["skills"]}} + assert common_utils.capability_plan(payload) == { + "native": {"skill_paths": ["skills"]} + } @pytest.mark.parametrize("value", [None, "", "relative/path"]) @@ -271,11 +265,15 @@ def fake_import(name: str, *args: object, **kwargs: object) -> object: monkeypatch.setattr(builtins, "__import__", fake_import) - assert common_utils.dump_yaml({"model": {"default": "demo"}}) == json.dumps( - {"model": {"default": "demo"}}, - indent=2, - sort_keys=False, - ) + "\n" + assert ( + common_utils.dump_yaml({"model": {"default": "demo"}}) + == json.dumps( + {"model": {"default": "demo"}}, + indent=2, + sort_keys=False, + ) + + "\n" + ) @pytest.mark.parametrize( @@ -326,9 +324,7 @@ def test_load_relay_plugin_config_wraps_and_normalizes_bare_observability_config previous_atof_dir.mkdir(parents=True) previous_atif_dir.mkdir(parents=True) (previous_atof_dir / "events.atof.jsonl").write_text("{}", encoding="utf-8") - (previous_atif_dir / "trajectory-old.atif.json").write_text( - "{}", encoding="utf-8" - ) + (previous_atif_dir / "trajectory-old.atif.json").write_text("{}", encoding="utf-8") payload = { "agent_name": "review-agent", "base_dir": str(tmp_path), @@ -346,7 +342,9 @@ def test_load_relay_plugin_config_wraps_and_normalizes_bare_observability_config assert plugin_config["components"][0]["kind"] == "observability" assert observability["version"] == 2 file_sink, stream_sink = observability["atof"]["sinks"] - assert file_sink["output_directory"] == str(tmp_path / "custom-relay" / "runtime-current") + assert file_sink["output_directory"] == str( + tmp_path / "custom-relay" / "runtime-current" + ) assert file_sink["filename"] == "events.atof.jsonl" assert file_sink["mode"] == "overwrite" assert Path(file_sink["output_directory"]).is_dir() @@ -354,14 +352,21 @@ def test_load_relay_plugin_config_wraps_and_normalizes_bare_observability_config "type": "stream", "url": "https://example.test/events", } - assert observability["atif"]["output_directory"] == str(tmp_path / "artifacts" / "relay" / "runtime-current") - assert observability["atif"]["filename_template"] == "trajectory-{session_id}.atif.json" + assert observability["atif"]["output_directory"] == str( + tmp_path / "artifacts" / "relay" / "runtime-current" + ) + assert ( + observability["atif"]["filename_template"] + == "trajectory-{session_id}.atif.json" + ) assert observability["atif"]["agent_name"] == "review-agent" assert observability["atif"]["model_name"] == "nvidia/review-model" assert Path(observability["atif"]["output_directory"]).is_dir() atof_file = Path(file_sink["output_directory"]) / "events.atof.jsonl" - atif_file = Path(observability["atif"]["output_directory"]) / "trajectory-current.atif.json" + atif_file = ( + Path(observability["atif"]["output_directory"]) / "trajectory-current.atif.json" + ) atof_file.write_text("{}", encoding="utf-8") atif_file.write_text("{}", encoding="utf-8") diff --git a/tests/adapters/test_adapters_common_relay_gateway.py b/tests/adapters/test_adapters_common_relay_gateway.py index 3d8365240..44adcb956 100644 --- a/tests/adapters/test_adapters_common_relay_gateway.py +++ b/tests/adapters/test_adapters_common_relay_gateway.py @@ -70,9 +70,7 @@ def test_relay_cli_contract_selects_compatible_contract( @pytest.mark.parametrize("output", ["nemo-relay 0.5.9", "nemo-relay 0.7.0"]) -def test_relay_cli_contract_rejects_unsupported_version( - monkeypatch, tmp_path, output -): +def test_relay_cli_contract_rejects_unsupported_version(monkeypatch, tmp_path, output): monkeypatch.setattr( relay_gateway.subprocess, "run", @@ -85,9 +83,7 @@ def test_relay_cli_contract_rejects_unsupported_version( relay_gateway.relay_cli_contract(tmp_path / "nemo-relay") -def test_relay_cli_contract_rejects_unparseable_output( - monkeypatch, tmp_path -): +def test_relay_cli_contract_rejects_unparseable_output(monkeypatch, tmp_path): monkeypatch.setattr( relay_gateway.subprocess, "run", @@ -117,6 +113,8 @@ def test_start_relay_gateway_captures_logs_and_waits_for_health(monkeypatch, tmp bind="127.0.0.1:43210", url="http://127.0.0.1:43210", log_path=log_path, + openai_base_url="https://openai.example/v1", + anthropic_base_url="https://anthropic.example", ) started = relay_gateway.start_relay_gateway( @@ -131,6 +129,10 @@ def test_start_relay_gateway_captures_logs_and_waits_for_health(monkeypatch, tmp str(config_path), "--bind", "127.0.0.1:43210", + "--openai-base-url", + "https://openai.example/v1", + "--anthropic-base-url", + "https://anthropic.example", ] assert mock_popen.call_args.kwargs["cwd"] == tmp_path assert mock_popen.call_args.kwargs["stderr"] is subprocess.STDOUT @@ -178,9 +180,7 @@ def test_start_relay_gateway_stops_failed_process_and_preserves_log( assert log_path.exists() -def test_start_relay_gateway_reports_readiness_and_stop_failures( - monkeypatch, tmp_path -): +def test_start_relay_gateway_reports_readiness_and_stop_failures(monkeypatch, tmp_path): config_path = tmp_path / "config.toml" config_path.write_text("", encoding="utf-8") readiness_error = relay_gateway.RelayGatewayError("not ready") diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index 777b2b861..a8b909d3d 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -231,6 +231,12 @@ def relay_payload_fixture(claude_payload, tmp_path) -> dict[str, Any]: def test_prepare_claude_relay_writes_gateway_config_and_complete_hook_plugin( relay_payload, monkeypatch, tmp_path ): + relay_payload["config"]["models"]["default"].update( + { + "provider": "acme", + "base_url": "https://acme.example/v1/", + } + ) executable = tmp_path / "bin" / "nemo-relay" executable.parent.mkdir() executable.touch() @@ -261,6 +267,7 @@ def test_prepare_claude_relay_writes_gateway_config_and_complete_hook_plugin( assert relay.gateway.bind == "127.0.0.1:43210" assert relay.gateway.url == "http://127.0.0.1:43210" assert relay.gateway.log_path == relay.gateway.config_path.parent / "gateway.log" + assert relay.gateway.anthropic_base_url == "https://acme.example" with relay.gateway.config_path.open("rb") as stream: assert tomllib.load(stream) == {"agents": {"claude": {"command": "claude"}}} with (relay.gateway.config_path.parent / "plugins.toml").open("rb") as stream: @@ -381,70 +388,116 @@ def test_build_options_rejects_skill_path_without_skill_manifest(claude_payload) adapter.build_options(claude_payload) -def test_build_options_maps_nvidia_provider_to_claude_gateway_environment( +def test_build_options_maps_custom_provider_to_claude_gateway_environment( claude_payload, ): model = claude_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", + "provider": "acme", "model": "aws/anthropic/claude-opus-4-5", - "api_key_env": "NVIDIA_API_KEY", - "base_url": "https://nvidia.example/v1/", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1/", } ) - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" + claude_payload["runtime_context"]["environment"]["env"].pop("ANTHROPIC_API_KEY") + os.environ["ACME_API_KEY"] = "acme-secret" options = adapter.build_options(claude_payload) assert options.model == "aws/anthropic/claude-opus-4-5" - assert options.env["ANTHROPIC_BASE_URL"] == "https://nvidia.example" - assert options.env["ANTHROPIC_API_KEY"] == "nvidia-secret" + assert options.env["ANTHROPIC_BASE_URL"] == "https://acme.example" + assert options.env["ANTHROPIC_API_KEY"] == "acme-secret" assert options.env["ANTHROPIC_AUTH_TOKEN"] == "" -def test_build_options_uses_nvidia_provider_endpoint_and_default_credential( +def test_build_options_requires_custom_provider_api_key_env( claude_payload, ): model = claude_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", + "provider": "acme", "model": "aws/anthropic/claude-opus-4-5", + "base_url": "https://acme.example/v1", } ) model.pop("api_key_env") claude_payload["runtime_context"]["environment"]["env"].pop("ANTHROPIC_API_KEY") - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" - options = adapter.build_options(claude_payload) - - assert options.env["ANTHROPIC_BASE_URL"] == "https://integrate.api.nvidia.com" - assert options.env["ANTHROPIC_API_KEY"] == "nvidia-secret" + with pytest.raises(adapter.AdapterConfigError, match="api_key_env is required"): + adapter.build_options(claude_payload) -def test_build_options_requires_nvidia_provider_credential(claude_payload): +def test_build_options_requires_custom_provider_credential(claude_payload): model = claude_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", + "provider": "acme", "model": "aws/anthropic/claude-opus-4-5", - "api_key_env": "NVIDIA_API_KEY", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1", } ) - os.environ.pop("NVIDIA_API_KEY", None) + os.environ.pop("ACME_API_KEY", None) - with pytest.raises(adapter.AdapterConfigError, match="NVIDIA_API_KEY is required"): + with pytest.raises(adapter.AdapterConfigError, match="ACME_API_KEY is required"): adapter.build_options(claude_payload) -def test_selected_model_rejects_unsupported_provider(claude_payload): +def test_build_options_requires_custom_provider_endpoint(claude_payload): model = claude_payload["config"]["models"]["default"] - model["provider"] = "openai" + model.update( + { + "provider": "acme", + "model": "aws/anthropic/claude-opus-4-5", + "api_key_env": "ACME_API_KEY", + } + ) + claude_payload["runtime_context"]["environment"]["env"] = { + "ACME_API_KEY": "acme-secret" + } + + with pytest.raises(adapter.AdapterConfigError, match="base_url is required"): + adapter.build_options(claude_payload) + + +@pytest.mark.parametrize( + ("name", "value"), + [ + ("ANTHROPIC_API_KEY", "conflicting-secret"), + ("ANTHROPIC_AUTH_TOKEN", "conflicting-token"), + ("ANTHROPIC_BASE_URL", "https://other.example"), + ], +) +def test_build_options_rejects_model_environment_conflicts( + claude_payload, + name, + value, +): + claude_payload["config"]["models"]["default"] = { + "provider": "acme", + "model": "aws/anthropic/claude-opus-4-5", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1", + } + claude_payload["runtime_context"]["environment"]["env"] = { + "ACME_API_KEY": "acme-secret", + name: value, + } with pytest.raises( - adapter.AdapterConfigError, match="provider must be anthropic or nvidia" + adapter.AdapterConfigError, + match=rf"environment\.env\.{name} conflicts", ): + adapter.build_options(claude_payload) + + +def test_selected_model_rejects_empty_provider(claude_payload): + model = claude_payload["config"]["models"]["default"] + model["provider"] = "" + + with pytest.raises(adapter.AdapterConfigError, match="non-empty string"): adapter.selected_model(claude_payload) diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index 32d1f36fc..3e9874437 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -570,68 +570,74 @@ def test_incomplete_sdk_turn_is_failed(codex_payload, mock_codex): assert output["turn_status"] == "completed" -def test_selected_model_rejects_unsupported_provider(codex_payload, mock_codex): +def test_custom_provider_requires_explicit_api_key_env(codex_payload, mock_codex): model = codex_payload["config"]["models"]["default"] - model["provider"] = "anthropic" + model.update( + { + "provider": "acme", + "model": "acme/code-model", + "base_url": "https://acme.example/v1", + } + ) error = runtime_start_error(codex_payload) assert error.code == "codex_invalid_configuration" - assert "provider must be openai or nvidia" in error.message + assert "api_key_env is required" in error.message mock_codex.assert_not_called() -def test_nvidia_provider_uses_responses_api_and_nvidia_credential( +def test_custom_provider_uses_responses_api_and_configured_credential( codex_payload, mock_codex ): model = codex_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", - "model": "openai/gpt-oss-120b", - "api_key_env": "NVIDIA_API_KEY", - "base_url": "https://nvidia.example/v1/", + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1/", } ) - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" + os.environ["ACME_API_KEY"] = "acme-secret" output = invoke_once(codex_payload) assert output["completed"] is True client = mock_codex.instances[0] - assert client.config.env["NVIDIA_API_KEY"] == "nvidia-secret" + assert client.config.env["ACME_API_KEY"] == "acme-secret" start = client.thread_start.await_args.kwargs - assert start["model"] == "openai/gpt-oss-120b" - assert start["model_provider"] == "nvidia" + assert start["model"] == "acme/code-model" + assert start["model_provider"] == "acme" assert Path(client.config.env["CODEX_HOME"]).parts[-3:] == ( ".fabric", "codex", - "nvidia-home", + "custom-provider-home", ) assert start["config"]["features"] == {"web_search": False} assert start["config"]["model_providers"] == { - "nvidia": { - "name": "NVIDIA", - "base_url": "https://nvidia.example/v1", - "env_key": "NVIDIA_API_KEY", + "acme": { + "name": "acme", + "base_url": "https://acme.example/v1", + "env_key": "ACME_API_KEY", "wire_api": "responses", } } -def test_nvidia_provider_normalizes_codex_home_creation_failure( +def test_custom_provider_normalizes_codex_home_creation_failure( codex_payload, mock_codex, monkeypatch ): model = codex_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", - "model": "openai/gpt-oss-120b", - "api_key_env": "NVIDIA_API_KEY", - "base_url": "https://nvidia.example/v1", + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1", } ) - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" + os.environ["ACME_API_KEY"] = "acme-secret" async def fail_to_create_home(*_args, **_kwargs): raise OSError("read-only filesystem") @@ -644,43 +650,42 @@ async def fail_to_create_home(*_args, **_kwargs): mock_codex.assert_not_called() -def test_nvidia_provider_requires_credential(codex_payload, mock_codex): +def test_custom_provider_requires_credential(codex_payload, mock_codex): model = codex_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", - "model": "openai/gpt-oss-120b", - "api_key_env": "NVIDIA_API_KEY", + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1", } ) - os.environ.pop("NVIDIA_API_KEY", None) + os.environ.pop("ACME_API_KEY", None) error = runtime_start_error(codex_payload) assert error.code == "codex_invalid_configuration" - assert "NVIDIA_API_KEY is required" in error.message - assert not (adapter.state_dir(codex_payload) / "nvidia-home").exists() + assert "ACME_API_KEY is required" in error.message + assert not (adapter.state_dir(codex_payload) / "custom-provider-home").exists() mock_codex.assert_not_called() -def test_nvidia_provider_uses_default_endpoint(codex_payload, mock_codex): +def test_custom_provider_requires_explicit_endpoint(codex_payload, mock_codex): model = codex_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", - "model": "openai/gpt-oss-120b", - "api_key_env": "NVIDIA_API_KEY", + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", } ) - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" + os.environ["ACME_API_KEY"] = "acme-secret" - output = invoke_once(codex_payload) + error = runtime_start_error(codex_payload) - assert output["completed"] is True - config = mock_codex.instances[0].thread_start.await_args.kwargs["config"] - assert config["model_providers"]["nvidia"]["base_url"] == ( - "https://integrate.api.nvidia.com/v1" - ) + assert error.code == "codex_invalid_configuration" + assert "base_url is required" in error.message + mock_codex.assert_not_called() def test_relay_uses_gateway_and_request_scoped_sdk_config( @@ -745,32 +750,56 @@ def test_relay_uses_gateway_and_request_scoped_sdk_config( stop_gateway.assert_called_once_with(process) -def test_relay_rejects_nvidia_provider(codex_payload, mock_codex): +def test_relay_routes_custom_provider_through_gateway(codex_payload, tmp_path): model = codex_payload["config"]["models"]["default"] model.update( { - "provider": "nvidia", - "model": "openai/gpt-oss-120b", - "api_key_env": "NVIDIA_API_KEY", - "base_url": "https://nvidia.example/v1", + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1", } ) codex_payload["telemetry_plan"] = { "providers": ["relay"], "relay_enabled": True, } - os.environ["NVIDIA_API_KEY"] = "nvidia-secret" + os.environ["ACME_API_KEY"] = "acme-secret" + gateway = adapter.relay_gateway.RelayGatewayLaunch( + executable=tmp_path / "nemo-relay", + config_path=tmp_path / "relay" / "config.toml", + bind="127.0.0.1:43210", + url="http://127.0.0.1:43210", + log_path=tmp_path / "relay" / "gateway.log", + openai_base_url="https://acme.example/v1", + ) + relay = adapter.CodexRelaySettings( + gateway=gateway, + plugin_config={"version": 1, "components": []}, + ) - error = runtime_start_error(codex_payload) + adapter.validate_runtime_payload(codex_payload) + config = adapter.thread_config(codex_payload, relay) - assert error.code == "codex_invalid_configuration" - assert error.message == ("NeMo Relay requires the built-in openai model provider") - mock_codex.assert_not_called() + assert config["model_providers"]["acme"] == { + "name": "acme", + "base_url": gateway.url, + "env_key": "ACME_API_KEY", + "wire_api": "responses", + } def test_prepare_relay_reuses_one_resolved_executable( codex_payload, monkeypatch, tmp_path ): + codex_payload["config"]["models"]["default"].update( + { + "provider": "acme", + "model": "acme/code-model", + "api_key_env": "ACME_API_KEY", + "base_url": "https://acme.example/v1/", + } + ) codex_payload["telemetry_plan"] = { "providers": ["relay"], "relay_enabled": True, @@ -800,6 +829,7 @@ def test_prepare_relay_reuses_one_resolved_executable( assert relay is not None assert relay.gateway.executable == executable assert relay.gateway.url == "http://127.0.0.1:43210" + assert relay.gateway.openai_base_url == "https://acme.example/v1" resolve.assert_called_once_with( Path(codex_payload["base_dir"]).resolve(), "nemo-relay", diff --git a/tests/adapters/test_deepagents.py b/tests/adapters/test_deepagents.py index 2b72ee3c9..85351f545 100644 --- a/tests/adapters/test_deepagents.py +++ b/tests/adapters/test_deepagents.py @@ -158,6 +158,7 @@ def make(tmp_path: Path, *, runtime_id: str = "run-1") -> dict[str, Any]: "provider": "nvidia", "model": "nvidia/nemotron-3-nano-30b-a3b", "api_key_env": "NVIDIA_API_KEY", + "base_url": "https://integrate.api.nvidia.com/v1", } }, }, @@ -1094,6 +1095,19 @@ async def test_openai_compatible_provider_requires_api_key_env(tmp_path, make_pa await adapter.DeepAgentsRuntime().start(lifecycle_start_payload(payload)) +async def test_openai_compatible_provider_requires_base_url(tmp_path, make_payload): + os.environ["CUSTOM_API_KEY"] = "sk-test" + payload = make_payload(tmp_path) + payload["config"]["models"]["default"] = { + "provider": "openai-compatible", + "model": "some/model", + "api_key_env": "CUSTOM_API_KEY", + } + + with pytest.raises(adapter.AdapterConfigError, match="base_url"): + await adapter.DeepAgentsRuntime().start(lifecycle_start_payload(payload)) + + def test_main_serves_persistent_runtime(monkeypatch): serve = MagicMock() monkeypatch.setattr(adapter.lifecycle, "serve", serve) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index acef2c446..da91e4693 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -467,6 +467,34 @@ async def test_runtime_start_rejects_native_telemetry(): await adapter.HermesRuntime().start(payload) +async def test_runtime_start_overrides_inherited_terminal_environment( + monkeypatch, + tmp_path: Path, +): + os.environ["TERMINAL_ENV"] = "docker" + + def stop_after_environment_setup(*_args, **_kwargs): + assert os.environ["TERMINAL_ENV"] == "local" + raise RuntimeError("stop after environment setup") + + monkeypatch.setattr(adapter, "write_hermes_config", stop_after_environment_setup) + payload = { + "base_dir": str(tmp_path), + "config": { + "harness": {"settings": {}}, + "models": {"default": {"provider": "nvidia", "model": "test-model"}}, + }, + "runtime_context": { + "runtime_id": "runtime-terminal-env", + "environment": {"workspace": str(tmp_path)}, + "artifacts": {"root": str(tmp_path / "artifacts")}, + }, + } + + with pytest.raises(RuntimeError, match="stop after environment setup"): + await adapter.HermesRuntime().start(payload) + + async def test_persistent_runtime_reuses_hermes_agent_session_and_history( monkeypatch, tmp_path: Path, diff --git a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py index 4547cbb84..087628ef6 100644 --- a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py +++ b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py @@ -108,7 +108,10 @@ def run_swebench_shim(payload: dict[str, Any]) -> dict[str, Any]: request = request_payload(payload) context = request.get("context", {}) environment = environment_payload(payload) - workspace = Path(environment.get("workspace") or ".") + workspace_value = environment.get("workspace") + if not isinstance(workspace_value, str) or not workspace_value: + raise ValueError("runtime_context.environment.workspace is required") + workspace = Path(workspace_value) target_file = workspace / settings.get("target_file", "calculator.py") before = settings.get("expected_before") after = settings.get("replacement") From 6e487f5e7b708c05a25b08e884a11eb70776131f Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 13:06:39 -0700 Subject: [PATCH 08/21] docs: use full product name in API metadata Signed-off-by: Ajay Thorve --- .../api/python-library-reference/nemo_fabric.client.md | 2 +- .../api/python-library-reference/nemo_fabric.models.md | 2 +- scripts/generate_api_docs.sh | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/api/python-library-reference/nemo_fabric.client.md b/docs/reference/api/python-library-reference/nemo_fabric.client.md index dbb27a993..8e871cafe 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.client.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.client.md @@ -1,7 +1,7 @@ --- title: "Client" slug: "/reference/api/python-library-reference/client" -description: "Resolve, plan, diagnose, and run agents with NeMo Fabric." +description: "Resolve, plan, diagnose, and run agents with NVIDIA NeMo Fabric." --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index 68e16d764..d2efd179c 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -1,7 +1,7 @@ --- title: "Models" slug: "/reference/api/python-library-reference/models" -description: "Pydantic authoring models for NeMo Fabric config and request inputs." +description: "Pydantic authoring models for NVIDIA NeMo Fabric config and request inputs." --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/scripts/generate_api_docs.sh b/scripts/generate_api_docs.sh index 685a61adb..ebabfcc3d 100755 --- a/scripts/generate_api_docs.sh +++ b/scripts/generate_api_docs.sh @@ -67,7 +67,7 @@ add_frontmatter \ add_frontmatter \ "$out/nemo_fabric.client.md" \ "Client" \ - "Resolve, plan, diagnose, and run agents with NeMo Fabric." \ + "Resolve, plan, diagnose, and run agents with NVIDIA NeMo Fabric." \ "/reference/api/python-library-reference/client" add_frontmatter \ "$out/nemo_fabric.runtime.md" \ @@ -77,7 +77,7 @@ add_frontmatter \ add_frontmatter \ "$out/nemo_fabric.models.md" \ "Models" \ - "Pydantic authoring models for NeMo Fabric config and request inputs." \ + "Pydantic authoring models for NVIDIA NeMo Fabric config and request inputs." \ "/reference/api/python-library-reference/models" add_frontmatter \ "$out/nemo_fabric.types.md" \ From cbf33727ff344e28f3f8746aead10d80cb76aa75 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 24 Jul 2026 14:37:27 -0700 Subject: [PATCH 09/21] fix: redact persisted adapter environment values Signed-off-by: Ajay Thorve --- crates/fabric-core/src/runtime.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index 12775817a..e4d260757 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -959,7 +959,16 @@ fn run_local_host_adapter_with_timeout( &artifacts, relay_config.as_ref(), )?; - let adapter_payload = serde_json::to_string_pretty(&adapter_invocation) + let mut persisted_invocation = adapter_invocation.clone(); + for value in persisted_invocation + .runtime_context + .environment + .env + .values_mut() + { + *value = "[REDACTED]".to_string(); + } + let adapter_payload = serde_json::to_string_pretty(&persisted_invocation) .map_err(FabricError::SerializeJson)?; let fabric_invocation = write_fabric_invocation(&fabric_home, &adapter_payload)?; let lifecycle_request = AdapterLifecycleRequest::new(AdapterLifecycleRequestKind::Invoke( @@ -2564,6 +2573,19 @@ for line in sys.stdin: assert_eq!(first.output["normalized_env"], serde_json::json!("visible")); assert_eq!(second.output["input"], serde_json::json!("second")); assert_eq!(first.metadata["host_pid"], second.metadata["host_pid"]); + let persisted_invocation: Value = serde_json::from_str( + &fs::read_to_string( + first.metadata["fabric_invocation"] + .as_str() + .expect("invocation path"), + ) + .expect("read persisted invocation"), + ) + .expect("parse persisted invocation"); + assert_eq!( + persisted_invocation["runtime_context"]["environment"]["env"]["FABRIC_NORMALIZED_ENV"], + serde_json::json!("[REDACTED]") + ); assert_eq!( first.metadata["adapter_runner"], serde_json::json!("persistent_local_host") From 9ceec1c75e741fa344ef6d00101fc89ce8e37cc7 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 27 Jul 2026 08:23:27 -0700 Subject: [PATCH 10/21] Updating versions for v0.2.0 Signed-off-by: David Gardner --- Cargo.toml | 4 ++-- adapters/claude/pyproject.toml | 4 ++-- adapters/claude/uv.lock | 4 ++-- adapters/codex/pyproject.toml | 4 ++-- adapters/codex/uv.lock | 4 ++-- adapters/common/pyproject.toml | 2 +- adapters/common/uv.lock | 2 +- adapters/deepagents/pyproject.toml | 4 ++-- adapters/deepagents/uv.lock | 4 ++-- adapters/hermes/pyproject.toml | 4 ++-- adapters/hermes/uv.lock | 4 ++-- pyproject.toml | 16 ++++++++-------- uv.lock | 12 ++++++------ 13 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc77c16f8..5ec865432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,13 @@ default-members = [ resolver = "2" [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/NVIDIA/nemo-fabric" [workspace.dependencies] -nemo-fabric-core = { path = "crates/fabric-core", version = "0.1.0" } +nemo-fabric-core = { path = "crates/fabric-core", version = "0.2.0" } clap = { version = "4", default-features = false, features = ["derive", "std", "help", "usage", "error-context", "suggestions"] } schemars = { version = "1", features = ["derive"] } diff --git a/adapters/claude/pyproject.toml b/adapters/claude/pyproject.toml index f80d80986..f35222caa 100644 --- a/adapters/claude/pyproject.toml +++ b/adapters/claude/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-claude" -version = "0.1.0" +version = "0.2.0" description = "Claude adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.1.0", + "nemo-fabric-adapters-common == 0.2.0", "claude-agent-sdk==0.2.120", "tomli-w~=1.2", ] diff --git a/adapters/claude/uv.lock b/adapters/claude/uv.lock index 012c18e12..b2b617547 100644 --- a/adapters/claude/uv.lock +++ b/adapters/claude/uv.lock @@ -350,7 +350,7 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-claude" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "claude-agent-sdk" }, @@ -367,7 +367,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "../common" } [[package]] diff --git a/adapters/codex/pyproject.toml b/adapters/codex/pyproject.toml index 7acd61c0f..63b5d0752 100644 --- a/adapters/codex/pyproject.toml +++ b/adapters/codex/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-codex" -version = "0.1.0" +version = "0.2.0" description = "Codex adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.1.0", + "nemo-fabric-adapters-common == 0.2.0", "openai-codex==0.144.4", "tomli-w~=1.2", ] diff --git a/adapters/codex/uv.lock b/adapters/codex/uv.lock index a76f126a6..b66fd4f11 100644 --- a/adapters/codex/uv.lock +++ b/adapters/codex/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-codex" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-adapters-common" }, @@ -30,7 +30,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "../common" } [[package]] diff --git a/adapters/common/pyproject.toml b/adapters/common/pyproject.toml index e428d1d49..cd3b63302 100644 --- a/adapters/common/pyproject.toml +++ b/adapters/common/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" description = "Shared Python helpers for NeMo Fabric adapters" authors = [ { name = "NVIDIA Corporation" }, diff --git a/adapters/common/uv.lock b/adapters/common/uv.lock index a9e754d66..b7d8656f9 100644 --- a/adapters/common/uv.lock +++ b/adapters/common/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11" [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } diff --git a/adapters/deepagents/pyproject.toml b/adapters/deepagents/pyproject.toml index ceb261b02..433ba938c 100644 --- a/adapters/deepagents/pyproject.toml +++ b/adapters/deepagents/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-deepagents" -version = "0.1.0" +version = "0.2.0" description = "LangChain Deep Agents adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.1.0", + "nemo-fabric-adapters-common == 0.2.0", "deepagents>=0.6.12,<0.7.0", "langchain>=1.3,<2.0", "langchain-mcp-adapters>=0.1,<0.3.0", diff --git a/adapters/deepagents/uv.lock b/adapters/deepagents/uv.lock index 72d964e8c..a55ddb68c 100644 --- a/adapters/deepagents/uv.lock +++ b/adapters/deepagents/uv.lock @@ -834,12 +834,12 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "../common" } [[package]] name = "nemo-fabric-adapters-deepagents" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "deepagents" }, diff --git a/adapters/hermes/pyproject.toml b/adapters/hermes/pyproject.toml index 0cc58fb21..d15d866c1 100644 --- a/adapters/hermes/pyproject.toml +++ b/adapters/hermes/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-hermes" -version = "0.1.0" +version = "0.2.0" description = "Hermes Agent adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -26,7 +26,7 @@ readme = "README.md" # Hermes Agent does not currently support Python 3.14 or later. requires-python = ">=3.11,<3.14" dependencies = [ - "nemo-fabric-adapters-common == 0.1.0", + "nemo-fabric-adapters-common == 0.2.0", "pyyaml>=6.0", # needed for writing hermes config files ] diff --git a/adapters/hermes/uv.lock b/adapters/hermes/uv.lock index 2809511b2..c2d5d663c 100644 --- a/adapters/hermes/uv.lock +++ b/adapters/hermes/uv.lock @@ -4,12 +4,12 @@ requires-python = ">=3.11, <3.14" [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "../common" } [[package]] name = "nemo-fabric-adapters-hermes" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-adapters-common" }, diff --git a/pyproject.toml b/pyproject.toml index 0986794bc..230b83d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric" -version = "0.1.0" +version = "0.2.0" description = "Python SDK distribution for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "pypi.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-runtime == 0.1.0", + "nemo-fabric-runtime == 0.2.0", ] [project.urls] @@ -38,15 +38,15 @@ packages = [] [project.optional-dependencies] codex = [ - "nemo-fabric-adapters-codex == 0.1.0", + "nemo-fabric-adapters-codex == 0.2.0", ] claude = [ - "nemo-fabric-adapters-claude == 0.1.0", + "nemo-fabric-adapters-claude == 0.2.0", ] deepagents = [ - "nemo-fabric-adapters-deepagents == 0.1.0", + "nemo-fabric-adapters-deepagents == 0.2.0", ] harbor = [ @@ -55,11 +55,11 @@ harbor = [ ] hermes = [ - "nemo-fabric-adapters-hermes == 0.1.0; python_version < '3.14'", + "nemo-fabric-adapters-hermes == 0.2.0; python_version < '3.14'", ] hermes-agent = [ - "nemo-fabric-adapters-hermes == 0.1.0; python_version < '3.14'", + "nemo-fabric-adapters-hermes == 0.2.0; python_version < '3.14'", "hermes-agent>=0.17.0; python_version < '3.14'", ] @@ -69,7 +69,7 @@ relay = [ ] runtime = [ - "nemo-fabric-runtime == 0.1.0", + "nemo-fabric-runtime == 0.2.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index b42dde975..d7cc43548 100644 --- a/uv.lock +++ b/uv.lock @@ -2047,7 +2047,7 @@ wheels = [ [[package]] name = "nemo-fabric" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-runtime" }, @@ -2169,7 +2169,7 @@ test = [ [[package]] name = "nemo-fabric-adapters-claude" -version = "0.1.0" +version = "0.2.0" source = { editable = "adapters/claude" } dependencies = [ { name = "claude-agent-sdk" }, @@ -2186,7 +2186,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-codex" -version = "0.1.0" +version = "0.2.0" source = { editable = "adapters/codex" } dependencies = [ { name = "nemo-fabric-adapters-common" }, @@ -2203,12 +2203,12 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.1.0" +version = "0.2.0" source = { editable = "adapters/common" } [[package]] name = "nemo-fabric-adapters-deepagents" -version = "0.1.0" +version = "0.2.0" source = { editable = "adapters/deepagents" } dependencies = [ { name = "deepagents" }, @@ -2240,7 +2240,7 @@ provides-extras = ["relay"] [[package]] name = "nemo-fabric-adapters-hermes" -version = "0.1.0" +version = "0.2.0" source = { editable = "adapters/hermes" } dependencies = [ { name = "nemo-fabric-adapters-common", marker = "python_full_version < '3.14'" }, From c49d6930446312759b8351d6dd002c53f24b2c15 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Mon, 27 Jul 2026 08:46:07 -0700 Subject: [PATCH 11/21] fix: read normalized adapter environment Signed-off-by: Ajay Thorve --- adapters/common/src/nemo_fabric_adapters/common/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index bc809028d..f1e58b601 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -91,7 +91,10 @@ def runtime_state_directory(base: str | Path, payload: dict[str, Any]) -> Path: def environment_payload(payload: dict[str, Any]) -> dict[str, Any]: return ( - runtime_context(payload).get("environment") or payload.get("environment") or {} + runtime_context(payload).get("environment") + or fabric_config(payload).get("environment") + or payload.get("environment") + or {} ) From 1593666330805a0fc4ce6ea19dbf0f4a03466057 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Mon, 27 Jul 2026 08:57:18 -0700 Subject: [PATCH 12/21] fix: read normalized shim environment Signed-off-by: Ajay Thorve --- .../src/nemo_fabric_test_adapters/hermes_shim/adapter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py index 087628ef6..642d98ce9 100644 --- a/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py +++ b/tests/fixtures/hermes-shim-agent/adapters/hermes-shim/src/nemo_fabric_test_adapters/hermes_shim/adapter.py @@ -54,7 +54,10 @@ def request_payload(payload: dict[str, Any]) -> dict[str, Any]: def environment_payload(payload: dict[str, Any]) -> dict[str, Any]: return ( - runtime_context(payload).get("environment") or payload.get("environment") or {} + runtime_context(payload).get("environment") + or fabric_config(payload).get("environment") + or payload.get("environment") + or {} ) From f90e425a7dbb0616450911d78c565792b69e193e Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Mon, 27 Jul 2026 19:02:26 -0700 Subject: [PATCH 13/21] docs: address documentation style review Signed-off-by: Ajay Thorve --- adapters/README.md | 8 ++++---- adapters/claude/README.md | 2 +- adapters/codex/README.md | 2 +- adapters/deepagents/README.md | 7 ++++--- adapters/hermes/README.md | 8 ++++---- docs/sdk/python.mdx | 15 ++++++++------- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/adapters/README.md b/adapters/README.md index a1fecb483..81dad5339 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -59,15 +59,15 @@ policy; NeMo Fabric does not reinterpret one as the other. Deep Agents subagents are limited to declarative local subagents that inherit the parent agent's capabilities. -### Complete `FabricConfig` Support +### Complete FabricConfig Support `Core` means NeMo Fabric owns the behavior and applies it uniformly before or around adapter execution. `Yes` means the adapter translates the normalized field into its harness. `No` means an explicitly configured value fails planning instead -of being ignored. Provider-specific Relay subfields and additive extension maps -are grouped because their support does not vary by adapter. +of being ignored. The following table groups provider-specific Relay subfields +and additive extension maps because their support does not vary by adapter: -| `FabricConfig` field | Claude | Codex | Deep Agents | Hermes Agent | +| `FabricConfig` Field | Claude | Codex | Deep Agents | Hermes Agent | | --- | --- | --- | --- | --- | | `schema_version` | Core | Core | Core | Core | | `metadata.name`, `.description` | Core | Core | Core | Core | diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 87dae1630..6f6882fa2 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -88,7 +88,7 @@ Configure portable capabilities through the normalized `FabricConfig` fields: configure an Anthropic Messages-compatible `base_url` and `api_key_env`. - `system_prompt` supplies the Claude system instructions. - `max_turns` sets the Claude turn limit. -- `runtime.timeout_seconds` sets the Fabric invocation deadline. +- `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` sets the Claude working directory, and `environment.env` supplies explicit harness-visible variables. - `tools.blocked` maps to Claude `disallowed_tools` using Claude-native tool diff --git a/adapters/codex/README.md b/adapters/codex/README.md index f9a17107b..7684d328c 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -86,7 +86,7 @@ Use normalized `FabricConfig` fields for portable configuration: authentication and endpoint discovery. Any other provider name must configure a Responses-compatible `base_url` and `api_key_env`. - `system_prompt` maps to Codex base instructions. -- `runtime.timeout_seconds` sets the Fabric invocation deadline. +- `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` sets the working directory, and `environment.env` supplies explicit harness-visible variables. - `mcp` maps stdio, HTTP, and streamable HTTP servers into the Codex thread's diff --git a/adapters/deepagents/README.md b/adapters/deepagents/README.md index bf0be5fd6..cfad839d0 100644 --- a/adapters/deepagents/README.md +++ b/adapters/deepagents/README.md @@ -47,7 +47,7 @@ NeMo Fabric maps the following into the harness: - The selected `models` role supplies `model`, `provider`, `api_key_env`, `base_url`, and `temperature`. - Top-level `system_prompt` becomes the Deep Agents `system_prompt`. -- `runtime.timeout_seconds` sets the Fabric invocation deadline. +- `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` roots the Deep Agents filesystem backend (`FilesystemBackend(root_dir=..., virtual_mode=True)`). `virtual_mode` confines the agent to the workspace: absolute paths and `..` cannot escape @@ -95,8 +95,9 @@ NeMo Fabric starts one local adapter host for every runtime. During runtime star the host compiles one Deep Agents graph, opens its async LangGraph checkpointer, and creates one thread ID. Every invocation reuses those native objects; later turns report `resumed` as `true`. The checkpointer lives under -the Fabric artifact root, scoped by runtime ID, and is closed during runtime -stop. The live host owns the thread identity, and LangGraph owns the transcript. +the NeMo Fabric artifact root, scoped by runtime ID, and is closed during +runtime stop. The live host owns the thread identity, and LangGraph owns the +transcript. `Fabric.run(...)` is a convenience over that same lifecycle: it starts the runtime, invokes it once, and stops it. It does not use a separate adapter diff --git a/adapters/hermes/README.md b/adapters/hermes/README.md index d1f315829..f625f0a52 100644 --- a/adapters/hermes/README.md +++ b/adapters/hermes/README.md @@ -45,10 +45,10 @@ The adapter receives a normalized payload from NeMo Fabric and materializes a na Per-tool `tools.blocked` is not accepted by Hermes because Hermes exposes toolset-level policy at this boundary. Keep Hermes-specific controls such as terminal timeout, reasoning configuration, and plugin configuration in -`harness.settings`. The adapter derives Hermes state from the Fabric artifact -root and creates a child under `runtimes/`, so invocations in one -NeMo Fabric runtime share state without sharing config or the session database -with another runtime. +`harness.settings`. The adapter derives Hermes state from the NeMo Fabric +artifact root and creates a child under `runtimes/`, so invocations +in one NeMo Fabric runtime share state without sharing config or the session +database with another runtime. ## Execution Model diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index 0e60a0d8a..9684ef3fb 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -175,13 +175,14 @@ NeMo Fabric-owned fields such as workspace, environment variables, and the invocation deadline apply to every local adapter. Adapter-translated fields are validated against the selected adapter descriptor during planning. -`Core` means Fabric owns the behavior and applies it uniformly before or around -adapter execution. `Yes` means the adapter translates the normalized field into -its harness. `No` means an explicitly configured value fails planning instead -of being ignored. Provider-specific Relay subfields and additive extension maps -are grouped because their support does not vary by adapter. - -| `FabricConfig` field | Claude | Codex | Deep Agents | Hermes Agent | +`Core` means NeMo Fabric owns the behavior and applies it uniformly before or +around adapter execution. `Yes` means the adapter translates the normalized +field into its harness. `No` means an explicitly configured value fails +planning instead of being ignored. The following table groups provider-specific +Relay subfields and additive extension maps because their support does not vary +by adapter: + +| `FabricConfig` Field | Claude | Codex | Deep Agents | Hermes Agent | | --- | --- | --- | --- | --- | | `schema_version` | Core | Core | Core | Core | | `metadata.name`, `.description` | Core | Core | Core | Core | From 5bc461aa00b331cc4952b6e76d6cc1a3aa979bea Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Mon, 27 Jul 2026 19:19:29 -0700 Subject: [PATCH 14/21] test: make scaffold path assertion portable Signed-off-by: Ajay Thorve --- tests/e2e/test_cli_scaffold.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/test_cli_scaffold.py b/tests/e2e/test_cli_scaffold.py index 8d6d9e3d6..a22841de4 100644 --- a/tests/e2e/test_cli_scaffold.py +++ b/tests/e2e/test_cli_scaffold.py @@ -70,7 +70,7 @@ def test_generated_rust_scaffold_builds(tmp_path: Path): manifest = (destination / "Cargo.toml").read_text() assert "[workspace]" in manifest core_dependency = tomllib.loads(manifest)["dependencies"]["nemo-fabric-core"] - assert Path(core_dependency["path"]) == ROOT / "crates/fabric-core" + assert Path(core_dependency["path"]).samefile(ROOT / "crates/fabric-core") expected_version = tomllib.loads((ROOT / "Cargo.toml").read_text())["workspace"][ "package" ]["version"] From f21f2b475e15ab30aca5f833048f33e542fdd283 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 08:33:35 -0700 Subject: [PATCH 15/21] Revert "Updating versions for v0.2.0" This reverts commit 9ceec1c75e741fa344ef6d00101fc89ce8e37cc7. Signed-off-by: Ajay Thorve --- Cargo.toml | 4 ++-- adapters/claude/pyproject.toml | 4 ++-- adapters/claude/uv.lock | 4 ++-- adapters/codex/pyproject.toml | 4 ++-- adapters/codex/uv.lock | 4 ++-- adapters/common/pyproject.toml | 2 +- adapters/common/uv.lock | 2 +- adapters/deepagents/pyproject.toml | 4 ++-- adapters/deepagents/uv.lock | 4 ++-- adapters/hermes/pyproject.toml | 4 ++-- adapters/hermes/uv.lock | 4 ++-- pyproject.toml | 16 ++++++++-------- uv.lock | 12 ++++++------ 13 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5ec865432..fc77c16f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,13 @@ default-members = [ resolver = "2" [workspace.package] -version = "0.2.0" +version = "0.1.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/NVIDIA/nemo-fabric" [workspace.dependencies] -nemo-fabric-core = { path = "crates/fabric-core", version = "0.2.0" } +nemo-fabric-core = { path = "crates/fabric-core", version = "0.1.0" } clap = { version = "4", default-features = false, features = ["derive", "std", "help", "usage", "error-context", "suggestions"] } schemars = { version = "1", features = ["derive"] } diff --git a/adapters/claude/pyproject.toml b/adapters/claude/pyproject.toml index f35222caa..f80d80986 100644 --- a/adapters/claude/pyproject.toml +++ b/adapters/claude/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-claude" -version = "0.2.0" +version = "0.1.0" description = "Claude adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.2.0", + "nemo-fabric-adapters-common == 0.1.0", "claude-agent-sdk==0.2.120", "tomli-w~=1.2", ] diff --git a/adapters/claude/uv.lock b/adapters/claude/uv.lock index b2b617547..012c18e12 100644 --- a/adapters/claude/uv.lock +++ b/adapters/claude/uv.lock @@ -350,7 +350,7 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-claude" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "claude-agent-sdk" }, @@ -367,7 +367,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "../common" } [[package]] diff --git a/adapters/codex/pyproject.toml b/adapters/codex/pyproject.toml index 63b5d0752..7acd61c0f 100644 --- a/adapters/codex/pyproject.toml +++ b/adapters/codex/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-codex" -version = "0.2.0" +version = "0.1.0" description = "Codex adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.2.0", + "nemo-fabric-adapters-common == 0.1.0", "openai-codex==0.144.4", "tomli-w~=1.2", ] diff --git a/adapters/codex/uv.lock b/adapters/codex/uv.lock index b66fd4f11..a76f126a6 100644 --- a/adapters/codex/uv.lock +++ b/adapters/codex/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-codex" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-adapters-common" }, @@ -30,7 +30,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "../common" } [[package]] diff --git a/adapters/common/pyproject.toml b/adapters/common/pyproject.toml index cd3b63302..e428d1d49 100644 --- a/adapters/common/pyproject.toml +++ b/adapters/common/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" description = "Shared Python helpers for NeMo Fabric adapters" authors = [ { name = "NVIDIA Corporation" }, diff --git a/adapters/common/uv.lock b/adapters/common/uv.lock index b7d8656f9..a9e754d66 100644 --- a/adapters/common/uv.lock +++ b/adapters/common/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11" [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } diff --git a/adapters/deepagents/pyproject.toml b/adapters/deepagents/pyproject.toml index 433ba938c..ceb261b02 100644 --- a/adapters/deepagents/pyproject.toml +++ b/adapters/deepagents/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-deepagents" -version = "0.2.0" +version = "0.1.0" description = "LangChain Deep Agents adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-adapters-common == 0.2.0", + "nemo-fabric-adapters-common == 0.1.0", "deepagents>=0.6.12,<0.7.0", "langchain>=1.3,<2.0", "langchain-mcp-adapters>=0.1,<0.3.0", diff --git a/adapters/deepagents/uv.lock b/adapters/deepagents/uv.lock index a55ddb68c..72d964e8c 100644 --- a/adapters/deepagents/uv.lock +++ b/adapters/deepagents/uv.lock @@ -834,12 +834,12 @@ wheels = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "../common" } [[package]] name = "nemo-fabric-adapters-deepagents" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "deepagents" }, diff --git a/adapters/hermes/pyproject.toml b/adapters/hermes/pyproject.toml index d15d866c1..0cc58fb21 100644 --- a/adapters/hermes/pyproject.toml +++ b/adapters/hermes/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-hermes" -version = "0.2.0" +version = "0.1.0" description = "Hermes Agent adapter for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -26,7 +26,7 @@ readme = "README.md" # Hermes Agent does not currently support Python 3.14 or later. requires-python = ">=3.11,<3.14" dependencies = [ - "nemo-fabric-adapters-common == 0.2.0", + "nemo-fabric-adapters-common == 0.1.0", "pyyaml>=6.0", # needed for writing hermes config files ] diff --git a/adapters/hermes/uv.lock b/adapters/hermes/uv.lock index c2d5d663c..2809511b2 100644 --- a/adapters/hermes/uv.lock +++ b/adapters/hermes/uv.lock @@ -4,12 +4,12 @@ requires-python = ">=3.11, <3.14" [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "../common" } [[package]] name = "nemo-fabric-adapters-hermes" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-adapters-common" }, diff --git a/pyproject.toml b/pyproject.toml index 230b83d2b..0986794bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric" -version = "0.2.0" +version = "0.1.0" description = "Python SDK distribution for NeMo Fabric" authors = [ { name = "NVIDIA Corporation" }, @@ -25,7 +25,7 @@ license-files = ["LICENSE"] readme = "pypi.md" requires-python = ">=3.11" dependencies = [ - "nemo-fabric-runtime == 0.2.0", + "nemo-fabric-runtime == 0.1.0", ] [project.urls] @@ -38,15 +38,15 @@ packages = [] [project.optional-dependencies] codex = [ - "nemo-fabric-adapters-codex == 0.2.0", + "nemo-fabric-adapters-codex == 0.1.0", ] claude = [ - "nemo-fabric-adapters-claude == 0.2.0", + "nemo-fabric-adapters-claude == 0.1.0", ] deepagents = [ - "nemo-fabric-adapters-deepagents == 0.2.0", + "nemo-fabric-adapters-deepagents == 0.1.0", ] harbor = [ @@ -55,11 +55,11 @@ harbor = [ ] hermes = [ - "nemo-fabric-adapters-hermes == 0.2.0; python_version < '3.14'", + "nemo-fabric-adapters-hermes == 0.1.0; python_version < '3.14'", ] hermes-agent = [ - "nemo-fabric-adapters-hermes == 0.2.0; python_version < '3.14'", + "nemo-fabric-adapters-hermes == 0.1.0; python_version < '3.14'", "hermes-agent>=0.17.0; python_version < '3.14'", ] @@ -69,7 +69,7 @@ relay = [ ] runtime = [ - "nemo-fabric-runtime == 0.2.0", + "nemo-fabric-runtime == 0.1.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index d7cc43548..b42dde975 100644 --- a/uv.lock +++ b/uv.lock @@ -2047,7 +2047,7 @@ wheels = [ [[package]] name = "nemo-fabric" -version = "0.2.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "nemo-fabric-runtime" }, @@ -2169,7 +2169,7 @@ test = [ [[package]] name = "nemo-fabric-adapters-claude" -version = "0.2.0" +version = "0.1.0" source = { editable = "adapters/claude" } dependencies = [ { name = "claude-agent-sdk" }, @@ -2186,7 +2186,7 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-codex" -version = "0.2.0" +version = "0.1.0" source = { editable = "adapters/codex" } dependencies = [ { name = "nemo-fabric-adapters-common" }, @@ -2203,12 +2203,12 @@ requires-dist = [ [[package]] name = "nemo-fabric-adapters-common" -version = "0.2.0" +version = "0.1.0" source = { editable = "adapters/common" } [[package]] name = "nemo-fabric-adapters-deepagents" -version = "0.2.0" +version = "0.1.0" source = { editable = "adapters/deepagents" } dependencies = [ { name = "deepagents" }, @@ -2240,7 +2240,7 @@ provides-extras = ["relay"] [[package]] name = "nemo-fabric-adapters-hermes" -version = "0.2.0" +version = "0.1.0" source = { editable = "adapters/hermes" } dependencies = [ { name = "nemo-fabric-adapters-common", marker = "python_full_version < '3.14'" }, From fbcc1c3e6b18972f90b51dc90af3c1f64d86867e Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 10:31:10 -0700 Subject: [PATCH 16/21] feat: normalize instructions and tool policy Signed-off-by: Ajay Thorve --- Cargo.lock | 6 +- adapters/claude/fabric-adapter.json | 5 +- .../nemo_fabric_adapters/claude/adapter.py | 40 +- adapters/codex/fabric-adapter.json | 2 +- .../src/nemo_fabric_adapters/codex/adapter.py | 2 +- .../src/nemo_fabric_adapters/common/utils.py | 31 +- adapters/deepagents/fabric-adapter.json | 3 +- .../deepagents/adapter.py | 46 +- adapters/hermes/fabric-adapter.json | 8 +- .../nemo_fabric_adapters/hermes/adapter.py | 10 +- .../adapters/claude/fabric-adapter.json | 5 +- .../assets/adapters/codex/fabric-adapter.json | 2 +- .../adapters/deepagents/fabric-adapter.json | 3 +- .../adapters/hermes/fabric-adapter.json | 8 +- crates/fabric-cli/src/app.rs | 5 +- crates/fabric-cli/src/presets.rs | 4 +- crates/fabric-cli/src/scaffold.rs | 30 +- .../fabric-cli/templates/python/main.py.tmpl | 6 +- crates/fabric-cli/templates/rust/main.rs.tmpl | 8 +- crates/fabric-core/src/config.rs | 443 ++++++++++-------- crates/fabric-core/src/doctor.rs | 75 ++- crates/fabric-core/src/lib.rs | 12 +- crates/fabric-core/src/schema.rs | 5 +- crates/fabric-python/src/lib.rs | 3 +- examples/code_review_agent/config.py | 25 +- .../adapters/claude/fabric-adapter.json | 5 +- .../adapters/hermes/fabric-adapter.json | 8 +- examples/notebooks/01_quickstart.ipynb | 14 +- examples/notebooks/02_variations.ipynb | 18 +- python/src/nemo_fabric/__init__.py | 6 +- .../integrations/harbor/fabric_agent.py | 49 +- python/src/nemo_fabric/models.py | 84 ++-- python/src/nemo_fabric/types.py | 169 ++++--- schemas/adapter-descriptor.schema.json | 17 +- schemas/agent.schema.json | 112 +++-- schemas/run-plan.schema.json | 160 ++++--- tests/adapters/test_adapaters_common_utils.py | 16 +- tests/adapters/test_claude_adapter.py | 34 +- tests/adapters/test_codex_adapter.py | 6 +- tests/adapters/test_deepagents.py | 18 +- tests/adapters/test_hermes_adapter.py | 20 +- tests/e2e/test_hermes_config_mapping.py | 6 +- tests/integrations/test_harbor_runner.py | 18 +- tests/python/test_native_sdk.py | 4 +- tests/python/test_notebook_examples.py | 10 +- tests/python/test_sdk_contract.py | 61 ++- tests/python/test_typed_config.py | 33 ++ 47 files changed, 988 insertions(+), 667 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57d4f2cc1..afa7716d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "fabric-python" -version = "0.2.0" +version = "0.1.0" dependencies = [ "nemo-fabric-core", "pyo3", @@ -89,7 +89,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "nemo-fabric-cli" -version = "0.2.0" +version = "0.1.0" dependencies = [ "clap", "nemo-fabric-core", @@ -98,7 +98,7 @@ dependencies = [ [[package]] name = "nemo-fabric-core" -version = "0.2.0" +version = "0.1.0" dependencies = [ "schemars", "serde", diff --git a/adapters/claude/fabric-adapter.json b/adapters/claude/fabric-adapter.json index 688f06586..52b11a520 100644 --- a/adapters/claude/fabric-adapter.json +++ b/adapters/claude/fabric-adapter.json @@ -10,8 +10,9 @@ "accepts": [ "models", "models.base_url", - "system_prompt", - "max_turns", + "instructions.system", + "runtime.max_turns", + "tools.enabled", "tools.blocked", "mcp", "skills" diff --git a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py index a7d72d334..81293ad31 100644 --- a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py +++ b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py @@ -29,6 +29,7 @@ from claude_agent_sdk import Message from claude_agent_sdk import ProcessError from claude_agent_sdk import ResultMessage +from claude_agent_sdk import HookMatcher from claude_agent_sdk._errors import MessageParseError from nemo_fabric_adapters.common import lifecycle from nemo_fabric_adapters.common import relay_gateway @@ -482,6 +483,39 @@ def discard_stderr(_: str) -> None: """Consume Claude Code stderr without exposing it through Fabric artifacts.""" +def tool_policy_hooks(payload: dict[str, Any]) -> dict[str, list[HookMatcher]] | None: + """Enforce the normalized tool policy across built-in, MCP, and plugin tools.""" + + enabled = common_utils.enabled_tools(payload) + blocked = set(common_utils.blocked_tools(payload)) + if enabled is None and not blocked: + return None + enabled_set = None if enabled is None else set(enabled) + + async def enforce_policy( + hook_input: dict[str, Any], + _tool_use_id: str | None, + _context: dict[str, Any], + ) -> dict[str, Any]: + tool_name = str(hook_input.get("tool_name") or "") + is_blocked = tool_name in blocked or ( + enabled_set is not None and tool_name not in enabled_set + ) + if not is_blocked: + return {} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": ( + f"Tool '{tool_name}' is blocked by the configured tools policy." + ), + } + } + + return {"PreToolUse": [HookMatcher(hooks=[enforce_policy])]} + + def build_options( payload: dict[str, Any], *, @@ -505,7 +539,8 @@ def build_options( ) cli_path = os.environ.get("FABRIC_TEST_CLAUDE_CLI_PATH") - system_prompt = common_utils.system_prompt(payload) + system_prompt = common_utils.system_instruction(payload) + enabled_tools = common_utils.enabled_tools(payload) plugins = _stage_skill_plugin(payload) has_skill_plugin = bool(plugins) if relay is not None: @@ -515,9 +550,10 @@ def build_options( cwd=resolve_cwd(payload), model=selected_model(payload), system_prompt=system_prompt, - tools=None, + tools=enabled_tools, allowed_tools=_string_list(settings.get("allowed_tools"), name="allowed_tools"), disallowed_tools=common_utils.blocked_tools(payload), + hooks=tool_policy_hooks(payload), permission_mode=permission_mode, max_turns=max_turns, max_budget_usd=max_budget, diff --git a/adapters/codex/fabric-adapter.json b/adapters/codex/fabric-adapter.json index be5ffbbe2..a31540298 100644 --- a/adapters/codex/fabric-adapter.json +++ b/adapters/codex/fabric-adapter.json @@ -10,7 +10,7 @@ "accepts": [ "models", "models.base_url", - "system_prompt", + "instructions.system", "mcp", "skills" ] diff --git a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py index 697ebc594..c8c109925 100644 --- a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py +++ b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py @@ -877,7 +877,7 @@ def _thread_options( settings = _settings(payload) return { "approval_mode": approval_mode(payload), - "base_instructions": common_utils.system_prompt(payload), + "base_instructions": common_utils.system_instruction(payload), "config": thread_config(payload, relay) or None, "cwd": str(resolve_cwd(payload)), "developer_instructions": _optional_string(settings, "developer_instructions"), diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index 861630bec..3c6aedea0 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -123,13 +123,15 @@ def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: return model_config -def system_prompt(payload: dict[str, Any]) -> str | None: - value = fabric_config(payload).get("system_prompt") +def system_instruction(payload: dict[str, Any]) -> str | None: + instructions = fabric_config(payload).get("instructions") or {} + system = instructions.get("system") or {} + value = system.get("content") return value if isinstance(value, str) else None def max_turns(payload: dict[str, Any]) -> int | None: - value = fabric_config(payload).get("max_turns") + value = (fabric_config(payload).get("runtime") or {}).get("max_turns") return value if isinstance(value, int) and not isinstance(value, bool) else None @@ -186,25 +188,16 @@ def tools_config(payload: dict[str, Any]) -> dict[str, Any]: return tools if isinstance(tools, dict) else {} -def blocked_tools(payload: dict[str, Any]) -> list[str]: - blocked = tools_config(payload).get("blocked") - return normalize_list(blocked) - - -def toolsets_config(payload: dict[str, Any]) -> dict[str, Any]: - toolsets = tools_config(payload).get("toolsets") or {} - return toolsets if isinstance(toolsets, dict) else {} - - -def enabled_toolsets(payload: dict[str, Any]) -> list[str] | None: - toolsets = toolsets_config(payload) - if "enabled" not in toolsets: +def enabled_tools(payload: dict[str, Any]) -> list[str] | None: + tools = tools_config(payload) + if "enabled" not in tools: return None - return normalize_list(toolsets.get("enabled")) + return normalize_list(tools.get("enabled")) -def blocked_toolsets(payload: dict[str, Any]) -> list[str]: - return normalize_list(toolsets_config(payload).get("blocked")) +def blocked_tools(payload: dict[str, Any]) -> list[str]: + blocked = tools_config(payload).get("blocked") + return normalize_list(blocked) def normalize_list(value: Any) -> list[str]: diff --git a/adapters/deepagents/fabric-adapter.json b/adapters/deepagents/fabric-adapter.json index 02f50f8fc..8b1d7baff 100644 --- a/adapters/deepagents/fabric-adapter.json +++ b/adapters/deepagents/fabric-adapter.json @@ -12,7 +12,8 @@ "models", "models.base_url", "models.temperature", - "system_prompt", + "instructions.system", + "tools.enabled", "tools.blocked", "mcp", "skills" diff --git a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py index dc51d837e..a05466442 100644 --- a/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py +++ b/adapters/deepagents/src/nemo_fabric_adapters/deepagents/adapter.py @@ -233,17 +233,22 @@ def _blocked_tool_names(payload: dict[str, Any]) -> set[str]: return set(common_utils.blocked_tools(payload)) +def _enabled_tool_names(payload: dict[str, Any]) -> set[str] | None: + enabled = common_utils.enabled_tools(payload) + return None if enabled is None else set(enabled) + + def _tool_gate_middleware( is_blocked: Callable[[Any], bool], message: Callable[[Any], str] ) -> ToolGateMiddleware: return ToolGateMiddleware(is_blocked, message) -def blocked_tools_middleware(blocked: set[str]) -> Any: - """Middleware that blocks explicitly denied tool calls across the full tool surface.""" +def tool_policy_middleware(enabled: set[str] | None, blocked: set[str]) -> Any: + """Enforce tool selection and blocking across the full tool surface.""" return _tool_gate_middleware( - lambda name: name in blocked, + lambda name: name in blocked or (enabled is not None and name not in enabled), lambda name: f"Tool '{name}' is blocked by the configured tools policy.", ) @@ -344,7 +349,7 @@ async def build_agent_kwargs( "model": model, "tools": await resolve_tools(payload), # deepagents 0.5.x/0.6.x take the system prompt as ``system_prompt``. - "system_prompt": common_utils.system_prompt(payload), + "system_prompt": common_utils.system_instruction(payload), "skills": resolve_skills(payload), "backend": resolve_backend(payload), } @@ -353,12 +358,15 @@ async def build_agent_kwargs( extra = settings.get("deepagents") if extra is not None: kwargs.update(_validated_passthrough(extra)) + enabled = _enabled_tool_names(payload) blocked = _blocked_tool_names(payload) - if blocked: + if enabled is not None or blocked: middleware = list(kwargs.get("middleware") or []) - middleware.append(blocked_tools_middleware(blocked)) + middleware.append(tool_policy_middleware(enabled, blocked)) kwargs["middleware"] = middleware - kwargs["subagents"] = _gated_subagents(kwargs.get("subagents"), blocked) + kwargs["subagents"] = _gated_subagents( + kwargs.get("subagents"), enabled, blocked + ) return {key: value for key, value in kwargs.items() if value is not None} @@ -390,46 +398,52 @@ def _validated_passthrough(extra: Any) -> dict[str, Any]: return dict(extra) -def _block_subagent(subagent: dict[str, Any], blocked: set[str]) -> dict[str, Any]: +def _gate_subagent( + subagent: dict[str, Any], enabled: set[str] | None, blocked: set[str] +) -> dict[str, Any]: gated = dict(subagent) gated["middleware"] = [ *(gated.get("middleware") or []), - blocked_tools_middleware(blocked), + tool_policy_middleware(enabled, blocked), ] return gated -def _gated_subagents(subagents: Any, blocked: set[str]) -> list[dict[str, Any]]: +def _gated_subagents( + subagents: Any, enabled: set[str] | None, blocked: set[str] +) -> list[dict[str, Any]]: if subagents is None: configured: list[Any] = [] elif isinstance(subagents, list): configured = subagents else: raise AdapterConfigError( - "harness.settings.deepagents.subagents must be a list when tools.blocked is configured." + "harness.settings.deepagents.subagents must be a list when a tools policy is configured." ) gated: list[dict[str, Any]] = [] for subagent in configured: if not isinstance(subagent, dict): raise AdapterConfigError( - "Deep Agents subagents must be mappings when tools.blocked is configured." + "Deep Agents subagents must be mappings when a tools policy is configured." ) name = str(subagent.get("name") or "") if "graph_id" in subagent: raise AdapterConfigError( - f"tools.blocked cannot be enforced for remote Deep Agents subagent '{name}'." + f"the tools policy cannot be enforced for remote Deep Agents subagent '{name}'." ) if "runnable" in subagent: raise AdapterConfigError( - f"tools.blocked cannot be enforced for precompiled Deep Agents subagent '{name}'." + f"the tools policy cannot be enforced for precompiled Deep Agents subagent '{name}'." ) - gated.append(_block_subagent(subagent, blocked)) + gated.append(_gate_subagent(subagent, enabled, blocked)) if not any(subagent.get("name") == "general-purpose" for subagent in gated): from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT - gated.insert(0, _block_subagent(dict(GENERAL_PURPOSE_SUBAGENT), blocked)) + gated.insert( + 0, _gate_subagent(dict(GENERAL_PURPOSE_SUBAGENT), enabled, blocked) + ) return gated diff --git a/adapters/hermes/fabric-adapter.json b/adapters/hermes/fabric-adapter.json index 53ff125e8..c77f3836d 100644 --- a/adapters/hermes/fabric-adapter.json +++ b/adapters/hermes/fabric-adapter.json @@ -12,10 +12,10 @@ "models", "models.base_url", "models.temperature", - "system_prompt", - "max_turns", - "tools.toolsets.enabled", - "tools.toolsets.blocked", + "instructions.system", + "runtime.max_turns", + "tools.enabled", + "tools.blocked", "mcp", "skills" ] diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 2c8e028cd..c4ae1f252 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -23,7 +23,7 @@ from nemo_fabric_adapters.common import lifecycle import nemo_fabric_adapters.common.utils as common_utils -# Default agent loop budget when FabricConfig.max_turns is unset. +# Default agent loop budget when FabricConfig.runtime.max_turns is unset. # Mirrors Hermes' own AIAgent default (agent/agent_init.py); a lower value such # as 1 silently starves multi-step tasks (they run out of budget before # answering while the trial still reports success). See FABRIC-85. @@ -77,7 +77,7 @@ def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: def disabled_toolsets(payload: dict[str, Any]) -> list[str]: - return common_utils.blocked_toolsets(payload) + return common_utils.blocked_tools(payload) def build_hermes_config( @@ -92,7 +92,7 @@ def build_hermes_config( provider = model_config.get("provider") base_url = common_utils.get_base_url(model_config) blocked_toolsets = disabled_toolsets(payload) - enabled_toolsets = common_utils.enabled_toolsets(payload) + enabled_toolsets = common_utils.enabled_tools(payload) config: dict[str, Any] = { "model": common_utils.without_none( @@ -194,7 +194,7 @@ def main() -> None: def resolve_hermes_toolsets( payload: dict[str, Any], config: dict[str, Any] ) -> list[str] | None: - enabled = common_utils.enabled_toolsets(payload) + enabled = common_utils.enabled_tools(payload) if enabled is not None: return enabled @@ -364,7 +364,7 @@ async def invoke(self, invocation: dict[str, Any]) -> dict[str, Any]: def invoke_turn() -> tuple[dict[str, Any], str]: return _invoke_hermes_turn( agent=self._agent, - system_prompt=common_utils.system_prompt(start_payload), + system_prompt=common_utils.system_instruction(start_payload), user_message=user_message, conversation_history=self._conversation_history, ) diff --git a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json index 688f06586..52b11a520 100644 --- a/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/claude/fabric-adapter.json @@ -10,8 +10,9 @@ "accepts": [ "models", "models.base_url", - "system_prompt", - "max_turns", + "instructions.system", + "runtime.max_turns", + "tools.enabled", "tools.blocked", "mcp", "skills" diff --git a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json index be5ffbbe2..a31540298 100644 --- a/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/codex/fabric-adapter.json @@ -10,7 +10,7 @@ "accepts": [ "models", "models.base_url", - "system_prompt", + "instructions.system", "mcp", "skills" ] diff --git a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json index 02f50f8fc..8b1d7baff 100644 --- a/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/deepagents/fabric-adapter.json @@ -12,7 +12,8 @@ "models", "models.base_url", "models.temperature", - "system_prompt", + "instructions.system", + "tools.enabled", "tools.blocked", "mcp", "skills" diff --git a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json index 53ff125e8..c77f3836d 100644 --- a/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json +++ b/crates/fabric-cli/assets/adapters/hermes/fabric-adapter.json @@ -12,10 +12,10 @@ "models", "models.base_url", "models.temperature", - "system_prompt", - "max_turns", - "tools.toolsets.enabled", - "tools.toolsets.blocked", + "instructions.system", + "runtime.max_turns", + "tools.enabled", + "tools.blocked", "mcp", "skills" ] diff --git a/crates/fabric-cli/src/app.rs b/crates/fabric-cli/src/app.rs index dfe547ec3..d9faf9c26 100644 --- a/crates/fabric-cli/src/app.rs +++ b/crates/fabric-cli/src/app.rs @@ -6,7 +6,8 @@ use clap::error::ErrorKind; use clap::{ArgGroup, Args, Parser, Subcommand}; use nemo_fabric_core::{ - ResolveContext, RunRequest, RunStatus, doctor_plan, resolve_run_plan_from_config, run_plan, + ResolveContext, RunRequest, RunStatus, doctor_plan, resolve_diagnostic_plan_from_config, + resolve_run_plan_from_config, run_plan, }; use crate::examples; @@ -238,7 +239,7 @@ fn run(cli: Cli) -> Result<(), Box> { } Command::Doctor(selector) => { let selected = select_source(&selector)?; - let plan = resolve_run_plan_from_config( + let plan = resolve_diagnostic_plan_from_config( selected.config().clone(), ResolveContext::new(selected.base_dir()), )?; diff --git a/crates/fabric-cli/src/presets.rs b/crates/fabric-cli/src/presets.rs index 9696adb3b..c8bf3cd3d 100644 --- a/crates/fabric-cli/src/presets.rs +++ b/crates/fabric-cli/src/presets.rs @@ -271,13 +271,13 @@ fn config( models: default_model .map(|model| BTreeMap::from_iter([("default".to_string(), model)])) .unwrap_or_default(), - system_prompt: None, - max_turns: None, + instructions: None, runtime: RuntimeConfig { input_schema: "text".to_string(), output_schema: "message".to_string(), artifacts: None, timeout_seconds: None, + max_turns: None, extensions: BTreeMap::new(), }, environment: Some(EnvironmentConfig { diff --git a/crates/fabric-cli/src/scaffold.rs b/crates/fabric-cli/src/scaffold.rs index e3054d65a..97b387ee7 100644 --- a/crates/fabric-cli/src/scaffold.rs +++ b/crates/fabric-cli/src/scaffold.rs @@ -165,16 +165,23 @@ fn render_python(config: &FabricConfig) -> String { &python_value(&Value::Object(config.harness.settings.clone())), ) .replace( - "{{SYSTEM_PROMPT}}", + "{{INSTRUCTIONS}}", &config - .system_prompt - .as_deref() - .map(python_string) + .instructions + .as_ref() + .and_then(|instructions| instructions.system.as_ref()) + .map(|instruction| { + format!( + "InstructionsConfig(system=InstructionConfig(content={}, mode=\"replace\"))", + python_string(&instruction.content) + ) + }) .unwrap_or_else(|| "None".to_string()), ) .replace( "{{MAX_TURNS}}", &config + .runtime .max_turns .map(|value| value.to_string()) .unwrap_or_else(|| "None".to_string()), @@ -274,16 +281,23 @@ fn render_rust(config: &FabricConfig) -> String { &rust_settings(&config.harness.settings), ) .replace( - "{{SYSTEM_PROMPT}}", + "{{INSTRUCTIONS}}", &config - .system_prompt - .as_deref() - .map(|value| format!("Some({}.to_string())", rust_string(value))) + .instructions + .as_ref() + .and_then(|instructions| instructions.system.as_ref()) + .map(|instruction| { + format!( + "Some(InstructionsConfig {{ system: Some(InstructionConfig {{ content: {}.to_string(), mode: InstructionMode::Replace, extensions: BTreeMap::new() }}), extensions: BTreeMap::new() }})", + rust_string(&instruction.content) + ) + }) .unwrap_or_else(|| "None".to_string()), ) .replace( "{{MAX_TURNS}}", &config + .runtime .max_turns .map(|value| format!("Some({value})")) .unwrap_or_else(|| "None".to_string()), diff --git a/crates/fabric-cli/templates/python/main.py.tmpl b/crates/fabric-cli/templates/python/main.py.tmpl index f27bde37c..67ce7df90 100644 --- a/crates/fabric-cli/templates/python/main.py.tmpl +++ b/crates/fabric-cli/templates/python/main.py.tmpl @@ -12,6 +12,8 @@ from nemo_fabric import EnvironmentConfig from nemo_fabric import Fabric from nemo_fabric import FabricConfig from nemo_fabric import HarnessConfig +from nemo_fabric import InstructionConfig +from nemo_fabric import InstructionsConfig from nemo_fabric import MetadataConfig from nemo_fabric import ModelConfig from nemo_fabric import RuntimeConfig @@ -30,12 +32,12 @@ def build_config() -> FabricConfig: settings={{HARNESS_SETTINGS}}, ), models={{MODELS}}, - system_prompt={{SYSTEM_PROMPT}}, - max_turns={{MAX_TURNS}}, + instructions={{INSTRUCTIONS}}, runtime=RuntimeConfig( input_schema="text", output_schema="message", timeout_seconds={{TIMEOUT_SECONDS}}, + max_turns={{MAX_TURNS}}, ), environment=EnvironmentConfig( provider="local", diff --git a/crates/fabric-cli/templates/rust/main.rs.tmpl b/crates/fabric-cli/templates/rust/main.rs.tmpl index d430fd87c..ddb1caa4d 100644 --- a/crates/fabric-cli/templates/rust/main.rs.tmpl +++ b/crates/fabric-cli/templates/rust/main.rs.tmpl @@ -6,8 +6,8 @@ use std::path::PathBuf; use nemo_fabric_core::{ ControlLocation, EnvironmentConfig, EnvironmentOwnership, FabricConfig, HarnessConfig, - MetadataConfig, ResolutionStrategy, ResolveContext, RunRequest, RuntimeConfig, SkillConfig, - resolve_run_plan_from_config, run_plan, + InstructionConfig, InstructionMode, InstructionsConfig, MetadataConfig, ResolutionStrategy, + ResolveContext, RunRequest, RuntimeConfig, SkillConfig, resolve_run_plan_from_config, run_plan, }; use serde_json::Map; @@ -26,13 +26,13 @@ fn build_config() -> FabricConfig { extensions: BTreeMap::new(), }, models: {{MODELS}}, - system_prompt: {{SYSTEM_PROMPT}}, - max_turns: {{MAX_TURNS}}, + instructions: {{INSTRUCTIONS}}, runtime: RuntimeConfig { input_schema: "text".to_string(), output_schema: "message".to_string(), artifacts: None, timeout_seconds: {{TIMEOUT_SECONDS}}, + max_turns: {{MAX_TURNS}}, extensions: BTreeMap::new(), }, environment: Some(EnvironmentConfig { diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 5db152e21..aa7211144 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -32,14 +32,10 @@ pub struct FabricConfig { /// Named model roles. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub models: BTreeMap, - /// Portable system instructions for the selected harness. + /// Portable agent instructions for the selected harness. #[serde(default, skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - /// Maximum number of harness turns within one invocation. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[schemars(range(min = 1))] - pub max_turns: Option, - /// Runtime input/output contract. + pub instructions: Option, + /// Invocation runtime contract. pub runtime: RuntimeConfig, /// Environment where the harness or its tools execute. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -64,30 +60,49 @@ pub struct FabricConfig { pub extensions: BTreeMap, } -/// Harness-neutral tool capability configuration. +/// How an instruction value is applied to the selected harness. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum InstructionMode { + /// Replace the harness default instruction value. + #[default] + Replace, +} + +/// One portable instruction value. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct InstructionConfig { + /// Instruction text. + pub content: String, + /// How the instruction is applied. + #[serde(default)] + pub mode: InstructionMode, + /// Additive instruction fields. + #[serde(default, flatten)] + pub extensions: BTreeMap, +} + +/// Harness-neutral agent instruction configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct ToolsConfig { - /// Adapter-native tool names to block. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub blocked: Vec, - /// Harness-defined toolset selection and blocking policy. +pub struct InstructionsConfig { + /// System instructions for the selected harness. #[serde(default, skip_serializing_if = "Option::is_none")] - pub toolsets: Option, - /// Additive tool configuration fields. + pub system: Option, + /// Additive instruction categories. #[serde(default, flatten)] pub extensions: BTreeMap, } -/// Harness-defined toolset capability configuration. +/// Harness-neutral tool capability configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct ToolsetConfig { - /// Toolsets to expose. `None` preserves the harness default; an empty list exposes none. +pub struct ToolsConfig { + /// Adapter-native tool names to expose. `None` preserves the harness default. #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option>, - /// Toolsets to exclude from the enabled or default harness toolset set. + /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, - /// Additive toolset configuration fields. + /// Additive tool configuration fields. #[serde(default, flatten)] pub extensions: BTreeMap, } @@ -382,20 +397,17 @@ pub enum AdapterConfigField { #[serde(rename = "models.temperature")] ModelTemperature, /// Portable system instructions. - #[serde(rename = "system_prompt")] - SystemPrompt, + #[serde(rename = "instructions.system")] + SystemInstructions, /// Per-invocation harness turn limit. - #[serde(rename = "max_turns")] + #[serde(rename = "runtime.max_turns")] MaxTurns, + /// Adapter-native tool names to expose. + #[serde(rename = "tools.enabled")] + EnabledTools, /// Adapter-native tool names to block. #[serde(rename = "tools.blocked")] BlockedTools, - /// Harness-defined toolsets to expose. - #[serde(rename = "tools.toolsets.enabled")] - EnabledToolsets, - /// Harness-defined toolsets to block. - #[serde(rename = "tools.toolsets.blocked")] - BlockedToolsets, /// Harness-native MCP servers. #[serde(rename = "mcp")] Mcp, @@ -483,7 +495,7 @@ pub struct ModelConfig { pub extensions: BTreeMap, } -/// Runtime input/output contract. +/// Invocation runtime contract. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct RuntimeConfig { /// Input schema label. @@ -499,6 +511,10 @@ pub struct RuntimeConfig { #[serde(default, skip_serializing_if = "Option::is_none")] #[schemars(extend("exclusiveMinimum" = 0.0))] pub timeout_seconds: Option, + /// Maximum number of harness turns within one invocation. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] + pub max_turns: Option, /// Additive normalized runtime fields. #[serde(default, flatten)] pub extensions: BTreeMap, @@ -1081,8 +1097,8 @@ fn validate_config(config: &FabricConfig) -> Result<()> { available: Vec::new(), }); } - if config.max_turns == Some(0) { - return invalid_config("max_turns", "must be greater than zero"); + if config.runtime.max_turns == Some(0) { + return invalid_config("runtime.max_turns", "must be greater than zero"); } if let Some(timeout) = config.runtime.timeout_seconds && (!timeout.is_finite() @@ -1134,19 +1150,16 @@ fn validate_config(config: &FabricConfig) -> Result<()> { } } if let Some(tools) = &config.tools { - validate_names("tools.blocked", &tools.blocked)?; - if let Some(toolsets) = &tools.toolsets { - if let Some(enabled) = &toolsets.enabled { - validate_names("tools.toolsets.enabled", enabled)?; - if let Some(name) = enabled.iter().find(|name| toolsets.blocked.contains(name)) { - return invalid_config( - "tools.toolsets", - format!("`{name}` cannot be both enabled and blocked"), - ); - } + if let Some(enabled) = &tools.enabled { + validate_names("tools.enabled", enabled)?; + if let Some(name) = enabled.iter().find(|name| tools.blocked.contains(name)) { + return invalid_config( + "tools", + format!("`{name}` cannot be both enabled and blocked"), + ); } - validate_names("tools.toolsets.blocked", &toolsets.blocked)?; } + validate_names("tools.blocked", &tools.blocked)?; } Ok(()) } @@ -1186,6 +1199,44 @@ pub fn resolve_run_plan_from_config_with_adapter_directories( config: FabricConfig, context: ResolveContext, adapter_directories: &[PathBuf], +) -> Result { + resolve_run_plan_from_config_with_adapter_directories_mode( + config, + context, + adapter_directories, + true, + ) +} + +/// Resolve a typed Fabric config while retaining adapter incompatibilities for diagnostics. +#[doc(hidden)] +pub fn resolve_diagnostic_plan_from_config( + config: FabricConfig, + context: ResolveContext, +) -> Result { + resolve_diagnostic_plan_from_config_with_adapter_directories(config, context, &[]) +} + +/// Resolve a diagnostic plan with additional adapter descriptor directories. +#[doc(hidden)] +pub fn resolve_diagnostic_plan_from_config_with_adapter_directories( + config: FabricConfig, + context: ResolveContext, + adapter_directories: &[PathBuf], +) -> Result { + resolve_run_plan_from_config_with_adapter_directories_mode( + config, + context, + adapter_directories, + false, + ) +} + +fn resolve_run_plan_from_config_with_adapter_directories_mode( + config: FabricConfig, + context: ResolveContext, + adapter_directories: &[PathBuf], + enforce_compatibility: bool, ) -> Result { validate_config(&config)?; let supplied_base_dir = context.base_dir; @@ -1195,7 +1246,7 @@ pub fn resolve_run_plan_from_config_with_adapter_directories( path: supplied_base_dir, source, })?; - resolve_run_plan(config, base_dir, adapter_directories) + resolve_run_plan(config, base_dir, adapter_directories, enforce_compatibility) } fn read_json(path: &Path) -> Result @@ -1216,17 +1267,22 @@ fn resolve_run_plan( config: FabricConfig, base_dir: PathBuf, adapter_directories: &[PathBuf], + enforce_compatibility: bool, ) -> Result { let adapter_descriptor = resolve_adapter_descriptor(&config, &base_dir, adapter_directories)?; let descriptor = adapter_descriptor .as_ref() .map(|adapter| &adapter.descriptor); - validate_adapter_config_compatibility(&config, descriptor)?; + if enforce_compatibility { + validate_adapter_config_compatibility(&config, descriptor)?; + } let resolution = resolve_resolution(&config, descriptor)?; let environment_plan = resolve_environment_plan(&config, &base_dir); validate_control_location(descriptor, environment_plan.as_ref())?; let capability_plan = resolve_capability_plan(&config, &base_dir, adapter_descriptor.as_ref()); - validate_capability_plan_compatibility(&capability_plan, descriptor)?; + if enforce_compatibility { + validate_capability_plan_compatibility(&capability_plan, descriptor)?; + } let capabilities = resolve_runtime_capabilities(&config, descriptor); let telemetry_plan = resolve_telemetry_plan(&config, descriptor)?; Ok(RunPlan { @@ -1266,62 +1322,94 @@ fn validate_adapter_config_compatibility( config: &FabricConfig, descriptor: Option<&AdapterDescriptor>, ) -> Result<()> { - let Some(descriptor) = descriptor else { + let Some(issue) = adapter_config_compatibility_issues(config, descriptor) + .into_iter() + .next() + else { return Ok(()); }; + Err(FabricError::AdapterCompatibility { + adapter_id: issue.adapter_id, + field: issue.field, + reason: issue.reason, + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AdapterCompatibilityIssue { + pub(crate) adapter_id: String, + pub(crate) field: String, + pub(crate) reason: String, +} + +pub(crate) fn adapter_config_compatibility_issues( + config: &FabricConfig, + descriptor: Option<&AdapterDescriptor>, +) -> Vec { + let Some(descriptor) = descriptor else { + return Vec::new(); + }; let accepts = |field: AdapterConfigField| descriptor.config.accepts.contains(&field); - let incompatible = |field: String, reason: String| { - Err(FabricError::AdapterCompatibility { - adapter_id: descriptor.adapter_id.clone(), - reason, - field, - }) + let incompatible = |field: String, reason: String| AdapterCompatibilityIssue { + adapter_id: descriptor.adapter_id.clone(), + reason, + field, }; + let mut issues = Vec::new(); - if config.system_prompt.is_some() && !accepts(AdapterConfigField::SystemPrompt) { - return incompatible( - "system_prompt".to_string(), + if config + .instructions + .as_ref() + .and_then(|instructions| instructions.system.as_ref()) + .is_some() + && !accepts(AdapterConfigField::SystemInstructions) + { + issues.push(incompatible( + "instructions.system".to_string(), "the adapter does not declare an equivalent native mapping".to_string(), - ); + )); } - if config.max_turns.is_some() && !accepts(AdapterConfigField::MaxTurns) { - return incompatible( - "max_turns".to_string(), + if config.runtime.max_turns.is_some() && !accepts(AdapterConfigField::MaxTurns) { + issues.push(incompatible( + "runtime.max_turns".to_string(), "the adapter does not declare an equivalent native mapping".to_string(), - ); + )); } if !config.models.is_empty() && !accepts(AdapterConfigField::Models) { - return incompatible( + issues.push(incompatible( "models".to_string(), "the adapter does not consume normalized model configuration".to_string(), - ); - } + )); + return issues; + }; + let selected_model = match (config.models.get_key_value("default"), config.models.len()) { (Some(model), _) => Some(model), (None, 0) => None, (None, 1) => config.models.first_key_value(), (None, _) => { - return incompatible( + issues.push(incompatible( "models".to_string(), "multiple model roles are configured and no default role selects one".to_string(), - ); + )); + None } }; if let Some((role, model)) = selected_model { if model.base_url.is_some() && !accepts(AdapterConfigField::ModelBaseUrl) { - return incompatible( + issues.push(incompatible( format!("models.{role}.base_url"), "the adapter does not declare custom endpoint support".to_string(), - ); + )); } if model.temperature.is_some() && !accepts(AdapterConfigField::ModelTemperature) { - return incompatible( + issues.push(incompatible( format!("models.{role}.temperature"), "the adapter does not declare an equivalent native mapping".to_string(), - ); + )); } } - Ok(()) + issues } fn resolve_adapter_descriptor( @@ -1489,61 +1577,35 @@ fn resolve_capability_plan( .collect() }) .unwrap_or_default(); + let enabled_tools = config + .tools + .as_ref() + .and_then(|tools| tools.enabled.clone()); let blocked_tools = config .tools .as_ref() .map(|tools| tools.blocked.clone()) .unwrap_or_default(); - let toolsets = config - .tools - .as_ref() - .and_then(|tools| tools.toolsets.clone()); + let enabled_tools_configured = enabled_tools.is_some(); let blocked_tools_configured = !blocked_tools.is_empty(); - let enabled_toolsets_configured = toolsets - .as_ref() - .is_some_and(|toolsets| toolsets.enabled.is_some()); - let blocked_toolsets_configured = toolsets - .as_ref() - .is_some_and(|toolsets| !toolsets.blocked.is_empty()); - let tools_configured = - blocked_tools_configured || enabled_toolsets_configured || blocked_toolsets_configured; + let tools_configured = enabled_tools_configured || blocked_tools_configured; let mut native = CapabilityTargetPlan::default(); let managed = CapabilityTargetPlan::default(); let mut unsupported = CapabilityTargetPlan::default(); let mut routes = Vec::new(); - if blocked_tools_configured { - if accepts(AdapterConfigField::BlockedTools) { - native.tools_configured = true; - routes.push(CapabilityRoute { - kind: CapabilityKind::Tools, - name: "tools.blocked".to_string(), - target: CapabilityTarget::HarnessNative, - reason: "selected adapter explicitly supports the NeMo Fabric blocked-tools policy" - .to_string(), - }); - } else { - unsupported.tools_configured = true; - routes.push(CapabilityRoute { - kind: CapabilityKind::Tools, - name: "tools.blocked".to_string(), - target: CapabilityTarget::Unsupported, - reason: "selected adapter does not explicitly declare blocked-tools policy support and NeMo Fabric-managed enforcement is not implemented".to_string(), - }); - } - } for (configured, support, field, description) in [ ( - enabled_toolsets_configured, - AdapterConfigField::EnabledToolsets, - "tools.toolsets.enabled", - "enabled-toolsets selection", + enabled_tools_configured, + AdapterConfigField::EnabledTools, + "tools.enabled", + "enabled-tools selection", ), ( - blocked_toolsets_configured, - AdapterConfigField::BlockedToolsets, - "tools.toolsets.blocked", - "blocked-toolsets policy", + blocked_tools_configured, + AdapterConfigField::BlockedTools, + "tools.blocked", + "blocked-tools policy", ), ] { if !configured { @@ -1555,7 +1617,9 @@ fn resolve_capability_plan( kind: CapabilityKind::Tools, name: field.to_string(), target: CapabilityTarget::HarnessNative, - reason: format!("selected adapter explicitly supports the Fabric {description}"), + reason: format!( + "selected adapter explicitly supports the NeMo Fabric {description}" + ), }); } else { unsupported.tools_configured = true; @@ -1564,7 +1628,7 @@ fn resolve_capability_plan( name: field.to_string(), target: CapabilityTarget::Unsupported, reason: format!( - "selected adapter does not explicitly declare {description} support" + "selected adapter does not explicitly declare {description} support and NeMo Fabric-managed enforcement is not implemented" ), }); } @@ -1622,8 +1686,8 @@ fn resolve_capability_plan( CapabilityPlan { tools: ToolsPlan { + enabled: enabled_tools, blocked: blocked_tools, - toolsets, }, tools_configured, skill_paths, @@ -1858,12 +1922,12 @@ pub struct CapabilityPlan { /// Normalized tool policy for a run. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ToolsPlan { + /// Adapter-native tool names to expose. `None` preserves the harness default. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option>, /// Adapter-native tool names to block. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocked: Vec, - /// Harness-defined toolset selection and blocking policy. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub toolsets: Option, } /// Capabilities routed to one target. @@ -1880,14 +1944,17 @@ pub struct CapabilityTargetPlan { pub mcp_servers: BTreeMap, } -/// One capability routing decision. +/// One capability execution assignment. +/// +/// Routes apply to executable tool, skill, and MCP capabilities. Adapter-translated +/// scalar configuration is validated separately against [`AdapterConfigSupport`]. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct CapabilityRoute { /// Capability kind. pub kind: CapabilityKind, /// Capability name. pub name: String, - /// Routing target. + /// Component responsible for executing the capability. pub target: CapabilityTarget, /// Human-readable reason for the selected route. pub reason: String, @@ -1915,15 +1982,17 @@ pub enum CapabilityKind { Mcp, } -/// Capability routing target. +/// Component responsible for executing a configured capability. +/// +/// This target describes execution ownership, not network routing. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CapabilityTarget { - /// Adapter maps the capability into harness-native config. + /// The selected adapter maps and executes the capability through its harness. HarnessNative, - /// NeMo Fabric exposes or manages the capability around the harness. + /// NeMo Fabric executes the capability outside the harness-native surface. FabricManaged, - /// Capability is configured but no executable surface exists. + /// Neither the adapter nor NeMo Fabric can execute the configured capability. Unsupported, } @@ -2081,8 +2150,15 @@ mod tests { #[test] fn normalized_fields_survive_planning() { let mut config = typed_config("nvidia.fabric.hermes"); - config.system_prompt = Some("Be concise.".to_string()); - config.max_turns = Some(7); + config.instructions = Some(InstructionsConfig { + system: Some(InstructionConfig { + content: "Be concise.".to_string(), + mode: InstructionMode::Replace, + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + config.runtime.max_turns = Some(7); config.runtime.timeout_seconds = Some(12.5); config.environment.as_mut().expect("environment").env = BTreeMap::from([("VISIBLE".to_string(), "yes".to_string())]); @@ -2099,12 +2175,8 @@ mod tests { }, ); config.tools = Some(ToolsConfig { - blocked: Vec::new(), - toolsets: Some(ToolsetConfig { - enabled: Some(vec!["terminal".to_string()]), - blocked: vec!["browser".to_string()], - extensions: BTreeMap::new(), - }), + enabled: Some(vec!["terminal".to_string()]), + blocked: vec!["browser".to_string()], extensions: BTreeMap::new(), }); @@ -2112,8 +2184,15 @@ mod tests { resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-normalized")) .expect("normalized plan"); - assert_eq!(plan.config.system_prompt.as_deref(), Some("Be concise.")); - assert_eq!(plan.config.max_turns, Some(7)); + assert_eq!( + plan.config + .instructions + .as_ref() + .and_then(|instructions| instructions.system.as_ref()) + .map(|instruction| instruction.content.as_str()), + Some("Be concise.") + ); + assert_eq!(plan.config.runtime.max_turns, Some(7)); assert_eq!(plan.config.runtime.timeout_seconds, Some(12.5)); assert_eq!( plan.environment_plan @@ -2122,16 +2201,11 @@ mod tests { Some(&"yes".to_string()) ); assert_eq!( - plan.capability_plan - .tools - .toolsets - .as_ref() - .and_then(|toolsets| toolsets.enabled.as_ref()), + plan.capability_plan.tools.enabled.as_ref(), Some(&vec!["terminal".to_string()]) ); assert!(plan.capability_plan.routes.iter().any(|route| { - route.name == "tools.toolsets.enabled" - && route.target == CapabilityTarget::HarnessNative + route.name == "tools.enabled" && route.target == CapabilityTarget::HarnessNative })); } @@ -2139,7 +2213,7 @@ mod tests { fn unsupported_normalized_scalar_reports_adapter_config_incompatibility() { for adapter_id in ["nvidia.fabric.codex", "nvidia.fabric.langchain.deepagents"] { let mut config = typed_config(adapter_id); - config.max_turns = Some(3); + config.runtime.max_turns = Some(3); let error = resolve_run_plan_from_config( config, @@ -2153,7 +2227,7 @@ mod tests { adapter_id: actual, field, .. - } if actual == adapter_id && field == "max_turns" + } if actual == adapter_id && field == "runtime.max_turns" )); } } @@ -2196,36 +2270,27 @@ mod tests { } #[test] - fn unsupported_toolsets_report_canonical_field() { - for adapter_id in [ - "nvidia.fabric.claude", - "nvidia.fabric.codex", - "nvidia.fabric.langchain.deepagents", - ] { - let mut config = typed_config(adapter_id); - config.tools = Some(ToolsConfig { - blocked: Vec::new(), - toolsets: Some(ToolsetConfig { - enabled: Some(vec!["terminal".to_string()]), - blocked: Vec::new(), - extensions: BTreeMap::new(), - }), - extensions: BTreeMap::new(), - }); + fn unsupported_enabled_tools_report_canonical_field() { + let adapter_id = "nvidia.fabric.codex"; + let mut config = typed_config(adapter_id); + config.tools = Some(ToolsConfig { + enabled: Some(vec!["terminal".to_string()]), + blocked: Vec::new(), + extensions: BTreeMap::new(), + }); - let error = - resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-toolsets")) - .expect_err("adapter does not advertise enabled toolsets"); + let error = + resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-enabled-tools")) + .expect_err("adapter does not advertise enabled tools"); - assert!(matches!( - error, - FabricError::AdapterCompatibility { - adapter_id: actual, - field, - .. - } if actual == adapter_id && field == "tools.toolsets.enabled" - )); - } + assert!(matches!( + error, + FabricError::AdapterCompatibility { + adapter_id: actual, + field, + .. + } if actual == adapter_id && field == "tools.enabled" + )); } #[test] @@ -2278,15 +2343,11 @@ mod tests { } #[test] - fn tool_and_toolset_policies_are_routed_independently() { + fn enabled_and_blocked_tool_policies_are_routed_independently() { let mut config = typed_config("nvidia.fabric.hermes"); config.tools = Some(ToolsConfig { - blocked: Vec::new(), - toolsets: Some(ToolsetConfig { - enabled: Some(Vec::new()), - blocked: vec!["browser".to_string()], - extensions: BTreeMap::new(), - }), + enabled: Some(Vec::new()), + blocked: vec!["browser".to_string()], extensions: BTreeMap::new(), }); @@ -2294,21 +2355,19 @@ mod tests { .expect("tool capability plan"); assert!(plan.capability_plan.routes.iter().any(|route| { - route.name == "tools.toolsets.enabled" - && route.target == CapabilityTarget::HarnessNative + route.name == "tools.enabled" && route.target == CapabilityTarget::HarnessNative })); assert!(plan.capability_plan.routes.iter().any(|route| { - route.name == "tools.toolsets.blocked" - && route.target == CapabilityTarget::HarnessNative + route.name == "tools.blocked" && route.target == CapabilityTarget::HarnessNative })); } #[test] fn unsupported_tool_policy_fails_during_planning() { - let mut config = typed_config("nvidia.fabric.hermes"); + let mut config = typed_config("nvidia.fabric.codex"); config.tools = Some(ToolsConfig { + enabled: None, blocked: vec!["Bash".to_string()], - toolsets: None, extensions: BTreeMap::new(), }); @@ -2316,7 +2375,7 @@ mod tests { config, ResolveContext::new("/tmp/fabric-unsupported-tools"), ) - .expect_err("Hermes does not support per-tool blocking"); + .expect_err("Codex does not support per-tool blocking"); assert!(matches!( error, @@ -2324,7 +2383,7 @@ mod tests { adapter_id, field, .. - } if adapter_id == "nvidia.fabric.hermes" && field == "tools.blocked" + } if adapter_id == "nvidia.fabric.codex" && field == "tools.blocked" )); } @@ -2361,25 +2420,21 @@ mod tests { } #[test] - fn overlapping_toolset_policy_is_invalid() { + fn overlapping_tool_policy_is_invalid() { let mut config = typed_config("nvidia.fabric.hermes"); config.tools = Some(ToolsConfig { - blocked: Vec::new(), - toolsets: Some(ToolsetConfig { - enabled: Some(vec!["browser".to_string()]), - blocked: vec!["browser".to_string()], - extensions: BTreeMap::new(), - }), + enabled: Some(vec!["browser".to_string()]), + blocked: vec!["browser".to_string()], extensions: BTreeMap::new(), }); let error = resolve_run_plan_from_config(config, ResolveContext::new("/tmp/fabric-invalid-tools")) - .expect_err("overlapping toolset policy"); + .expect_err("overlapping tool policy"); assert!(matches!( error, - FabricError::InvalidConfig { field, .. } if field == "tools.toolsets" + FabricError::InvalidConfig { field, .. } if field == "tools" )); } diff --git a/crates/fabric-core/src/doctor.rs b/crates/fabric-core/src/doctor.rs index 8327311cb..a97ceeb2b 100644 --- a/crates/fabric-core/src/doctor.rs +++ b/crates/fabric-core/src/doctor.rs @@ -12,7 +12,7 @@ use serde_json::Value; use crate::config::{ AdapterKind, CapabilityKind, CapabilityTarget, ControlLocation, EnvironmentOwnership, - ResolutionStrategy, RunPlan, + ResolutionStrategy, RunPlan, adapter_config_compatibility_issues, }; /// Diagnostic status. @@ -56,6 +56,7 @@ pub struct DoctorReport { pub fn doctor_plan(plan: &RunPlan) -> DoctorReport { let mut checks = Vec::new(); checks.push(check_adapter_descriptor(plan)); + checks.extend(check_adapter_config_compatibility(plan)); checks.push(check_resolution(plan)); checks.extend(check_runtime_execution_surface(plan)); checks.push(check_environment_context(plan)); @@ -71,6 +72,31 @@ pub fn doctor_plan(plan: &RunPlan) -> DoctorReport { } } +fn check_adapter_config_compatibility(plan: &RunPlan) -> Vec { + adapter_config_compatibility_issues( + &plan.config, + plan.adapter_descriptor + .as_ref() + .map(|adapter| &adapter.descriptor), + ) + .into_iter() + .map(|issue| { + let mut metadata = BTreeMap::new(); + metadata.insert("adapter_id".to_string(), Value::String(issue.adapter_id)); + metadata.insert("field".to_string(), Value::String(issue.field.clone())); + check_with_metadata( + "config.unsupported", + DoctorStatus::Fail, + format!( + "configuration at `{}` cannot be implemented by the selected adapter: {}", + issue.field, issue.reason + ), + metadata, + ) + }) + .collect() +} + fn check_adapter_descriptor(plan: &RunPlan) -> DoctorCheck { if let Some(adapter) = &plan.adapter_descriptor { let mut metadata = BTreeMap::new(); @@ -467,7 +493,11 @@ fn worst(left: DoctorStatus, right: DoctorStatus) -> DoctorStatus { #[cfg(test)] mod tests { use super::*; - use crate::config::{FabricConfig, ResolveContext, resolve_run_plan_from_config}; + use crate::config::{ + FabricConfig, ResolveContext, resolve_diagnostic_plan_from_config, + resolve_run_plan_from_config, + }; + use crate::error::FabricError; #[test] fn diagnoses_a_typed_plan_without_file_source_fields() { @@ -504,4 +534,45 @@ mod tests { .any(|check| check.message.contains(&expected_command)) ); } + + #[test] + fn diagnoses_unsupported_config_without_weakening_strict_planning() { + let config: FabricConfig = serde_json::from_value(serde_json::json!({ + "schema_version": "fabric.agent/v1alpha1", + "metadata": {"name": "incompatible-agent"}, + "harness": { + "adapter_id": "nvidia.fabric.codex", + "resolution": "preinstalled" + }, + "runtime": {"max_turns": 3}, + "tools": {"enabled": []} + })) + .expect("typed config"); + let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + + let strict_error = + resolve_run_plan_from_config(config.clone(), ResolveContext::new(&base_dir)) + .expect_err("strict planning must reject unsupported config"); + assert!(matches!( + strict_error, + FabricError::AdapterCompatibility { .. } + )); + + let plan = resolve_diagnostic_plan_from_config(config, ResolveContext::new(base_dir)) + .expect("diagnostic plan"); + let report = doctor_plan(&plan); + + assert_eq!(report.status, DoctorStatus::Fail); + assert!(report.checks.iter().any(|check| { + check.name == "config.unsupported" + && check.status == DoctorStatus::Fail + && check.metadata.get("field") + == Some(&Value::String("runtime.max_turns".to_string())) + })); + assert!(report.checks.iter().any(|check| { + check.name == "capability.unsupported" + && check.status == DoctorStatus::Fail + && check.message.contains("tools.enabled") + })); + } } diff --git a/crates/fabric-core/src/lib.rs b/crates/fabric-core/src/lib.rs index 717c55f23..dd6c0bc7f 100644 --- a/crates/fabric-core/src/lib.rs +++ b/crates/fabric-core/src/lib.rs @@ -13,11 +13,13 @@ pub use config::{ ADAPTER_CONTRACT_VERSION, AdapterConfigField, AdapterConfigSupport, AdapterDescriptor, AdapterDescriptorSource, AdapterKind, AdapterRequirements, AdapterTelemetryProviderSupport, AdapterTelemetrySupport, CapabilityPlan, ControlLocation, EnvironmentConfig, - EnvironmentOwnership, EnvironmentPlan, FabricConfig, HarnessConfig, McpConfig, McpExposure, - McpServerPlan, MetadataConfig, ModelConfig, ResolutionStrategy, ResolveContext, - ResolvedAdapterDescriptor, RunPlan, RuntimeCapabilities, RuntimeConfig, SkillConfig, - TelemetryConfig, TelemetryPlan, TelemetryProvider, TelemetryProviderConfig, ToolsConfig, - ToolsetConfig, load_adapter_descriptor, resolve_run_plan_from_config, + EnvironmentOwnership, EnvironmentPlan, FabricConfig, HarnessConfig, InstructionConfig, + InstructionMode, InstructionsConfig, McpConfig, McpExposure, McpServerPlan, MetadataConfig, + ModelConfig, ResolutionStrategy, ResolveContext, ResolvedAdapterDescriptor, RunPlan, + RuntimeCapabilities, RuntimeConfig, SkillConfig, TelemetryConfig, TelemetryPlan, + TelemetryProvider, TelemetryProviderConfig, ToolsConfig, load_adapter_descriptor, + resolve_diagnostic_plan_from_config, + resolve_diagnostic_plan_from_config_with_adapter_directories, resolve_run_plan_from_config, resolve_run_plan_from_config_with_adapter_directories, }; pub use doctor::{DoctorCheck, DoctorReport, DoctorStatus, doctor_plan}; diff --git a/crates/fabric-core/src/schema.rs b/crates/fabric-core/src/schema.rs index 645555bbb..53fb983b9 100644 --- a/crates/fabric-core/src/schema.rs +++ b/crates/fabric-core/src/schema.rs @@ -216,7 +216,10 @@ mod tests { fn agent_schema_enforces_positive_invocation_limits() { let schema = generate_schema(SchemaName::Agent).expect("schema generation"); - assert_eq!(schema["properties"]["max_turns"]["minimum"], 1); + assert_eq!( + schema["$defs"]["RuntimeConfig"]["properties"]["max_turns"]["minimum"], + 1 + ); assert_eq!( schema["$defs"]["RuntimeConfig"]["properties"]["timeout_seconds"]["exclusiveMinimum"], 0.0 diff --git a/crates/fabric-python/src/lib.rs b/crates/fabric-python/src/lib.rs index 0cdc81fcf..975dfff37 100644 --- a/crates/fabric-python/src/lib.rs +++ b/crates/fabric-python/src/lib.rs @@ -11,6 +11,7 @@ use std::time::{Duration, Instant}; use nemo_fabric_core::{ FabricConfig, ResolveContext, RunPlan, RunRequest, RuntimeHandle, doctor_plan, + resolve_diagnostic_plan_from_config_with_adapter_directories, resolve_run_plan_from_config_with_adapter_directories, run_plan, }; use pyo3::exceptions::PyRuntimeError; @@ -66,7 +67,7 @@ fn doctor_config( let (context, adapter_directories) = resolve_context(py, base_dir, &config)?; let plan = py .detach(|| { - resolve_run_plan_from_config_with_adapter_directories( + resolve_diagnostic_plan_from_config_with_adapter_directories( config, context, &adapter_directories, diff --git a/examples/code_review_agent/config.py b/examples/code_review_agent/config.py index 803eae0ba..91614be63 100644 --- a/examples/code_review_agent/config.py +++ b/examples/code_review_agent/config.py @@ -10,6 +10,8 @@ from nemo_fabric import EnvironmentConfig from nemo_fabric import FabricConfig from nemo_fabric import HarnessConfig +from nemo_fabric import InstructionConfig +from nemo_fabric import InstructionsConfig from nemo_fabric import MetadataConfig from nemo_fabric import ModelConfig from nemo_fabric import RelayAtifConfig @@ -19,6 +21,7 @@ from nemo_fabric import RelayOtlpConfig from nemo_fabric import RuntimeConfig from nemo_fabric import TelemetryConfig +from nemo_fabric import ToolsConfig BASE_DIR = Path(__file__).resolve().parent WORKSPACE = "./repos/my-service" @@ -77,13 +80,15 @@ def hermes_config() -> FabricConfig: model = config.models["default"] assert isinstance(model, ModelConfig) model.base_url = "https://integrate.api.nvidia.com/v1" - config.system_prompt = "You are a concise smoke test assistant." - config.max_turns = 1 - config.configure_toolsets(enabled=[]) + config.instructions = InstructionsConfig( + system=InstructionConfig(content="You are a concise smoke test assistant.") + ) + config.tools = ToolsConfig(enabled=[]) config.runtime = RuntimeConfig( input_schema="chat", output_schema="message", artifacts="./artifacts/hermes", + max_turns=1, ) config.environment = EnvironmentConfig( provider="local", @@ -129,7 +134,9 @@ def deepagents_config() -> FabricConfig: resolution="preinstalled", settings={}, ) - config.system_prompt = "You are a concise smoke test assistant." + config.instructions = InstructionsConfig( + system=InstructionConfig(content="You are a concise smoke test assistant.") + ) config.runtime = RuntimeConfig( input_schema="chat", output_schema="message", @@ -148,7 +155,7 @@ def claude_config() -> FabricConfig: """Return the complete Claude adapter variant. The Claude adapter reads the working directory from ``environment.workspace`` - and reads portable instructions from ``FabricConfig.system_prompt``. + and reads portable instructions from ``FabricConfig.instructions.system``. Claude-specific controls such as ``permission_mode`` stay in ``harness.settings``. """ @@ -161,8 +168,12 @@ def claude_config() -> FabricConfig: "permission_mode": "dontAsk", }, ) - config.system_prompt = ( - "You are a concise code reviewer. Point out correctness bugs and risks." + config.instructions = InstructionsConfig( + system=InstructionConfig( + content=( + "You are a concise code reviewer. Point out correctness bugs and risks." + ) + ) ) config.models = { "default": ModelConfig( diff --git a/examples/harbor/swebench/adapters/claude/fabric-adapter.json b/examples/harbor/swebench/adapters/claude/fabric-adapter.json index 688f06586..52b11a520 100644 --- a/examples/harbor/swebench/adapters/claude/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/claude/fabric-adapter.json @@ -10,8 +10,9 @@ "accepts": [ "models", "models.base_url", - "system_prompt", - "max_turns", + "instructions.system", + "runtime.max_turns", + "tools.enabled", "tools.blocked", "mcp", "skills" diff --git a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json index 53ff125e8..c77f3836d 100644 --- a/examples/harbor/swebench/adapters/hermes/fabric-adapter.json +++ b/examples/harbor/swebench/adapters/hermes/fabric-adapter.json @@ -12,10 +12,10 @@ "models", "models.base_url", "models.temperature", - "system_prompt", - "max_turns", - "tools.toolsets.enabled", - "tools.toolsets.blocked", + "instructions.system", + "runtime.max_turns", + "tools.enabled", + "tools.blocked", "mcp", "skills" ] diff --git a/examples/notebooks/01_quickstart.ipynb b/examples/notebooks/01_quickstart.ipynb index 0e70e6456..3f1f2820f 100644 --- a/examples/notebooks/01_quickstart.ipynb +++ b/examples/notebooks/01_quickstart.ipynb @@ -187,11 +187,12 @@ " Fabric,\n", " FabricConfig,\n", " HarnessConfig,\n", + " InstructionConfig,\n", + " InstructionsConfig,\n", " MetadataConfig,\n", " ModelConfig,\n", " RuntimeConfig,\n", " ToolsConfig,\n", - " ToolsetConfig,\n", ")\n", "\n", "WORKSPACE = REPO_ROOT / \"examples\" / \"code_review_agent\" / \"repos\" / \"my-service\"\n", @@ -224,15 +225,20 @@ " api_key_env=\"NVIDIA_API_KEY\",\n", " )\n", " },\n", - " system_prompt=\"You are a code reviewer. Point out correctness bugs and risks, concisely.\",\n", - " max_turns=1,\n", + " instructions=InstructionsConfig(\n", + " system=InstructionConfig(\n", + " content=\"You are a code reviewer. Point out correctness bugs and risks, concisely.\",\n", + " mode=\"replace\",\n", + " ),\n", + " ),\n", " environment=EnvironmentConfig(provider=\"local\", workspace=WORKSPACE),\n", - " tools=ToolsConfig(toolsets=ToolsetConfig(enabled=[])),\n", + " tools=ToolsConfig(enabled=[]),\n", " # The logical input/output contract and the artifact root for this agent.\n", " runtime=RuntimeConfig(\n", " input_schema=\"chat\",\n", " output_schema=\"message\",\n", " artifacts=str(ARTIFACTS),\n", + " max_turns=1,\n", " ),\n", ")\n", "print(\"configured:\", config.metadata.name)" diff --git a/examples/notebooks/02_variations.ipynb b/examples/notebooks/02_variations.ipynb index 1089a5652..d3cac2f18 100644 --- a/examples/notebooks/02_variations.ipynb +++ b/examples/notebooks/02_variations.ipynb @@ -104,6 +104,8 @@ "from nemo_fabric import (\n", " Fabric,\n", " HarnessConfig,\n", + " InstructionConfig,\n", + " InstructionsConfig,\n", " ModelConfig,\n", " RelayAtofConfig,\n", " RelayAtofFileSinkConfig,\n", @@ -184,7 +186,11 @@ " adapter_id=harness[\"adapter_id\"], resolution=\"preinstalled\",\n", " settings={**harness[\"settings\"], \"python\": harness[\"python\"]},\n", " )\n", + " cfg.instructions = InstructionsConfig(\n", + " system=InstructionConfig(content=INSTRUCTION, mode=\"replace\"),\n", + " )\n", " cfg.models = {\"default\": harness[\"model\"]}\n", + " cfg.runtime.max_turns = harness.get(\"max_turns\")\n", " cfg.runtime.input_schema = harness[\"input_schema\"] # Codex requires \"text\"\n", " return cfg\n", "\n", @@ -198,14 +204,13 @@ " {\"name\": \"Hermes\", \"adapter_id\": \"nvidia.fabric.hermes\", \"python\": HERMES_PY,\n", " \"input_schema\": \"chat\", \"model\": NVIDIA_MODEL, \"key\": \"NVIDIA_API_KEY\",\n", " \"needs\": \"a Hermes install (repo README's Hermes quick start) + NVIDIA_API_KEY\",\n", - " \"settings\": {\"system_prompt\": INSTRUCTION, \"workspace\": WORKSPACE,\n", - " \"base_url\": \"https://integrate.api.nvidia.com/v1\",\n", - " \"max_iterations\": 1, \"max_tokens\": 512,\n", - " \"reasoning_config\": {\"effort\": \"none\"}, \"enabled_toolsets\": []}},\n", + " \"max_turns\": 1,\n", + " \"settings\": {\"max_tokens\": 512,\n", + " \"reasoning_config\": {\"effort\": \"none\"}}},\n", " {\"name\": \"Deep Agents\", \"adapter_id\": \"nvidia.fabric.langchain.deepagents\", \"python\": FABRIC_PY,\n", " \"input_schema\": \"chat\", \"model\": NVIDIA_MODEL, \"key\": \"NVIDIA_API_KEY\",\n", " \"needs\": \"the Deep Agents adapter (nemo-fabric[deepagents]) + NVIDIA_API_KEY\",\n", - " \"settings\": {\"system_prompt\": INSTRUCTION, \"workspace\": WORKSPACE}},\n", + " \"settings\": {}},\n", " {\"name\": \"Codex\", \"adapter_id\": \"nvidia.fabric.codex\", \"python\": FABRIC_PY,\n", " \"input_schema\": \"text\",\n", " \"model\": ModelConfig(provider=\"openai\", model=\"openai/gpt-5.4\"),\n", @@ -216,7 +221,8 @@ " \"model\": ModelConfig(provider=\"anthropic\", model=\"anthropic/claude-sonnet-4-5\",\n", " api_key_env=\"ANTHROPIC_API_KEY\"),\n", " \"needs\": \"the Claude adapter + ANTHROPIC_API_KEY\",\n", - " \"settings\": {\"system_prompt\": INSTRUCTION, \"permission_mode\": \"dontAsk\"}},\n", + " \"max_turns\": 1,\n", + " \"settings\": {\"permission_mode\": \"dontAsk\"}},\n", "]\n", "\n", "\n", diff --git a/python/src/nemo_fabric/__init__.py b/python/src/nemo_fabric/__init__.py index 65bd1b848..79bfd8d84 100644 --- a/python/src/nemo_fabric/__init__.py +++ b/python/src/nemo_fabric/__init__.py @@ -14,6 +14,8 @@ from nemo_fabric.models import FabricBaseModel from nemo_fabric.models import FabricConfig from nemo_fabric.models import HarnessConfig +from nemo_fabric.models import InstructionConfig +from nemo_fabric.models import InstructionsConfig from nemo_fabric.models import McpConfig from nemo_fabric.models import McpServerConfig from nemo_fabric.models import MetadataConfig @@ -35,7 +37,6 @@ from nemo_fabric.models import TelemetryConfig from nemo_fabric.models import TelemetryProviderConfig from nemo_fabric.models import ToolsConfig -from nemo_fabric.models import ToolsetConfig from nemo_fabric.runtime import Runtime from nemo_fabric.runtime import RuntimeStatus from nemo_fabric.streaming import InvokeStream @@ -69,6 +70,8 @@ "FabricError", "FabricEvent", "HarnessConfig", + "InstructionConfig", + "InstructionsConfig", "InvokeStream", "McpConfig", "McpServerConfig", @@ -102,5 +105,4 @@ "TelemetryProviderConfig", "TelemetryRef", "ToolsConfig", - "ToolsetConfig", ] diff --git a/python/src/nemo_fabric/integrations/harbor/fabric_agent.py b/python/src/nemo_fabric/integrations/harbor/fabric_agent.py index 9f428b7a9..12ad830a5 100644 --- a/python/src/nemo_fabric/integrations/harbor/fabric_agent.py +++ b/python/src/nemo_fabric/integrations/harbor/fabric_agent.py @@ -19,6 +19,8 @@ from nemo_fabric import EnvironmentConfig from nemo_fabric import FabricConfig from nemo_fabric import HarnessConfig +from nemo_fabric import InstructionConfig +from nemo_fabric import InstructionsConfig from nemo_fabric import MetadataConfig from nemo_fabric import ModelConfig from nemo_fabric import RelayAtifConfig @@ -28,7 +30,6 @@ from nemo_fabric import RunRequest from nemo_fabric import RunResult from nemo_fabric import RuntimeConfig -from nemo_fabric import ToolsetConfig from nemo_fabric import ToolsConfig from nemo_fabric.integrations.harbor.models import FabricRunPayload from nemo_fabric.integrations.harbor.models import HarborMcpServer @@ -101,13 +102,12 @@ def __init__( fabric_workspace: str = HARBOR_DEFAULT_WORKSPACE, fabric_harness_settings: dict[str, Any] | None = None, fabric_model_base_url: str | None = None, - fabric_system_prompt: str | None = None, + fabric_system_instruction: str | None = None, fabric_max_turns: int | None = None, fabric_runtime_timeout_seconds: float | None = None, fabric_environment_env: dict[str, str] | None = None, fabric_blocked_tools: list[str] | None = None, - fabric_enabled_toolsets: list[str] | None = None, - fabric_blocked_toolsets: list[str] | None = None, + fabric_enabled_tools: list[str] | None = None, fabric_telemetry: Literal["none", "relay"] = "none", fabric_python: str = "python3", fabric_package: str | None = None, @@ -145,17 +145,16 @@ def __init__( self.fabric_workspace = str(workspace) self.fabric_harness_settings = dict(fabric_harness_settings or {}) self.fabric_model_base_url = fabric_model_base_url - self.fabric_system_prompt = fabric_system_prompt + self.fabric_system_instruction = fabric_system_instruction self.fabric_max_turns = fabric_max_turns self.fabric_runtime_timeout_seconds = fabric_runtime_timeout_seconds self.fabric_environment_env = dict(fabric_environment_env or {}) self.fabric_blocked_tools = blocked_tools - self.fabric_enabled_toolsets = ( - list(fabric_enabled_toolsets) - if fabric_enabled_toolsets is not None + self.fabric_enabled_tools = ( + list(fabric_enabled_tools) + if fabric_enabled_tools is not None else None ) - self.fabric_blocked_toolsets = list(fabric_blocked_toolsets or []) self.fabric_telemetry = fabric_telemetry self.fabric_python = fabric_python self.fabric_package = fabric_package @@ -279,13 +278,12 @@ def _build_config(self) -> FabricConfig: workspace=self.fabric_workspace, harness_settings=self.fabric_harness_settings, model_base_url=self.fabric_model_base_url, - system_prompt=self.fabric_system_prompt, + system_instruction=self.fabric_system_instruction, max_turns=self.fabric_max_turns, timeout_seconds=self.fabric_runtime_timeout_seconds, environment_env=self.fabric_environment_env, blocked_tools=self.fabric_blocked_tools, - enabled_toolsets=self.fabric_enabled_toolsets, - blocked_toolsets=self.fabric_blocked_toolsets, + enabled_tools=self.fabric_enabled_tools, telemetry=self.fabric_telemetry, model_name=self.model_name, skills_dir=self.skills_dir, @@ -367,13 +365,12 @@ def build_harbor_config( workspace: str, harness_settings: dict[str, Any] | None = None, model_base_url: str | None = None, - system_prompt: str | None = None, + system_instruction: str | None = None, max_turns: int | None = None, timeout_seconds: float | None = None, environment_env: dict[str, str] | None = None, blocked_tools: list[str] | None = None, - enabled_toolsets: list[str] | None = None, - blocked_toolsets: list[str] | None = None, + enabled_tools: list[str] | None = None, telemetry: Literal["none", "relay"] = "none", model_name: str | None = None, skills_dir: str | Path | None = None, @@ -394,14 +391,6 @@ def build_harbor_config( "IS_SANDBOX": "1", **(environment_env or {}), } - toolsets = ( - ToolsetConfig( - enabled=enabled_toolsets, - blocked=list(blocked_toolsets or []), - ) - if enabled_toolsets is not None or blocked_toolsets - else None - ) config = FabricConfig( metadata=MetadataConfig( name=name, @@ -417,6 +406,7 @@ def build_harbor_config( output_schema="message", artifacts=artifact_root, timeout_seconds=timeout_seconds, + max_turns=max_turns, ), environment=EnvironmentConfig( provider="local", @@ -424,14 +414,19 @@ def build_harbor_config( artifacts=artifact_root, env=dict(environment_env or {}), ), - system_prompt=system_prompt, - max_turns=max_turns, + instructions=( + InstructionsConfig( + system=InstructionConfig(content=system_instruction), + ) + if system_instruction is not None + else None + ), tools=( ToolsConfig( + enabled=enabled_tools, blocked=list(blocked_tools or []), - toolsets=toolsets, ) - if blocked_tools or toolsets is not None + if blocked_tools or enabled_tools is not None else None ), ) diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index cd44cec10..feb0cc315 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -105,8 +105,21 @@ class HarnessConfig(FabricBaseModel): settings: dict[str, Any] = Field(default_factory=dict) +class InstructionConfig(FabricBaseModel): + """One portable instruction value.""" + + content: str + mode: Literal["replace"] = "replace" + + +class InstructionsConfig(FabricBaseModel): + """Harness-neutral agent instructions.""" + + system: InstructionConfig | None = None + + class RuntimeConfig(FabricBaseModel): - """Runtime input/output contract.""" + """Invocation runtime contract.""" input_schema: str | None = None output_schema: str | None = None @@ -116,6 +129,7 @@ class RuntimeConfig(FabricBaseModel): gt=0, allow_inf_nan=False, ) + max_turns: int | None = Field(default=None, gt=0) class EnvironmentConfig(FabricBaseModel): @@ -456,58 +470,35 @@ def remove_provider(self, provider: Literal["relay", "native"]) -> Self: return self -class ToolsetConfig(FabricBaseModel): - """Harness-defined toolset selection and blocking policy. - - ``enabled=None`` preserves the selected harness default, while an empty - ``enabled`` list exposes no toolsets. ``blocked`` excludes toolsets from - either the enabled list or the harness default. - """ +class ToolsConfig(FabricBaseModel): + """Harness-neutral tool capability configuration.""" enabled: list[str] | None = Field( default=None, description=( - "Toolsets to expose. None preserves the harness default; an empty list " - "exposes no toolsets." - ), - ) - blocked: list[str] = Field( - default_factory=list, - description=( - "Toolsets to exclude from the enabled or default harness toolset set." + "Adapter-native tools to expose. None preserves the harness default; " + "an empty list exposes no tools." ), ) + blocked: list[str] = Field(default_factory=list) + + @field_validator("enabled", "blocked") + @classmethod + def _validate_tools(cls, value: list[str] | None) -> list[str] | None: + if value is not None and any(not tool.strip() for tool in value): + raise ValueError("tool names must not be empty") + return value @model_validator(mode="after") def _validate_policy(self) -> Self: - for field, values in ( - ("enabled", self.enabled or []), - ("blocked", self.blocked), - ): - if any(not value.strip() for value in values): - raise ValueError(f"tools.toolsets.{field} entries must not be empty") if self.enabled is not None: overlap = set(self.enabled).intersection(self.blocked) if overlap: name = sorted(overlap)[0] - raise ValueError(f"toolset {name!r} cannot be both enabled and blocked") + raise ValueError(f"tool {name!r} cannot be both enabled and blocked") return self -class ToolsConfig(FabricBaseModel): - """Harness-neutral tool capability configuration.""" - - blocked: list[str] = Field(default_factory=list) - toolsets: ToolsetConfig | None = None - - @field_validator("blocked") - @classmethod - def _validate_blocked(cls, value: list[str]) -> list[str]: - if any(not tool.strip() for tool in value): - raise ValueError("tools.blocked entries must not be empty") - return value - - class FabricConfig(FabricBaseModel): """SDK-facing typed NeMo Fabric agent configuration. @@ -523,8 +514,7 @@ class FabricConfig(FabricBaseModel): runtime: RuntimeConfig = Field(default_factory=RuntimeConfig) environment: EnvironmentConfig | None = None models: dict[str, ModelConfig] = Field(default_factory=dict) - system_prompt: str | None = None - max_turns: int | None = Field(default=None, gt=0) + instructions: InstructionsConfig | None = None mcp: McpConfig | None = None skills: SkillConfig | None = None telemetry: TelemetryConfig | None = None @@ -605,22 +595,6 @@ def block_tools(self, *tools: str) -> Self: self.tools.blocked = existing return self - def configure_toolsets( - self, - *, - enabled: Sequence[str] | None = None, - blocked: Sequence[str] = (), - ) -> Self: - """Set harness-defined toolset selection and blocking policy.""" - - if self.tools is None: - self.tools = ToolsConfig() - self.tools.toolsets = ToolsetConfig( - enabled=None if enabled is None else list(enabled), - blocked=list(blocked), - ) - return self - def enable_relay( self, *, diff --git a/python/src/nemo_fabric/types.py b/python/src/nemo_fabric/types.py index eb14f36a6..295370ede 100644 --- a/python/src/nemo_fabric/types.py +++ b/python/src/nemo_fabric/types.py @@ -214,18 +214,84 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_HarnessConfig": ) +class _InstructionConfig(_ConfigMapping): + """One portable instruction value.""" + + _fields = frozenset({"content", "mode"}) + + def __init__( + self, + *, + content: str, + mode: str = "replace", + extra_fields: Mapping[str, Any] | None = None, + ) -> None: + if mode != "replace": + raise FabricConfigError("instruction mode must be replace") + super().__init__( + { + "content": content + if isinstance(content, str) + else _required_text(content, "instruction content"), + "mode": mode, + }, + extra_fields=extra_fields, + ) + + @classmethod + def from_mapping(cls, value: Mapping[str, Any]) -> "_InstructionConfig": + """Validate an instruction mapping and preserve extension fields.""" + + data = _mapping(value, "instruction") + return cls( + content=data.get("content"), + mode=data.get("mode", "replace"), + extra_fields={key: item for key, item in data.items() if key not in cls._fields}, + ) + + +class _InstructionsConfig(_ConfigMapping): + """Harness-neutral agent instructions.""" + + _fields = frozenset({"system"}) + + def __init__( + self, + *, + system: _InstructionConfig | Mapping[str, Any] | None = None, + extra_fields: Mapping[str, Any] | None = None, + ) -> None: + values: dict[str, Any] = {} + if system is not None: + values["system"] = _coerce(_InstructionConfig, system, "system instruction") + super().__init__(values, extra_fields=extra_fields) + + @classmethod + def from_mapping(cls, value: Mapping[str, Any]) -> "_InstructionsConfig": + """Validate an instructions mapping and preserve extension fields.""" + + data = _mapping(value, "instructions") + return cls( + system=data.get("system"), + extra_fields={key: item for key, item in data.items() if key not in cls._fields}, + ) + + class _RuntimeConfig(_ConfigMapping): - """Runtime input/output contract. + """Invocation runtime contract. Attributes: input_schema: Optional logical input contract identifier. output_schema: Optional logical output contract identifier. artifacts: Optional artifact-root path. timeout_seconds: Optional invocation deadline in seconds. + max_turns: Optional harness turn limit. extra_fields: Preserved extension fields not recognized by this SDK. """ - _fields = frozenset({"input_schema", "output_schema", "artifacts", "timeout_seconds"}) + _fields = frozenset( + {"input_schema", "output_schema", "artifacts", "timeout_seconds", "max_turns"} + ) def __init__( self, @@ -234,6 +300,7 @@ def __init__( output_schema: str | None = None, artifacts: str | Path | None = None, timeout_seconds: float | None = None, + max_turns: int | None = None, extra_fields: Mapping[str, Any] | None = None, ) -> None: values: dict[str, Any] = {} @@ -255,6 +322,14 @@ def __init__( "runtime timeout_seconds must be a finite number greater than zero" ) values["timeout_seconds"] = float(timeout_seconds) + if max_turns is not None: + if ( + isinstance(max_turns, bool) + or not isinstance(max_turns, int) + or max_turns <= 0 + ): + raise FabricConfigError("runtime max_turns must be greater than zero") + values["max_turns"] = max_turns super().__init__(values, extra_fields=extra_fields) @classmethod @@ -267,6 +342,7 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_RuntimeConfig": output_schema=data.get("output_schema"), artifacts=data.get("artifacts"), timeout_seconds=data.get("timeout_seconds"), + max_turns=data.get("max_turns"), extra_fields={key: item for key, item in data.items() if key not in cls._fields}, ) @@ -386,8 +462,8 @@ def remove_path(self, path: str | Path) -> "_SkillConfig": return self -class _ToolsetConfig(_ConfigMapping): - """Toolset selection and blocking policy.""" +class _ToolsConfig(_ConfigMapping): + """Harness-neutral tool capability configuration.""" _fields = frozenset({"enabled", "blocked"}) _omit_if_empty = frozenset({"blocked"}) @@ -404,72 +480,33 @@ def __init__( isinstance(values, (str, bytes)) or not isinstance(values, Sequence) ): raise FabricConfigError( - f"tools toolsets {name} must be an ordered sequence of strings" + f"tools {name} must be an ordered sequence of strings" ) enabled_values = ( None if enabled is None - else [_required_text(toolset, "enabled toolset") for toolset in enabled] + else [_required_text(tool, "enabled tool") for tool in enabled] ) blocked_values = [ - _required_text(toolset, "blocked toolset") for toolset in (blocked or []) + _required_text(tool, "blocked tool") for tool in (blocked or []) ] overlap = set(enabled_values or []).intersection(blocked_values) if overlap: name = sorted(overlap)[0] - raise FabricConfigError(f"toolset {name!r} cannot be both enabled and blocked") + raise FabricConfigError(f"tool {name!r} cannot be both enabled and blocked") values: dict[str, Any] = {"blocked": blocked_values} if enabled_values is not None: values["enabled"] = enabled_values super().__init__(values, extra_fields=extra_fields) - @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> "_ToolsetConfig": - """Validate a toolset mapping and preserve extension fields.""" - - data = _mapping(value, "tools toolsets") - return cls( - enabled=data.get("enabled"), - blocked=data.get("blocked", []), - extra_fields={key: item for key, item in data.items() if key not in cls._fields}, - ) - - -class _ToolsConfig(_ConfigMapping): - """Harness-neutral tool capability configuration.""" - - _fields = frozenset({"blocked", "toolsets"}) - _omit_if_empty = frozenset({"blocked"}) - - def __init__( - self, - *, - blocked: Sequence[str] | None = None, - toolsets: _ToolsetConfig | Mapping[str, Any] | None = None, - extra_fields: Mapping[str, Any] | None = None, - ) -> None: - if blocked is not None and ( - isinstance(blocked, (str, bytes)) or not isinstance(blocked, Sequence) - ): - raise FabricConfigError("tools blocked must be an ordered sequence of strings") - values: dict[str, Any] = { - "blocked": [_required_text(tool, "blocked tool") for tool in (blocked or [])] - } - if toolsets is not None: - values["toolsets"] = _coerce(_ToolsetConfig, toolsets, "tools toolsets") - super().__init__(values, extra_fields=extra_fields) - @classmethod def from_mapping(cls, value: Mapping[str, Any]) -> "_ToolsConfig": """Validate a tools mapping and preserve extension fields.""" data = _mapping(value, "tools") - blocked = data.get("blocked", []) - if isinstance(blocked, (str, bytes)) or not isinstance(blocked, Sequence): - raise FabricConfigError("tools blocked must be an ordered sequence of strings") return cls( - blocked=blocked, - toolsets=data.get("toolsets"), + enabled=data.get("enabled"), + blocked=data.get("blocked", []), extra_fields={key: item for key, item in data.items() if key not in cls._fields}, ) @@ -635,11 +672,10 @@ class _FabricConfigSnapshot(_ConfigMapping): schema_version: Agent schema identifier. metadata: Required ``MetadataConfig`` agent identity. harness: Required ``HarnessConfig`` adapter selection. - runtime: Runtime input/output configuration. + runtime: Invocation runtime configuration. environment: Optional execution environment configuration. models: Named, JSON-compatible model configurations. - system_prompt: Optional agent system instructions. - max_turns: Optional shared harness turn limit. + instructions: Optional portable agent instructions. mcp: Optional MCP configuration. skills: Optional skill configuration. telemetry: Optional telemetry configuration. @@ -656,8 +692,7 @@ class _FabricConfigSnapshot(_ConfigMapping): "runtime", "environment", "models", - "system_prompt", - "max_turns", + "instructions", "mcp", "skills", "telemetry", @@ -676,8 +711,7 @@ def __init__( schema_version: str = "fabric.agent/v1alpha1", environment: _EnvironmentConfig | Mapping[str, Any] | None = None, models: Mapping[str, Any] | None = None, - system_prompt: str | None = None, - max_turns: int | None = None, + instructions: _InstructionsConfig | Mapping[str, Any] | None = None, mcp: Mapping[str, Any] | None = None, skills: Mapping[str, Any] | None = None, telemetry: Mapping[str, Any] | None = None, @@ -693,6 +727,11 @@ def __init__( "runtime", ) environment_value = None if environment is None else _coerce(_EnvironmentConfig, environment, "environment") + instructions_value = ( + None + if instructions is None + else _coerce(_InstructionsConfig, instructions, "instructions") + ) mcp_value = None if mcp is None else _coerce(_McpConfig, mcp, "mcp") skills_value = None if skills is None else _coerce(_SkillConfig, skills, "skills") telemetry_value = None if telemetry is None else _coerce(_TelemetryConfig, telemetry, "telemetry") @@ -705,20 +744,9 @@ def __init__( "runtime": runtime_value, "models": _mapping({} if models is None else models, "models"), } - if system_prompt is not None: - if not isinstance(system_prompt, str): - raise FabricConfigError("system_prompt must be a string") - values["system_prompt"] = system_prompt - if max_turns is not None: - if ( - isinstance(max_turns, bool) - or not isinstance(max_turns, int) - or max_turns <= 0 - ): - raise FabricConfigError("max_turns must be greater than zero") - values["max_turns"] = max_turns for key, item in ( ("environment", environment_value), + ("instructions", instructions_value), ("mcp", mcp_value), ("skills", skills_value), ("telemetry", telemetry_value), @@ -750,8 +778,7 @@ def from_mapping(cls, value: Mapping[str, Any]) -> "_FabricConfigSnapshot": runtime=data.get("runtime"), environment=data.get("environment"), models=data.get("models"), - system_prompt=data.get("system_prompt"), - max_turns=data.get("max_turns"), + instructions=data.get("instructions"), mcp=data.get("mcp"), skills=data.get("skills"), telemetry=data.get("telemetry"), @@ -837,7 +864,7 @@ def add_skill_path(self, path: str | Path) -> "_FabricConfigSnapshot": return self def block_tools(self, *tools: str) -> _FabricConfigSnapshot: - """Block adapter-native tool names or toolsets and return this config.""" + """Block adapter-native tool names and return this config.""" self.tools.block(*tools) return self diff --git a/schemas/adapter-descriptor.schema.json b/schemas/adapter-descriptor.schema.json index 253a1cd81..ba1a2e44d 100644 --- a/schemas/adapter-descriptor.schema.json +++ b/schemas/adapter-descriptor.schema.json @@ -19,28 +19,23 @@ "type": "string" }, { - "const": "system_prompt", + "const": "instructions.system", "description": "Portable system instructions.", "type": "string" }, { - "const": "max_turns", + "const": "runtime.max_turns", "description": "Per-invocation harness turn limit.", "type": "string" }, { - "const": "tools.blocked", - "description": "Adapter-native tool names to block.", + "const": "tools.enabled", + "description": "Adapter-native tool names to expose.", "type": "string" }, { - "const": "tools.toolsets.enabled", - "description": "Harness-defined toolsets to expose.", - "type": "string" - }, - { - "const": "tools.toolsets.blocked", - "description": "Harness-defined toolsets to block.", + "const": "tools.blocked", + "description": "Adapter-native tool names to block.", "type": "string" }, { diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index f5ba8ec0c..198efcd6a 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -120,6 +120,53 @@ ], "type": "object" }, + "InstructionConfig": { + "additionalProperties": true, + "description": "One portable instruction value.", + "properties": { + "content": { + "description": "Instruction text.", + "type": "string" + }, + "mode": { + "$ref": "#/$defs/InstructionMode", + "default": "replace", + "description": "How the instruction is applied." + } + }, + "required": [ + "content" + ], + "type": "object" + }, + "InstructionMode": { + "description": "How an instruction value is applied to the selected harness.", + "oneOf": [ + { + "const": "replace", + "description": "Replace the harness default instruction value.", + "type": "string" + } + ] + }, + "InstructionsConfig": { + "additionalProperties": true, + "description": "Harness-neutral agent instruction configuration.", + "properties": { + "system": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionConfig" + }, + { + "type": "null" + } + ], + "description": "System instructions for the selected harness." + } + }, + "type": "object" + }, "McpConfig": { "additionalProperties": true, "description": "MCP capability configuration.", @@ -891,7 +938,7 @@ }, "RuntimeConfig": { "additionalProperties": true, - "description": "Runtime input/output contract.", + "description": "Invocation runtime contract.", "properties": { "artifacts": { "description": "Artifact directory.", @@ -905,6 +952,15 @@ "description": "Input schema label.", "type": "string" }, + "max_turns": { + "description": "Maximum number of harness turns within one invocation.", + "format": "uint32", + "minimum": 1, + "type": [ + "integer", + "null" + ] + }, "output_schema": { "default": "text", "description": "Output schema label.", @@ -971,33 +1027,8 @@ }, "type": "array" }, - "toolsets": { - "anyOf": [ - { - "$ref": "#/$defs/ToolsetConfig" - }, - { - "type": "null" - } - ], - "description": "Harness-defined toolset selection and blocking policy." - } - }, - "type": "object" - }, - "ToolsetConfig": { - "additionalProperties": true, - "description": "Harness-defined toolset capability configuration.", - "properties": { - "blocked": { - "description": "Toolsets to exclude from the enabled or default harness toolset set.", - "items": { - "type": "string" - }, - "type": "array" - }, "enabled": { - "description": "Toolsets to expose. `None` preserves the harness default; an empty list exposes none.", + "description": "Adapter-native tool names to expose. `None` preserves the harness default.", "items": { "type": "string" }, @@ -1029,14 +1060,16 @@ "$ref": "#/$defs/HarnessConfig", "description": "Harness selection and harness-specific settings." }, - "max_turns": { - "description": "Maximum number of harness turns within one invocation.", - "format": "uint32", - "minimum": 1, - "type": [ - "integer", - "null" - ] + "instructions": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionsConfig" + }, + { + "type": "null" + } + ], + "description": "Portable agent instructions for the selected harness." }, "mcp": { "anyOf": [ @@ -1073,7 +1106,7 @@ }, "runtime": { "$ref": "#/$defs/RuntimeConfig", - "description": "Runtime input/output contract." + "description": "Invocation runtime contract." }, "schema_version": { "description": "Config schema version.", @@ -1090,13 +1123,6 @@ ], "description": "Skill capability configuration." }, - "system_prompt": { - "description": "Portable system instructions for the selected harness.", - "type": [ - "string", - "null" - ] - }, "telemetry": { "anyOf": [ { diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 680b3ae93..cb118bbd9 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -19,28 +19,23 @@ "type": "string" }, { - "const": "system_prompt", + "const": "instructions.system", "description": "Portable system instructions.", "type": "string" }, { - "const": "max_turns", + "const": "runtime.max_turns", "description": "Per-invocation harness turn limit.", "type": "string" }, { - "const": "tools.blocked", - "description": "Adapter-native tool names to block.", + "const": "tools.enabled", + "description": "Adapter-native tool names to expose.", "type": "string" }, { - "const": "tools.toolsets.enabled", - "description": "Harness-defined toolsets to expose.", - "type": "string" - }, - { - "const": "tools.toolsets.blocked", - "description": "Harness-defined toolsets to block.", + "const": "tools.blocked", + "description": "Adapter-native tool names to block.", "type": "string" }, { @@ -334,7 +329,7 @@ "type": "object" }, "CapabilityRoute": { - "description": "One capability routing decision.", + "description": "One capability execution assignment.\n\nRoutes apply to executable tool, skill, and MCP capabilities. Adapter-translated\nscalar configuration is validated separately against [`AdapterConfigSupport`].", "properties": { "kind": { "$ref": "#/$defs/CapabilityKind", @@ -350,7 +345,7 @@ }, "target": { "$ref": "#/$defs/CapabilityTarget", - "description": "Routing target." + "description": "Component responsible for executing the capability." } }, "required": [ @@ -362,21 +357,21 @@ "type": "object" }, "CapabilityTarget": { - "description": "Capability routing target.", + "description": "Component responsible for executing a configured capability.\n\nThis target describes execution ownership, not network routing.", "oneOf": [ { "const": "harness_native", - "description": "Adapter maps the capability into harness-native config.", + "description": "The selected adapter maps and executes the capability through its harness.", "type": "string" }, { "const": "fabric_managed", - "description": "NeMo Fabric exposes or manages the capability around the harness.", + "description": "NeMo Fabric executes the capability outside the harness-native surface.", "type": "string" }, { "const": "unsupported", - "description": "Capability is configured but no executable surface exists.", + "description": "Neither the adapter nor NeMo Fabric can execute the configured capability.", "type": "string" } ] @@ -574,14 +569,16 @@ "$ref": "#/$defs/HarnessConfig", "description": "Harness selection and harness-specific settings." }, - "max_turns": { - "description": "Maximum number of harness turns within one invocation.", - "format": "uint32", - "minimum": 1, - "type": [ - "integer", - "null" - ] + "instructions": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionsConfig" + }, + { + "type": "null" + } + ], + "description": "Portable agent instructions for the selected harness." }, "mcp": { "anyOf": [ @@ -618,7 +615,7 @@ }, "runtime": { "$ref": "#/$defs/RuntimeConfig", - "description": "Runtime input/output contract." + "description": "Invocation runtime contract." }, "schema_version": { "description": "Config schema version.", @@ -635,13 +632,6 @@ ], "description": "Skill capability configuration." }, - "system_prompt": { - "description": "Portable system instructions for the selected harness.", - "type": [ - "string", - "null" - ] - }, "telemetry": { "anyOf": [ { @@ -703,6 +693,53 @@ ], "type": "object" }, + "InstructionConfig": { + "additionalProperties": true, + "description": "One portable instruction value.", + "properties": { + "content": { + "description": "Instruction text.", + "type": "string" + }, + "mode": { + "$ref": "#/$defs/InstructionMode", + "default": "replace", + "description": "How the instruction is applied." + } + }, + "required": [ + "content" + ], + "type": "object" + }, + "InstructionMode": { + "description": "How an instruction value is applied to the selected harness.", + "oneOf": [ + { + "const": "replace", + "description": "Replace the harness default instruction value.", + "type": "string" + } + ] + }, + "InstructionsConfig": { + "additionalProperties": true, + "description": "Harness-neutral agent instruction configuration.", + "properties": { + "system": { + "anyOf": [ + { + "$ref": "#/$defs/InstructionConfig" + }, + { + "type": "null" + } + ], + "description": "System instructions for the selected harness." + } + }, + "type": "object" + }, "McpConfig": { "additionalProperties": true, "description": "MCP capability configuration.", @@ -1556,7 +1593,7 @@ }, "RuntimeConfig": { "additionalProperties": true, - "description": "Runtime input/output contract.", + "description": "Invocation runtime contract.", "properties": { "artifacts": { "description": "Artifact directory.", @@ -1570,6 +1607,15 @@ "description": "Input schema label.", "type": "string" }, + "max_turns": { + "description": "Maximum number of harness turns within one invocation.", + "format": "uint32", + "minimum": 1, + "type": [ + "integer", + "null" + ] + }, "output_schema": { "default": "text", "description": "Output schema label.", @@ -1699,57 +1745,31 @@ }, "type": "array" }, - "toolsets": { - "anyOf": [ - { - "$ref": "#/$defs/ToolsetConfig" - }, - { - "type": "null" - } - ], - "description": "Harness-defined toolset selection and blocking policy." - } - }, - "type": "object" - }, - "ToolsPlan": { - "description": "Normalized tool policy for a run.", - "properties": { - "blocked": { - "description": "Adapter-native tool names to block.", + "enabled": { + "description": "Adapter-native tool names to expose. `None` preserves the harness default.", "items": { "type": "string" }, - "type": "array" - }, - "toolsets": { - "anyOf": [ - { - "$ref": "#/$defs/ToolsetConfig" - }, - { - "type": "null" - } - ], - "description": "Harness-defined toolset selection and blocking policy." + "type": [ + "array", + "null" + ] } }, "type": "object" }, - "ToolsetConfig": { - "additionalProperties": true, - "description": "Harness-defined toolset capability configuration.", + "ToolsPlan": { + "description": "Normalized tool policy for a run.", "properties": { "blocked": { - "description": "Toolsets to exclude from the enabled or default harness toolset set.", + "description": "Adapter-native tool names to block.", "items": { "type": "string" }, "type": "array" }, "enabled": { - "description": "Toolsets to expose. `None` preserves the harness default; an empty list exposes none.", + "description": "Adapter-native tool names to expose. `None` preserves the harness default.", "items": { "type": "string" }, diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index 35e66d6bb..34f766368 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -134,15 +134,16 @@ def test_selected_model_config( assert common_utils.selected_model_config(payload) == expected -def test_normalized_agent_and_toolset_accessors(): +def test_normalized_instruction_runtime_and_tool_accessors(): payload = { "config": { - "system_prompt": "Be concise.", - "max_turns": 7, - "runtime": {"timeout_seconds": 12.5}, + "instructions": { + "system": {"content": "Be concise.", "mode": "replace"} + }, + "runtime": {"timeout_seconds": 12.5, "max_turns": 7}, "tools": { + "enabled": [], "blocked": ["Bash"], - "toolsets": {"enabled": [], "blocked": ["browser"]}, }, }, "runtime_context": { @@ -150,13 +151,12 @@ def test_normalized_agent_and_toolset_accessors(): }, } - assert common_utils.system_prompt(payload) == "Be concise." + assert common_utils.system_instruction(payload) == "Be concise." assert common_utils.max_turns(payload) == 7 assert common_utils.timeout_seconds(payload, default=30) == 12.5 assert common_utils.environment_env(payload) == {"VISIBLE": "yes"} assert common_utils.blocked_tools(payload) == ["Bash"] - assert common_utils.enabled_toolsets(payload) == [] - assert common_utils.blocked_toolsets(payload) == ["browser"] + assert common_utils.enabled_tools(payload) == [] def test_payload_accessors_use_canonical_plan_fields(tmp_path): diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index a8b909d3d..a687abba3 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -90,8 +90,9 @@ def test_claude_descriptor_is_narrow_and_versioned(): "accepts": [ "models", "models.base_url", - "system_prompt", - "max_turns", + "instructions.system", + "runtime.max_turns", + "tools.enabled", "tools.blocked", "mcp", "skills", @@ -128,9 +129,10 @@ def claude_payload_fixture(tmp_path) -> dict[str, Any]: "setting_sources": [], }, }, - "system_prompt": "Review carefully.", - "max_turns": 4, - "runtime": {"timeout_seconds": 30}, + "instructions": { + "system": {"content": "Review carefully.", "mode": "replace"} + }, + "runtime": {"timeout_seconds": 30, "max_turns": 4}, "models": { "default": { "provider": "anthropic", @@ -181,6 +183,7 @@ def test_build_options_maps_normalized_capabilities_and_claude_settings(claude_p assert options.tools is None assert options.allowed_tools == ["Read"] assert options.disallowed_tools == ["Bash"] + assert options.hooks is not None assert options.permission_mode == "dontAsk" assert options.max_turns == 4 assert options.max_budget_usd == 1.5 @@ -204,6 +207,27 @@ def test_build_options_maps_normalized_capabilities_and_claude_settings(claude_p assert "ANTHROPIC_BASE_URL" not in options.env +async def test_tool_policy_hooks_gate_built_in_and_mcp_tools(claude_payload): + claude_payload["config"]["tools"] = { + "enabled": ["Read"], + "blocked": ["Bash"], + } + + options = adapter.build_options(claude_payload) + + assert options.tools == ["Read"] + assert options.hooks is not None + hook = options.hooks["PreToolUse"][0].hooks[0] + assert await hook({"tool_name": "Read"}, None, {"signal": None}) == {} + for tool_name in ("Bash", "mcp__repo__search"): + output = await hook( + {"tool_name": tool_name}, + None, + {"signal": None}, + ) + assert output["hookSpecificOutput"]["permissionDecision"] == "deny" + + @pytest.fixture(name="relay_payload") def relay_payload_fixture(claude_payload, tmp_path) -> dict[str, Any]: relay_intent_path = tmp_path / "relay-config.json" diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index 3e9874437..b17a191c8 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -75,7 +75,9 @@ def codex_payload_fixture(tmp_path): "model": "openai/gpt-5.4", } }, - "system_prompt": "Review carefully.", + "instructions": { + "system": {"content": "Review carefully.", "mode": "replace"} + }, "runtime": {}, }, "runtime_context": { @@ -980,7 +982,7 @@ def test_descriptor_has_no_codex_binary_requirement(): assert descriptor["config"]["accepts"] == [ "models", "models.base_url", - "system_prompt", + "instructions.system", "mcp", "skills", ] diff --git a/tests/adapters/test_deepagents.py b/tests/adapters/test_deepagents.py index 73a52f483..540e2dba4 100644 --- a/tests/adapters/test_deepagents.py +++ b/tests/adapters/test_deepagents.py @@ -152,7 +152,9 @@ def make(tmp_path: Path, *, runtime_id: str = "run-1") -> dict[str, Any]: "base_dir": str(tmp_path), "config": { "harness": {"settings": {}}, - "system_prompt": "be concise", + "instructions": { + "system": {"content": "be concise", "mode": "replace"} + }, "models": { "default": { "provider": "nvidia", @@ -620,11 +622,11 @@ async def test_mcp_servers_become_adapter_tools( @pytest.mark.usefixtures("use_real_langgraph") -async def test_blocked_tools_middleware_blocks_configured_tools(): +async def test_tool_policy_middleware_enforces_enabled_and_blocked_tools(): pytest.importorskip("langchain.agents.middleware") from langchain_core.messages import ToolMessage - middleware = adapter.blocked_tools_middleware({"write_file"}) + middleware = adapter.tool_policy_middleware({"read_file"}, {"write_file"}) async def handler(_request: types.SimpleNamespace) -> str: return "executed" @@ -641,6 +643,10 @@ def request(name: str) -> types.SimpleNamespace: allowed = await middleware.awrap_tool_call(request("read_file"), handler) assert allowed == "executed" + unselected = await middleware.awrap_tool_call(request("search"), handler) + assert isinstance(unselected, ToolMessage) + assert unselected.status == "error" + @pytest.mark.usefixtures("use_real_langgraph") async def test_real_langgraph_async_checkpointer(tmp_path, make_payload, monkeypatch): @@ -931,17 +937,17 @@ async def test_blocked_tools_reject_unenforceable_subagents( [ ( {"name": "researcher"}, - "harness.settings.deepagents.subagents must be a list when tools.blocked is configured.", + "harness.settings.deepagents.subagents must be a list when a tools policy is configured.", ), ( [{"name": "researcher"}, "invalid"], - "Deep Agents subagents must be mappings when tools.blocked is configured.", + "Deep Agents subagents must be mappings when a tools policy is configured.", ), ], ) def test_gated_subagents_reject_invalid_configuration(subagents, message): with pytest.raises(adapter.AdapterConfigError) as error: - adapter._gated_subagents(subagents, {"write_file"}) + adapter._gated_subagents(subagents, None, {"write_file"}) assert str(error.value) == message diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index da91e4693..e69d25dd3 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -185,12 +185,10 @@ def test_build_hermes_config_maps_fabric_config_to_hermes_config(): "plugins_enabled": ["custom/plugin"], } }, - "max_turns": 4, + "runtime": {"max_turns": 4}, "tools": { - "toolsets": { - "enabled": ["git"], - "blocked": ["browser"], - } + "enabled": ["git"], + "blocked": ["browser"], }, "models": { "review": { @@ -270,7 +268,7 @@ def test_build_hermes_config_omits_max_turns_when_fabric_limit_null(): payload = { "config": { "harness": {"settings": {}}, - "max_turns": None, + "runtime": {"max_turns": None}, "models": {"default": {"provider": "nvidia", "model": "nvidia/test-model"}}, } } @@ -337,7 +335,7 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( "base_dir": str(tmp_path), "config": { "harness": {"settings": {}}, - "tools": {"toolsets": {"enabled": ["git", "shell"]}}, + "tools": {"enabled": ["git", "shell"]}, "models": { "review": { "provider": "nvidia", @@ -561,9 +559,11 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( "base_dir": str(tmp_path), "config": { "harness": {"settings": {}}, - "system_prompt": "system", - "max_turns": None, - "tools": {"toolsets": {"enabled": []}}, + "instructions": { + "system": {"content": "system", "mode": "replace"} + }, + "runtime": {"max_turns": None}, + "tools": {"enabled": []}, "models": { "default": { "provider": "test-provider", diff --git a/tests/e2e/test_hermes_config_mapping.py b/tests/e2e/test_hermes_config_mapping.py index ad175d454..7f6c0d70b 100644 --- a/tests/e2e/test_hermes_config_mapping.py +++ b/tests/e2e/test_hermes_config_mapping.py @@ -64,11 +64,7 @@ def payload(tmpdir: str) -> dict: "base_url": "https://integrate.api.nvidia.com/v1", } }, - "tools": { - "toolsets": { - "enabled": [], - } - }, + "tools": {"enabled": []}, }, "settings": { "terminal_timeout": 30, diff --git a/tests/integrations/test_harbor_runner.py b/tests/integrations/test_harbor_runner.py index be6d51ef3..62ae53792 100644 --- a/tests/integrations/test_harbor_runner.py +++ b/tests/integrations/test_harbor_runner.py @@ -250,7 +250,7 @@ def test_claude_calculator_run_uses_current_adapter_contract(): assert config.harness.adapter_id == "nvidia.fabric.claude" assert settings["permission_mode"] == "bypassPermissions" - assert config.max_turns == 20 + assert config.runtime.max_turns == 20 assert config.runtime.timeout_seconds == 600 assert config.models["default"].provider == "anthropic" dockerfile = CALCULATOR_DOCKERFILE.read_text(encoding="utf-8") @@ -431,11 +431,11 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path blocked_tools=["browser"], telemetry="relay", ) - toolsets = build_harbor_config( + selected_tools = build_harbor_config( adapter_id="nvidia.fabric.hermes", workspace="/testbed", - enabled_toolsets=[], - blocked_toolsets=["browser"], + enabled_tools=[], + blocked_tools=["browser"], telemetry="relay", ) claude = build_harbor_config( @@ -461,12 +461,10 @@ def test_swebench_matrix_translates_harbor_inputs_to_typed_config(tmp_path: Path ] assert tools.tools is not None assert tools.tools.blocked == ["browser"] - assert tools.tools.toolsets is None - assert toolsets.tools is not None - assert toolsets.tools.blocked == [] - assert toolsets.tools.toolsets is not None - assert toolsets.tools.toolsets.enabled == [] - assert toolsets.tools.toolsets.blocked == ["browser"] + assert tools.tools.enabled is None + assert selected_tools.tools is not None + assert selected_tools.tools.enabled == [] + assert selected_tools.tools.blocked == ["browser"] assert relay.telemetry is not None assert "relay" in relay.telemetry.providers assert relay.relay is not None diff --git a/tests/python/test_native_sdk.py b/tests/python/test_native_sdk.py index 097905c71..8b4339917 100644 --- a/tests/python/test_native_sdk.py +++ b/tests/python/test_native_sdk.py @@ -67,11 +67,13 @@ def test_native_run_rejects_multiple_request_sources(hermes_shim_agent_dir: Path def test_plan_rejects_adapter_incompatible_normalized_tool_policy(): config = base_config() + config.harness.adapter_id = "nvidia.fabric.codex" + config.models["default"].temperature = None config.block_tools("Bash") with pytest.raises( FabricConfigError, - match=r"nvidia\.fabric\.hermes.*tools\.blocked", + match=r"nvidia\.fabric\.codex.*tools\.blocked", ): Fabric().plan(config, base_dir=BASE_DIR) diff --git a/tests/python/test_notebook_examples.py b/tests/python/test_notebook_examples.py index 1b66238d3..c8d484a44 100644 --- a/tests/python/test_notebook_examples.py +++ b/tests/python/test_notebook_examples.py @@ -16,7 +16,13 @@ import pytest from examples.code_review_agent import BASE_DIR, base_config -from nemo_fabric import Fabric, HarnessConfig, ModelConfig +from nemo_fabric import ( + Fabric, + HarnessConfig, + InstructionConfig, + InstructionsConfig, + ModelConfig, +) ROOT = Path(__file__).resolve().parents[2] @@ -34,6 +40,8 @@ def _variation_harness_definitions(base_dir=BASE_DIR): "base_config": base_config, "BASE_DIR": base_dir, "HarnessConfig": HarnessConfig, + "InstructionConfig": InstructionConfig, + "InstructionsConfig": InstructionsConfig, "ModelConfig": ModelConfig, "os": os, "Path": Path, diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index e075806fd..14e07a6a9 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -26,6 +26,8 @@ from nemo_fabric import FabricRuntimeError from nemo_fabric import FabricStateError from nemo_fabric import HarnessConfig +from nemo_fabric import InstructionConfig +from nemo_fabric import InstructionsConfig from nemo_fabric import McpConfig from nemo_fabric import MetadataConfig from nemo_fabric import RelayAtifConfig @@ -45,7 +47,6 @@ from nemo_fabric import RuntimeHandle from nemo_fabric import SkillConfig from nemo_fabric import TelemetryConfig -from nemo_fabric import ToolsetConfig from nemo_fabric import ToolsConfig from nemo_fabric.types import _FabricConfigSnapshot from nemo_fabric.types import _ToolsConfig @@ -146,7 +147,8 @@ def test_typed_config_authoring_helpers_emit_schema_shape(): output_dir="./artifacts/relay", ) config.block_tools("browser", "shell", "browser") - config.configure_toolsets(enabled=["terminal"], blocked=["browser"]) + assert config.tools is not None + config.tools.enabled = ["terminal"] assert isinstance(config.mcp, McpConfig) assert isinstance(config.skills, SkillConfig) @@ -154,11 +156,8 @@ def test_typed_config_authoring_helpers_emit_schema_shape(): assert isinstance(config.tools, ToolsConfig) assert config.to_mapping()["tools"] == { + "enabled": ["terminal"], "blocked": ["browser", "shell"], - "toolsets": { - "enabled": ["terminal"], - "blocked": ["browser"], - }, } assert config.to_mapping()["skills"] == {"paths": ["./skills/review"]} assert config.to_mapping()["mcp"] == { @@ -213,9 +212,10 @@ def test_typed_config_serializes_normalized_execution_fields(): config = FabricConfig( metadata=MetadataConfig(name="demo"), harness=HarnessConfig(adapter_id="test.fabric.shim"), - system_prompt="Be concise.", - max_turns=7, - runtime=RuntimeConfig(timeout_seconds=12.5), + instructions=InstructionsConfig( + system=InstructionConfig(content="Be concise.", mode="replace") + ), + runtime=RuntimeConfig(timeout_seconds=12.5, max_turns=7), environment=EnvironmentConfig(env={"VISIBLE": "yes"}), models={ "default": { @@ -224,36 +224,30 @@ def test_typed_config_serializes_normalized_execution_fields(): "base_url": "https://models.example/v1", } }, - tools=ToolsConfig( - toolsets=ToolsetConfig(enabled=[], blocked=["browser"]) - ), + tools=ToolsConfig(enabled=[], blocked=["browser"]), ) mapping = config.to_mapping() - assert mapping["system_prompt"] == "Be concise." - assert mapping["max_turns"] == 7 + assert mapping["instructions"]["system"] == { + "content": "Be concise.", + "mode": "replace", + } + assert mapping["runtime"]["max_turns"] == 7 assert mapping["runtime"]["timeout_seconds"] == 12.5 assert mapping["environment"]["env"] == {"VISIBLE": "yes"} assert mapping["models"]["default"]["base_url"] == ( "https://models.example/v1" ) - assert mapping["tools"]["toolsets"] == { - "enabled": [], - "blocked": ["browser"], - } + assert mapping["tools"] == {"enabled": [], "blocked": ["browser"]} with pytest.raises(ValidationError, match="greater than 0"): RuntimeConfig(timeout_seconds=0) with pytest.raises(ValidationError, match="finite number"): RuntimeConfig(timeout_seconds=float("inf")) with pytest.raises(ValidationError, match="greater than 0"): - FabricConfig( - metadata=MetadataConfig(name="demo"), - harness=HarnessConfig(adapter_id="test.fabric.shim"), - max_turns=0, - ) + RuntimeConfig(max_turns=0) with pytest.raises(ValidationError, match="both enabled and blocked"): - ToolsetConfig(enabled=["browser"], blocked=["browser"]) + ToolsConfig(enabled=["browser"], blocked=["browser"]) def test_run_plan_config_block_tools_emits_canonical_shape(): @@ -264,27 +258,26 @@ def test_run_plan_config_block_tools_emits_canonical_shape(): assert config.to_mapping()["tools"] == {"blocked": ["browser", "shell"]} -def test_run_plan_config_preserves_normalized_toolsets_and_execution_fields(): +def test_run_plan_config_preserves_normalized_tools_and_execution_fields(): raw = _plan()["config"] raw.update( { - "system_prompt": "Be concise.", - "max_turns": 5, - "runtime": {"timeout_seconds": 9}, - "environment": {"provider": "local", "env": {"VISIBLE": "yes"}}, - "tools": { - "toolsets": {"enabled": [], "blocked": ["browser"]}, + "instructions": { + "system": {"content": "Be concise.", "mode": "replace"} }, + "runtime": {"timeout_seconds": 9, "max_turns": 5}, + "environment": {"provider": "local", "env": {"VISIBLE": "yes"}}, + "tools": {"enabled": [], "blocked": ["browser"]}, } ) config = _FabricConfigSnapshot.from_mapping(raw) - assert config.to_mapping()["system_prompt"] == "Be concise." - assert config.to_mapping()["max_turns"] == 5 + assert config.to_mapping()["instructions"]["system"]["content"] == "Be concise." + assert config.to_mapping()["runtime"]["max_turns"] == 5 assert config.to_mapping()["runtime"]["timeout_seconds"] == 9 assert config.to_mapping()["environment"]["env"] == {"VISIBLE": "yes"} - assert config.to_mapping()["tools"]["toolsets"] == { + assert config.to_mapping()["tools"] == { "enabled": [], "blocked": ["browser"], } diff --git a/tests/python/test_typed_config.py b/tests/python/test_typed_config.py index 87ca04e83..40a7167f2 100644 --- a/tests/python/test_typed_config.py +++ b/tests/python/test_typed_config.py @@ -19,8 +19,10 @@ from pathlib import Path from shutil import copytree +import pytest from nemo_fabric import Fabric from nemo_fabric import FabricConfig +from nemo_fabric import FabricConfigError from nemo_fabric import RunRequest from nemo_fabric import RunResult @@ -130,7 +132,38 @@ async def runs_with_typed_config_and_adapter_directory(client: Fabric) -> None: assert result["output"]["received"] == "hello typed" +async def diagnoses_adapter_incompatibility_without_weakening_plan(client: Fabric) -> None: + """Doctor reports unsupported config that strict planning rejects.""" + + config = FabricConfig.from_mapping( + { + "metadata": {"name": "incompatible-agent"}, + "harness": {"adapter_id": "nvidia.fabric.codex"}, + "runtime": {"max_turns": 3}, + "tools": {"enabled": []}, + } + ) + + with pytest.raises(FabricConfigError, match="runtime.max_turns"): + client.plan(config, base_dir=ROOT) + + report = await client.doctor(config, base_dir=ROOT) + + assert report.status == "fail" + assert any( + check.name == "config.unsupported" + and check.metadata.get("field") == "runtime.max_turns" + for check in report.checks + ) + assert any( + check.name == "capability.unsupported" + and "tools.enabled" in check.message + for check in report.checks + ) + + async def test_typed_config(): client = Fabric() await resolves_and_diagnoses_typed_config(client) await runs_with_typed_config_and_adapter_directory(client) + await diagnoses_adapter_incompatibility_without_weakening_plan(client) From bc21added7fcf5d59e469c35830e5750d3712763 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 10:31:28 -0700 Subject: [PATCH 17/21] docs: document normalized adapter configuration Signed-off-by: Ajay Thorve --- README.md | 3 +- adapters/README.md | 34 +-- adapters/claude/README.md | 31 ++- adapters/codex/README.md | 7 +- adapters/deepagents/README.md | 18 +- adapters/hermes/README.md | 10 +- docs/getting-started/quickstart.mdx | 3 +- docs/integrations/harness/claude.mdx | 6 +- docs/integrations/harness/codex.mdx | 4 +- docs/integrations/harness/hermes.mdx | 6 +- .../api/python-library-reference/index.md | 5 +- .../nemo_fabric.models.md | 237 +++++++++++------- .../config/enum-adapterconfigfield.mdx | 27 +- .../config/enum-capabilitykind.mdx | 2 +- .../config/enum-capabilitytarget.mdx | 14 +- .../config/enum-instructionmode.mdx | 116 +++++++++ .../config/enum-mcpexposure.mdx | 2 +- .../config/enum-relayatifstorageconfig.mdx | 2 +- .../config/enum-relayatofmode.mdx | 2 +- .../config/enum-relayatofsinkconfig.mdx | 2 +- .../enum-relayatofstreamfieldnamepolicy.mdx | 2 +- .../config/enum-relayatofstreamtransport.mdx | 2 +- .../config/enum-relayotlptransport.mdx | 2 +- .../config/enum-relayunsupportedbehavior.mdx | 2 +- .../config/enum-resolutionstrategy.mdx | 2 +- .../config/enum-telemetryprovider.mdx | 2 +- .../config/fn-load-adapter-descriptor.mdx | 2 +- .../fn-resolve-run-plan-from-config.mdx | 2 +- .../nemo-fabric-core/config/index.mdx | 12 +- .../config/struct-capabilityroute.mdx | 8 +- .../config/struct-fabricconfig.mdx | 12 +- .../config/struct-instructionconfig.mdx | 102 ++++++++ ...nfig.mdx => struct-instructionsconfig.mdx} | 60 +++-- .../config/struct-mcpconfig.mdx | 2 +- .../config/struct-mcpserverconfig.mdx | 2 +- .../config/struct-mcpserverplan.mdx | 2 +- .../config/struct-metadataconfig.mdx | 2 +- .../config/struct-modelconfig.mdx | 2 +- .../config/struct-relayatifconfig.mdx | 2 +- .../config/struct-relayatofconfig.mdx | 2 +- .../config/struct-relaycomponentconfig.mdx | 2 +- .../config/struct-relayconfig.mdx | 2 +- .../config/struct-relayconfigpolicy.mdx | 2 +- .../struct-relayobservabilityconfig.mdx | 2 +- .../config/struct-relayotlpconfig.mdx | 2 +- .../config/struct-resolvecontext.mdx | 2 +- .../struct-resolvedadapterdescriptor.mdx | 2 +- .../config/struct-runplan.mdx | 2 +- .../config/struct-runtimecapabilities.mdx | 2 +- .../config/struct-runtimeconfig.mdx | 12 +- .../config/struct-skillconfig.mdx | 2 +- .../config/struct-telemetryconfig.mdx | 2 +- .../config/struct-telemetryplan.mdx | 2 +- .../config/struct-telemetryproviderconfig.mdx | 2 +- .../config/struct-toolsconfig.mdx | 12 +- .../config/struct-toolsplan.mdx | 12 +- .../doctor/enum-doctorstatus.mdx | 2 +- .../doctor/fn-doctor-plan.mdx | 2 +- .../nemo-fabric-core/doctor/index.mdx | 2 +- .../doctor/struct-doctorcheck.mdx | 2 +- .../doctor/struct-doctorreport.mdx | 2 +- .../error/enum-fabricerror.mdx | 2 +- .../nemo-fabric-core/error/index.mdx | 2 +- .../nemo-fabric-core/error/type-result.mdx | 2 +- .../nemo-fabric-core/fn-version.mdx | 2 +- .../nemo-fabric-core/index.mdx | 4 +- .../nemo-fabric-core/runtime/index.mdx | 2 +- .../nemo-fabric-core/schema/index.mdx | 2 +- docs/sdk/python.mdx | 99 ++++---- examples/harbor/README.md | 7 +- .../consumer/nemo-fabric-integrate/SKILL.md | 52 ++-- .../references/config-mapping.md | 29 ++- 72 files changed, 657 insertions(+), 373 deletions(-) create mode 100644 docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-instructionmode.mdx create mode 100644 docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionconfig.mdx rename docs/reference/api/rust-library-reference/nemo-fabric-core/config/{struct-toolsetconfig.mdx => struct-instructionsconfig.mdx} (67%) diff --git a/README.md b/README.md index 300e3616c..b57b3bdaf 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,13 @@ from nemo_fabric import ( HarnessConfig, MetadataConfig, ModelConfig, + RuntimeConfig, ) config = FabricConfig( metadata=MetadataConfig(name="quickstart-agent"), harness=HarnessConfig(adapter_id="nvidia.fabric.hermes"), - max_turns=1, + runtime=RuntimeConfig(max_turns=1), models={ "default": ModelConfig( provider="nvidia", diff --git a/adapters/README.md b/adapters/README.md index 81dad5339..c74d3ee57 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -45,19 +45,24 @@ provider should expose more precise provenance. | Agent Harness | Models | Tool Policy | MCP | Skills | Subagents | | --- | --- | --- | --- | --- | --- | -| [Claude](claude/README.md) | Native Anthropic or a configured Anthropic Messages-compatible provider | `tools.blocked` maps to Claude `disallowed_tools`; toolsets unsupported | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | -| [Codex](codex/README.md) | Native OpenAI or a configured Responses-compatible provider | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | -| [LangChain Deep Agents](deepagents/README.md) | LangChain model providers | `tools.blocked` middleware covers built-ins, MCP, and local subagents; toolsets unsupported | Normalized through `langchain-mcp-adapters` | Normalized | Constrained declarative local delegation | -| [Hermes Agent](hermes/README.md) | Configurable provider, model, and base URL | `tools.toolsets` maps to Hermes toolset controls; per-tool blocking unsupported | Normalized | Normalized | Not exposed | +| [Claude](claude/README.md) | Native Anthropic or a configured Anthropic Messages-compatible provider | `tools.enabled` selects built-ins; a pre-tool hook enforces enabled and blocked names across built-in, MCP, and plugin tools | Normalized: stdio, HTTP, streamable HTTP, and SSE | Normalized `skills.paths` | Not exposed | +| [Codex](codex/README.md) | Native OpenAI or a configured Responses-compatible provider | `tools.enabled` and `tools.blocked` unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized `SKILL.md` directories | Not exposed | +| [LangChain Deep Agents](deepagents/README.md) | LangChain model providers | Middleware enforces `tools.enabled` and `tools.blocked` across built-ins, MCP, and local subagents | Normalized through `langchain-mcp-adapters` | Normalized | Constrained declarative local delegation | +| [Hermes Agent](hermes/README.md) | Configurable provider, model, and base URL | `tools.enabled` and `tools.blocked` map to Hermes native toolset selectors | Normalized | Normalized | Not exposed | "Normalized" means that the adapter accepts the corresponding `FabricConfig` field. "Not exposed" does not mean that the underlying harness lacks the -feature; it means that NeMo Fabric does not provide a portable configuration surface -for it. Individual tools and harness-defined toolsets are separate normalized -policies. Planning fails when the selected adapter cannot enforce a configured -policy; NeMo Fabric does not reinterpret one as the other. Deep Agents subagents -are limited to declarative local subagents that inherit the parent agent's -capabilities. +feature; it means that NeMo Fabric does not provide a portable configuration +surface for it. Tool values are adapter-native selectors; NeMo Fabric does not +define a cross-harness tool-name catalog. Planning fails when the selected +adapter cannot enforce a configured policy. Deep Agents subagents are limited +to declarative local subagents that inherit the parent agent's capabilities. + +`RunPlan.capability_plan.routes` records execution ownership, not network +routing. `harness_native` assigns a capability to the selected adapter, +`fabric_managed` assigns it to NeMo Fabric, and `unsupported` means neither can +execute it. Scalar fields are validated separately against +`adapter_descriptor.config.accepts`. ### Complete FabricConfig Support @@ -79,15 +84,14 @@ and additive extension maps because their support does not vary by adapter: | `models..base_url` | Yes | Yes | Yes | Yes | | `models..temperature` | No | No | Yes | Yes | | `models..settings.` | No keys declared | No keys declared | No keys declared | No keys declared | -| `system_prompt` | Yes | Yes; base instructions | Yes | Yes | -| `max_turns` | Yes | No | No | Yes; iteration limit | +| `instructions.system` | Yes | Yes; base instructions | Yes | Yes | | `runtime.input_schema`, `.output_schema` | Core | Core | Core | Core | | `runtime.artifacts`, `.timeout_seconds` | Core | Core | Core | Core | +| `runtime.max_turns` | Yes | No | No | Yes; iteration limit | | `environment.provider`, `.control_location`, `.ownership` | Core | Core | Core | Core | | `environment.workspace`, `.artifacts`, `.env` | Core | Core | Core | Core | | `environment.connection`, `.metadata`, `.settings` | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | -| `tools.blocked` | Yes | No | Yes | No | -| `tools.toolsets.enabled`, `.blocked` | No | No | No | Yes | +| `tools.enabled`, `.blocked` | Yes | No | Yes | Yes; native selectors are Hermes toolset names | | `skills.paths` | Yes | Yes | Yes | Yes | | `mcp.servers..transport`, `.url` with `harness_native` exposure | Yes | Yes | Yes | Yes | | `mcp.servers..exposure = "fabric_managed"` | No; not implemented | No; not implemented | No; not implemented | No; not implemented | @@ -100,6 +104,8 @@ and additive extension maps because their support does not vary by adapter: The selected model role is `default`, or the sole configured role when no `default` exists. More than one role without `default` fails planning. +`runtime.max_turns` is optional; omitting it preserves adapter-native defaults +without creating a compatibility requirement. ## Runtime and Observability Compatibility diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 6f6882fa2..f7950528f 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -86,13 +86,15 @@ Configure portable capabilities through the normalized `FabricConfig` fields: - `models` selects the Claude model. The native `anthropic` provider retains Claude authentication and endpoint discovery. Any other provider name must configure an Anthropic Messages-compatible `base_url` and `api_key_env`. -- `system_prompt` supplies the Claude system instructions. -- `max_turns` sets the Claude turn limit. +- `instructions.system` supplies the Claude system instructions. +- `runtime.max_turns` sets the Claude turn limit. - `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` sets the Claude working directory, and `environment.env` supplies explicit harness-visible variables. -- `tools.blocked` maps to Claude `disallowed_tools` using Claude-native tool - names. +- `tools.enabled` selects Claude built-in tools. `None` preserves the Claude + default, while an empty list disables every tool. +- `tools.blocked` maps to Claude `disallowed_tools`. A pre-tool hook enforces + both lists across built-in, MCP, and plugin tools. - `mcp` configures stdio, HTTP, streamable HTTP, or SSE servers. For stdio, NeMo Fabric parses `url` as a command plus arguments. - `skills.paths` names skill directories that contain `SKILL.md`. The adapter @@ -148,6 +150,8 @@ from nemo_fabric import ( Fabric, FabricConfig, HarnessConfig, + InstructionConfig, + InstructionsConfig, McpConfig, McpServerConfig, MetadataConfig, @@ -174,11 +178,22 @@ config = FabricConfig( api_key_env="ANTHROPIC_API_KEY", ) }, - system_prompt="Review changes for correctness and regressions.", - max_turns=8, - runtime=RuntimeConfig(artifacts="./artifacts", timeout_seconds=600), + instructions=InstructionsConfig( + system=InstructionConfig( + content="Review changes for correctness and regressions.", + mode="replace", + ) + ), + runtime=RuntimeConfig( + artifacts="./artifacts", + timeout_seconds=600, + max_turns=8, + ), environment=EnvironmentConfig(provider="local", workspace="."), - tools=ToolsConfig(blocked=["WebFetch"]), + tools=ToolsConfig( + enabled=["Read", "Edit", "Bash"], + blocked=["WebFetch"], + ), mcp=McpConfig( servers={ "repo": McpServerConfig( diff --git a/adapters/codex/README.md b/adapters/codex/README.md index 7684d328c..b14b5e333 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -85,7 +85,7 @@ Use normalized `FabricConfig` fields for portable configuration: - `models` selects the Codex model. The native `openai` provider retains Codex authentication and endpoint discovery. Any other provider name must configure a Responses-compatible `base_url` and `api_key_env`. -- `system_prompt` maps to Codex base instructions. +- `instructions.system` maps to Codex base instructions. - `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` sets the working directory, and `environment.env` supplies explicit harness-visible variables. @@ -114,8 +114,9 @@ Codex-specific controls belong in `harness.settings`: runtime starts, such as Codex-only MCP timeout or required-server options Set model selection and endpoints through `models`, system instructions through -`system_prompt`, the invocation deadline through `runtime.timeout_seconds`, and -the working directory and explicit environment through `environment`. +`instructions.system`, the invocation deadline through +`runtime.timeout_seconds`, and the working directory and explicit environment +through `environment`. For `Fabric.start_runtime(...)`, the model provider, MCP configuration, skill roots, and `config_overrides` are fixed when the runtime starts and cannot vary diff --git a/adapters/deepagents/README.md b/adapters/deepagents/README.md index cfad839d0..5985a530c 100644 --- a/adapters/deepagents/README.md +++ b/adapters/deepagents/README.md @@ -46,7 +46,7 @@ NeMo Fabric maps the following into the harness: - The selected `models` role supplies `model`, `provider`, `api_key_env`, `base_url`, and `temperature`. -- Top-level `system_prompt` becomes the Deep Agents `system_prompt`. +- `instructions.system` becomes the Deep Agents `system_prompt`. - `runtime.timeout_seconds` sets the NeMo Fabric invocation deadline. - `environment.workspace` roots the Deep Agents filesystem backend (`FilesystemBackend(root_dir=..., virtual_mode=True)`). `virtual_mode` @@ -56,9 +56,9 @@ NeMo Fabric maps the following into the harness: - Configured MCP servers are loaded as Deep Agents tools via `langchain-mcp-adapters`. A misconfigured server (non-mapping, empty target, unsupported transport) is a normalized configuration failure, not a silent drop. -- `tools.blocked` is enforced by middleware across the full tool surface — Deep - Agents built-ins (including `task`), MCP tools, and **delegated subagents** - alike. Use Deep Agents/native tool names in the blocked list. +- `tools.enabled` and `tools.blocked` are enforced by middleware across the full + tool surface: Deep Agents built-ins (including `task`), MCP tools, and + **delegated subagents** alike. Use Deep Agents-native tool names. - `harness.settings.deepagents` forwards a small set of **documented, JSON-serializable** `create_deep_agent` options (currently `subagents` and `interrupt_on`). It is not a general Python-object escape hatch: the SDK config @@ -73,11 +73,11 @@ NeMo Fabric maps the following into the harness: Deep Agents can delegate to subagents through its built-in `task` tool. Subagents **inherit** the parent run's model, tools, skills, workspace, telemetry, and -permissions. When `tools.blocked` is configured, NeMo Fabric supplies an explicitly -gated `general-purpose` subagent and gates every declarative local subagent, so -delegation cannot broaden capabilities beyond the parent. Remote and precompiled -subagents are rejected in that case because their execution cannot be governed by -the local middleware. Independently configured subagent tools, skills, models, +permissions. When a normalized tools policy is configured, NeMo Fabric supplies +an explicitly gated `general-purpose` subagent and gates every declarative local +subagent, so delegation cannot broaden capabilities beyond the parent. Remote +and precompiled subagents are rejected in that case because their execution +cannot be governed by the local middleware. Independently configured subagent tools, skills, models, MCP servers, middleware, or permissions are **not** exposed through the NeMo Fabric SDK yet; a `subagents` definition here only carries JSON-shaped fields. diff --git a/adapters/hermes/README.md b/adapters/hermes/README.md index f625f0a52..1481e947d 100644 --- a/adapters/hermes/README.md +++ b/adapters/hermes/README.md @@ -33,17 +33,17 @@ The adapter receives a normalized payload from NeMo Fabric and materializes a na - selected model provider, model name, base URL, and temperature through `models`; -- top-level `system_prompt` and `max_turns`; +- `instructions.system` and `runtime.max_turns`; - workspace and explicit environment variables through `environment`; - invocation timeout through `runtime.timeout_seconds`; - NeMo Fabric skills as external skill directories for Hermes Agent; - NeMo Fabric MCP servers as Hermes Agent MCP server config; -- `tools.toolsets.enabled` and `tools.toolsets.blocked` as Hermes toolset - selection and blocking policy; +- `tools.enabled` and `tools.blocked` as Hermes-native toolset selection and + blocking policy; - optional NeMo Relay telemetry plugin configuration. -Per-tool `tools.blocked` is not accepted by Hermes because Hermes exposes -toolset-level policy at this boundary. Keep Hermes-specific controls such as +Tool selectors are Hermes toolset names because that is the native policy +surface Hermes exposes. Keep Hermes-specific controls such as terminal timeout, reasoning configuration, and plugin configuration in `harness.settings`. The adapter derives Hermes state from the NeMo Fabric artifact root and creates a child under `runtimes/`, so invocations diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 0b827f2ed..3b69350cf 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -34,6 +34,7 @@ from nemo_fabric import ( HarnessConfig, MetadataConfig, ModelConfig, + RuntimeConfig, ) config = FabricConfig( @@ -42,7 +43,7 @@ config = FabricConfig( adapter_id="nvidia.fabric.hermes", resolution="preinstalled", ), - max_turns=1, + runtime=RuntimeConfig(max_turns=1), models={ "default": ModelConfig( provider="nvidia", diff --git a/docs/integrations/harness/claude.mdx b/docs/integrations/harness/claude.mdx index 1918ed5fd..9a0ea1981 100644 --- a/docs/integrations/harness/claude.mdx +++ b/docs/integrations/harness/claude.mdx @@ -32,9 +32,9 @@ harness = HarnessConfig(adapter_id="nvidia.fabric.claude") ``` Use normalized `FabricConfig` fields to configure the model, workspace, skills, -MCP servers, blocked tools, and telemetry. Use `harness.settings` for supported -Claude-specific options such as `system_prompt`, `allowed_tools`, -`permission_mode`, `max_turns`, and `max_budget_usd`. +MCP servers, instructions, turn limit, tool policy, and telemetry. Use +`harness.settings` for supported Claude-specific options such as +`allowed_tools`, `permission_mode`, `max_budget_usd`, and `setting_sources`. ## Choose an Authentication Mode diff --git a/docs/integrations/harness/codex.mdx b/docs/integrations/harness/codex.mdx index deb521c71..dbd31204f 100644 --- a/docs/integrations/harness/codex.mdx +++ b/docs/integrations/harness/codex.mdx @@ -30,7 +30,7 @@ harness = HarnessConfig(adapter_id="nvidia.fabric.codex") Use normalized `FabricConfig` fields to configure the model, workspace, skills, MCP servers, and telemetry. The Codex adapter does not support normalized -`tools.blocked` policy. Use `harness.settings` for supported Codex-specific +`tools.enabled` or `tools.blocked` policy. Use `harness.settings` for supported Codex-specific options such as `sandbox`, `approval_mode`, `base_instructions`, `developer_instructions`, and `output_schema`. @@ -70,7 +70,7 @@ and cannot vary between `Runtime.invoke(...)` calls. Start a new runtime to change them. `Fabric.run(...)` starts the same runtime, invokes it once, and stops it, so the same settings are scoped to that single invocation. -The Codex adapter does not declare `tools.blocked` support. Codex can filter +The Codex adapter does not declare `tools.enabled` or `tools.blocked` support. Codex can filter individual MCP server tools, but the pinned runtime does not provide one deny mechanism that covers built-in, local, MCP, and hosted tools. NeMo Fabric reports normalized blocked-tool policy as unsupported instead of enforcing diff --git a/docs/integrations/harness/hermes.mdx b/docs/integrations/harness/hermes.mdx index 2f079f4b8..0a4564e21 100644 --- a/docs/integrations/harness/hermes.mdx +++ b/docs/integrations/harness/hermes.mdx @@ -32,9 +32,9 @@ harness = HarnessConfig(adapter_id="nvidia.fabric.hermes") ``` Use normalized `FabricConfig` fields to configure the model, workspace, skills, -MCP servers, blocked toolsets, and telemetry. Use `harness.settings` only for -Hermes Agent-specific options such as `hermes_home` or -`disabled_toolsets`. +MCP servers, instructions, turn limit, native tool selectors, and telemetry. +Use `harness.settings` only for Hermes Agent-specific options such as terminal +timeout or reasoning configuration. ## Understand the Runtime Lifecycle diff --git a/docs/reference/api/python-library-reference/index.md b/docs/reference/api/python-library-reference/index.md index c48f3a1c1..f7a7cf7b2 100644 --- a/docs/reference/api/python-library-reference/index.md +++ b/docs/reference/api/python-library-reference/index.md @@ -27,6 +27,8 @@ SPDX-License-Identifier: Apache-2.0 --> - [`models.FabricBaseModel`](./nemo_fabric.models.md#class-fabricbasemodel): Base class for SDK-facing Pydantic models. - [`models.FabricConfig`](./nemo_fabric.models.md#class-fabricconfig): SDK-facing typed NeMo Fabric agent configuration. - [`models.HarnessConfig`](./nemo_fabric.models.md#class-harnessconfig): Harness adapter selection plus adapter-owned settings. +- [`models.InstructionConfig`](./nemo_fabric.models.md#class-instructionconfig): One portable instruction value. +- [`models.InstructionsConfig`](./nemo_fabric.models.md#class-instructionsconfig): Harness-neutral agent instructions. - [`models.McpConfig`](./nemo_fabric.models.md#class-mcpconfig): MCP capability configuration. - [`models.McpServerConfig`](./nemo_fabric.models.md#class-mcpserverconfig): MCP server configuration. - [`models.MetadataConfig`](./nemo_fabric.models.md#class-metadataconfig): Human-readable agent identity. @@ -43,12 +45,11 @@ SPDX-License-Identifier: Apache-2.0 --> - [`models.RelayOtlpConfig`](./nemo_fabric.models.md#class-relayotlpconfig): NeMo Relay OTLP export configuration for OpenTelemetry/OpenInference. - [`models.RelayS3StorageConfig`](./nemo_fabric.models.md#class-relays3storageconfig): NeMo Relay ATIF S3 storage configuration. - [`models.RunRequest`](./nemo_fabric.models.md#class-runrequest): One validated NeMo Fabric invocation request. -- [`models.RuntimeConfig`](./nemo_fabric.models.md#class-runtimeconfig): Runtime input/output contract. +- [`models.RuntimeConfig`](./nemo_fabric.models.md#class-runtimeconfig): Invocation runtime contract. - [`models.SkillConfig`](./nemo_fabric.models.md#class-skillconfig): Skill capability configuration. - [`models.TelemetryConfig`](./nemo_fabric.models.md#class-telemetryconfig): Telemetry configuration. - [`models.TelemetryProviderConfig`](./nemo_fabric.models.md#class-telemetryproviderconfig): Provider-specific telemetry configuration. - [`models.ToolsConfig`](./nemo_fabric.models.md#class-toolsconfig): Harness-neutral tool capability configuration. -- [`models.ToolsetConfig`](./nemo_fabric.models.md#class-toolsetconfig): Harness-defined toolset selection and blocking policy. - [`types.AdapterInfo`](./nemo_fabric.types.md#class-adapterinfo): Resolved adapter identity attached to a run plan. - [`types.ArtifactManifest`](./nemo_fabric.types.md#class-artifactmanifest): Normalized collection of artifacts produced by a run. - [`types.ArtifactRef`](./nemo_fabric.types.md#class-artifactref): Reference to one artifact produced by a run. diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index 47ba69790..d1f15e70d 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -190,6 +190,147 @@ Returns the set of fields that have been explicitly set on this model instance. +**Returns:** + A set of strings representing the fields that have been set, i.e. that were not filled from defaults. + + + +--- + + +### classmethod `from_mapping` + +```python +def from_mapping(value: Mapping[str, Any]) -> Self +``` + +Validate a mapping using this Pydantic model. + +--- + + +### method `to_mapping` + +```python +def to_mapping() -> dict[str, Any] +``` + +Return a detached JSON-compatible mapping for Rust/core calls. + + +--- + + +## class `InstructionConfig` + +One portable instruction value. + + + +### Fields + +The model defines the following fields: + +| Field | Type | Required | Default | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `content` | `str` | Yes | — | — | — | +| `mode` | `Literal['replace']` | No | `'replace'` | — | — | + +--- + +### property extra_fields + +Return fields preserved by the extension point for this model. + +--- + +### property model_extra + +Get extra fields set during validation. + + + +**Returns:** + A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. + +--- + +### property model_fields_set + +Returns the set of fields that have been explicitly set on this model instance. + + + +**Returns:** + A set of strings representing the fields that have been set, i.e. that were not filled from defaults. + + + +--- + + +### classmethod `from_mapping` + +```python +def from_mapping(value: Mapping[str, Any]) -> Self +``` + +Validate a mapping using this Pydantic model. + +--- + + +### method `to_mapping` + +```python +def to_mapping() -> dict[str, Any] +``` + +Return a detached JSON-compatible mapping for Rust/core calls. + + +--- + + +## class `InstructionsConfig` + +Harness-neutral agent instructions. + + + +### Fields + +The model defines the following fields: + +| Field | Type | Required | Default | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `system` | `InstructionConfig \| None` | No | `None` | — | — | + +--- + +### property extra_fields + +Return fields preserved by the extension point for this model. + +--- + +### property model_extra + +Get extra fields set during validation. + + + +**Returns:** + A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. + +--- + +### property model_fields_set + +Returns the set of fields that have been explicitly set on this model instance. + + + **Returns:** A set of strings representing the fields that have been set, i.e. that were not filled from defaults. @@ -223,7 +364,7 @@ Return a detached JSON-compatible mapping for Rust/core calls. ## class `RuntimeConfig` -Runtime input/output contract. +Invocation runtime contract. @@ -237,6 +378,7 @@ The model defines the following fields: | `output_schema` | `str \| None` | No | `None` | — | — | | `artifacts` | `str \| Path \| None` | No | `None` | — | — | | `timeout_seconds` | `float \| None` | No | `None` | `Gt(gt=0), _PydanticGeneralMetadata(allow_inf_nan=False)` | — | +| `max_turns` | `int \| None` | No | `None` | `Gt(gt=0)` | — | --- @@ -1708,79 +1850,6 @@ Return a detached JSON-compatible mapping for Rust/core calls. --- -## class `ToolsetConfig` - -Harness-defined toolset selection and blocking policy. - -``enabled=None`` preserves the selected harness default, while an empty ``enabled`` list exposes no toolsets. ``blocked`` excludes toolsets from either the enabled list or the harness default. - - - -### Fields - -The model defines the following fields: - -| Field | Type | Required | Default | Constraints | Description | -| --- | --- | --- | --- | --- | --- | -| `enabled` | `list[str] \| None` | No | `None` | — | Toolsets to expose. None preserves the harness default; an empty list exposes no toolsets. | -| `blocked` | `list[str]` | No | `list()` | — | Toolsets to exclude from the enabled or default harness toolset set. | - ---- - -### property extra_fields - -Return fields preserved by the extension point for this model. - ---- - -### property model_extra - -Get extra fields set during validation. - - - -**Returns:** - A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. - ---- - -### property model_fields_set - -Returns the set of fields that have been explicitly set on this model instance. - - - -**Returns:** - A set of strings representing the fields that have been set, i.e. that were not filled from defaults. - - - ---- - - -### classmethod `from_mapping` - -```python -def from_mapping(value: Mapping[str, Any]) -> Self -``` - -Validate a mapping using this Pydantic model. - ---- - - -### method `to_mapping` - -```python -def to_mapping() -> dict[str, Any] -``` - -Return a detached JSON-compatible mapping for Rust/core calls. - - ---- - - ## class `ToolsConfig` Harness-neutral tool capability configuration. @@ -1793,8 +1862,8 @@ The model defines the following fields: | Field | Type | Required | Default | Constraints | Description | | --- | --- | --- | --- | --- | --- | +| `enabled` | `list[str] \| None` | No | `None` | — | Adapter-native tools to expose. None preserves the harness default; an empty list exposes no tools. | | `blocked` | `list[str]` | No | `list()` | — | — | -| `toolsets` | `ToolsetConfig \| None` | No | `None` | — | — | --- @@ -1872,8 +1941,7 @@ The model defines the following fields: | `runtime` | `RuntimeConfig` | No | `RuntimeConfig()` | — | — | | `environment` | `EnvironmentConfig \| None` | No | `None` | — | — | | `models` | `dict[str, ModelConfig]` | No | `dict()` | — | — | -| `system_prompt` | `str \| None` | No | `None` | — | — | -| `max_turns` | `int \| None` | No | `None` | `Gt(gt=0)` | — | +| `instructions` | `InstructionsConfig \| None` | No | `None` | — | — | | `mcp` | `McpConfig \| None` | No | `None` | — | — | | `skills` | `SkillConfig \| None` | No | `None` | — | — | | `telemetry` | `TelemetryConfig \| None` | No | `None` | — | — | @@ -1953,21 +2021,6 @@ Block adapter-native tool names and return this config. --- -### method `configure_toolsets` - -```python -def configure_toolsets( - *, - enabled: Sequence[str] | None = None, - blocked: Sequence[str] = (), -) -> Self -``` - -Set harness-defined toolset selection and blocking policy. - ---- - - ### method `enable_relay` ```python diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx index 5d8b391b9..76013daf7 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-adapterconfigfield.mdx @@ -14,11 +14,10 @@ pub enum AdapterConfigField { Models, ModelBaseUrl, ModelTemperature, - SystemPrompt, + SystemInstructions, MaxTurns, + EnabledTools, BlockedTools, - EnabledToolsets, - BlockedToolsets, Mcp, Skills, } @@ -46,9 +45,9 @@ Custom model endpoint. Model temperature. -### `SystemPrompt` +### `SystemInstructions` -
+
Portable system instructions. @@ -58,23 +57,17 @@ Portable system instructions. Per-invocation harness turn limit. -### `BlockedTools` - -
- -Adapter-native tool names to block. +### `EnabledTools` -### `EnabledToolsets` +
-
+Adapter-native tool names to expose. -Harness-defined toolsets to expose. - -### `BlockedToolsets` +### `BlockedTools` -
+
-Harness-defined toolsets to block. +Adapter-native tool names to block. ### `Mcp` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx index 387e8f72f..d9c7fcc7d 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitykind.mdx @@ -2,7 +2,7 @@ title: "Enum Capability Kind" sidebar-title: "CapabilityKind" description: "Capability kind." -position: 40 +position: 41 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx index b71a6429e..234b7e510 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-capabilitytarget.mdx @@ -1,8 +1,8 @@ --- title: "Enum Capability Target" sidebar-title: "CapabilityTarget" -description: "Capability routing target." -position: 41 +description: "Component responsible for executing a configured capability." +position: 42 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} @@ -17,7 +17,9 @@ pub enum CapabilityTarget { } ``` -Capability routing target. +Component responsible for executing a configured capability. + +This target describes execution ownership, not network routing. ## Variants @@ -25,19 +27,19 @@ Capability routing target.
-Adapter maps the capability into harness-native config. +The selected adapter maps and executes the capability through its harness. ### `FabricManaged`
-NeMo Fabric exposes or manages the capability around the harness. +NeMo Fabric executes the capability outside the harness-native surface. ### `Unsupported`
-Capability is configured but no executable surface exists. +Neither the adapter nor NeMo Fabric can execute the configured capability. ## Trait Implementations diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-instructionmode.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-instructionmode.mdx new file mode 100644 index 000000000..f5e94243f --- /dev/null +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-instructionmode.mdx @@ -0,0 +1,116 @@ +--- +title: "Enum Instruction Mode" +sidebar-title: "InstructionMode" +description: "How an instruction value is applied to the selected harness." +position: 18 +--- +{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 */} + +Generated from `cargo doc --no-deps -p nemo-fabric-core`. + +```rust +pub enum InstructionMode { + Replace, +} +``` + +How an instruction value is applied to the selected harness. + +## Variants + +### `Replace` + +
+ +Replace the harness default instruction value. + +## Trait Implementations + +### `impl Clone for InstructionMode` + +
Clone for InstructionMode"}} />
+ +#### `clone` + +
clone(&self) -> InstructionMode"}} />
+ +#### `clone_from` + +
clone_from(&mut self, source: &Self)"}} />
+ +### `impl Debug for InstructionMode` + +
Debug for InstructionMode"}} />
+ +#### `fmt` + +
fmt(&self, f: &mut Formatter<'_>) -> Result"}} />
+ +### `impl Default for InstructionMode` + +
Default for InstructionMode"}} />
+ +#### `default` + +
default() -> InstructionMode"}} />
+ +### `impl<'de> Deserialize<'de> for InstructionMode` + +
Deserialize<'de> for InstructionMode"}} />
+ +#### `deserialize` + +
deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where\n    __D: Deserializer<'de>,"}} />
+ +### `impl JsonSchema for InstructionMode` + +
InstructionMode"}} />
+ +#### `schema_name` + +
Cow<'static, str>"}} />
+ +#### `schema_id` + +
Cow<'static, str>"}} />
+ +#### `json_schema` + +
+ +#### `inline_schema` + +
bool"}} />
+ +### `impl PartialEq for InstructionMode` + +
PartialEq for InstructionMode"}} />
+ +#### `eq` + +
eq(&self, other: &InstructionMode) -> bool"}} />
+ +#### `ne` + +
ne(&self, other: &Rhs) -> bool"}} />
+ +### `impl Serialize for InstructionMode` + +
Serialize for InstructionMode"}} />
+ +#### `serialize` + +
serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>where\n    __S: Serializer,"}} />
+ +### `impl Copy for InstructionMode` + +
Copy for InstructionMode"}} />
+ +### `impl Eq for InstructionMode` + +
Eq for InstructionMode"}} />
+ +### `impl StructuralPartialEq for InstructionMode` + +
StructuralPartialEq for InstructionMode"}} />
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx index 0a5e689ca..7c79c7a94 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-mcpexposure.mdx @@ -2,7 +2,7 @@ title: "Enum McpExposure" sidebar-title: "McpExposure" description: "MCP exposure strategy." -position: 18 +position: 21 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx index 8fd8f3f9d..0be6dfb2a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatifstorageconfig.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atif Storage Config" sidebar-title: "RelayAtifStorageConfig" description: "Relay ATIF remote storage configuration." -position: 45 +position: 47 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx index 3435ea75d..9a040d0f4 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofmode.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Mode" sidebar-title: "RelayAtofMode" description: "Relay ATOF file mode." -position: 46 +position: 48 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx index 250a512e6..58c34f63c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofsinkconfig.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Sink Config" sidebar-title: "RelayAtofSinkConfig" description: "Relay ATOF sink configuration." -position: 47 +position: 49 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx index e5bbfe554..d384b634b 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamfieldnamepolicy.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Stream Field Name Policy" sidebar-title: "RelayAtofStreamFieldNamePolicy" description: "Relay ATOF stream field-name policy." -position: 48 +position: 50 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx index dce9b4450..e81ca0d55 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayatofstreamtransport.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Atof Stream Transport" sidebar-title: "RelayAtofStreamTransport" description: "Relay ATOF stream transport." -position: 49 +position: 51 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx index a248e8959..4e3be35c2 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayotlptransport.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Otlp Transport" sidebar-title: "RelayOtlpTransport" description: "Relay OTLP transport." -position: 50 +position: 52 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx index 70bf3d8b8..5b9b5b9a7 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-relayunsupportedbehavior.mdx @@ -2,7 +2,7 @@ title: "Enum Relay Unsupported Behavior" sidebar-title: "RelayUnsupportedBehavior" description: "Relay unsupported/unknown config handling." -position: 51 +position: 53 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx index 5b843a097..fb85a702d 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-resolutionstrategy.mdx @@ -2,7 +2,7 @@ title: "Enum Resolution Strategy" sidebar-title: "ResolutionStrategy" description: "Adapter install or availability strategy." -position: 22 +position: 25 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx index 567fd8c42..27d869fcd 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/enum-telemetryprovider.mdx @@ -2,7 +2,7 @@ title: "Enum Telemetry Provider" sidebar-title: "TelemetryProvider" description: "Telemetry runtime provider." -position: 31 +position: 34 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx index ff10ecb6d..ea329c478 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-load-adapter-descriptor.mdx @@ -2,7 +2,7 @@ title: "Function load_adapter_descriptor" sidebar-title: "load_adapter_descriptor" description: "Load an adapter descriptor from JSON package metadata." -position: 35 +position: 37 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx index bc3d43c8c..5f4098022 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/fn-resolve-run-plan-from-config.mdx @@ -2,7 +2,7 @@ title: "Function resolve_run_plan_from_config" sidebar-title: "resolve_run_plan_from_config" description: "Resolve a typed NVIDIA NeMo Fabric config into a runnable plan." -position: 36 +position: 38 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx index 781ec6517..f8c9d41f6 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/index.mdx @@ -2,7 +2,7 @@ title: "Module config" sidebar-title: "config" description: "NeMo Fabric config models and loading helpers." -position: 68 +position: 70 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} @@ -19,12 +19,14 @@ NeMo Fabric config models and loading helpers. - [AdapterTelemetryProviderSupport](struct-adaptertelemetryprovidersupport.mdx): Telemetry capabilities for one adapter-supported provider. - [AdapterTelemetrySupport](struct-adaptertelemetrysupport.mdx): Adapter telemetry support. - [CapabilityPlan](struct-capabilityplan.mdx): Resolved capability configuration. -- [CapabilityRoute](struct-capabilityroute.mdx): One capability routing decision. +- [CapabilityRoute](struct-capabilityroute.mdx): One capability execution assignment. - [CapabilityTargetPlan](struct-capabilitytargetplan.mdx): Capabilities routed to one target. - [EnvironmentConfig](struct-environmentconfig.mdx): Execution environment configuration. - [EnvironmentPlan](struct-environmentplan.mdx): Resolved environment plan. - [FabricConfig](struct-fabricconfig.mdx): Versioned NVIDIA NeMo Fabric agent config. - [HarnessConfig](struct-harnessconfig.mdx): Harness selection. +- [InstructionConfig](struct-instructionconfig.mdx): One portable instruction value. +- [InstructionsConfig](struct-instructionsconfig.mdx): Harness-neutral agent instruction configuration. - [McpConfig](struct-mcpconfig.mdx): MCP capability configuration. - [McpServerConfig](struct-mcpserverconfig.mdx): MCP server configuration. - [McpServerPlan](struct-mcpserverplan.mdx): Resolved MCP server exposure. @@ -41,14 +43,13 @@ NeMo Fabric config models and loading helpers. - [ResolvedAdapterDescriptor](struct-resolvedadapterdescriptor.mdx): Adapter descriptor selected for a run plan. - [RunPlan](struct-runplan.mdx): Resolved NeMo Fabric run plan. - [RuntimeCapabilities](struct-runtimecapabilities.mdx): Lifecycle behavior implemented by a resolved runtime path. -- [RuntimeConfig](struct-runtimeconfig.mdx): Runtime input/output contract. +- [RuntimeConfig](struct-runtimeconfig.mdx): Invocation runtime contract. - [SkillConfig](struct-skillconfig.mdx): Skill capability configuration. - [TelemetryConfig](struct-telemetryconfig.mdx): Telemetry configuration. - [TelemetryPlan](struct-telemetryplan.mdx): Resolved telemetry plan. - [TelemetryProviderConfig](struct-telemetryproviderconfig.mdx): Provider-specific telemetry configuration. - [ToolsConfig](struct-toolsconfig.mdx): Harness-neutral tool capability configuration. - [ToolsPlan](struct-toolsplan.mdx): Normalized tool policy for a run. -- [ToolsetConfig](struct-toolsetconfig.mdx): Harness-defined toolset capability configuration. ## Enums @@ -56,9 +57,10 @@ NeMo Fabric config models and loading helpers. - [AdapterDescriptorSource](enum-adapterdescriptorsource.mdx): Where NeMo Fabric resolved an adapter descriptor from. - [AdapterKind](enum-adapterkind.mdx): Adapter implementation kind. - [CapabilityKind](enum-capabilitykind.mdx): Capability kind. -- [CapabilityTarget](enum-capabilitytarget.mdx): Capability routing target. +- [CapabilityTarget](enum-capabilitytarget.mdx): Component responsible for executing a configured capability. - [ControlLocation](enum-controllocation.mdx): Where NeMo Fabric control code runs relative to the environment. - [EnvironmentOwnership](enum-environmentownership.mdx): Whether NeMo Fabric owns the underlying environment resource. +- [InstructionMode](enum-instructionmode.mdx): How an instruction value is applied to the selected harness. - [McpExposure](enum-mcpexposure.mdx): MCP exposure strategy. - [RelayAtifStorageConfig](enum-relayatifstorageconfig.mdx): Relay ATIF remote storage configuration. - [RelayAtofMode](enum-relayatofmode.mdx): Relay ATOF file mode. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityroute.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityroute.mdx index f2ddab201..4ba76e7ad 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityroute.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-capabilityroute.mdx @@ -1,7 +1,7 @@ --- title: "Struct Capability Route" sidebar-title: "CapabilityRoute" -description: "One capability routing decision." +description: "One capability execution assignment." position: 7 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. @@ -11,7 +11,9 @@ Generated from `cargo doc --no-deps -p nemo-fabric-core`.
CapabilityKind,\n    pub name: String,\n    pub target: CapabilityTarget,\n    pub reason: String,\n}"}} />
-One capability routing decision. +One capability execution assignment. + +Routes apply to executable tool, skill, and MCP capabilities. Adapter-translated scalar configuration is validated separately against [`AdapterConfigSupport`](struct-adapterconfigsupport.mdx). ## Fields @@ -25,7 +27,7 @@ Capability name. ### `target: CapabilityTarget` -Routing target. +Component responsible for executing the capability. ### `reason: String` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx index 902a8c4f6..fac25b79c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-fabricconfig.mdx @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub metadata: MetadataConfig,\n    pub harness: HarnessConfig,\n    pub models: BTreeMap<String, ModelConfig>,\n    pub system_prompt: Option<String>,\n    pub max_turns: Option<u32>,\n    pub runtime: RuntimeConfig,\n    pub environment: Option<EnvironmentConfig>,\n    pub tools: Option<ToolsConfig>,\n    pub skills: Option<SkillConfig>,\n    pub mcp: Option<McpConfig>,\n    pub telemetry: Option<TelemetryConfig>,\n    pub relay: Option<RelayConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub metadata: MetadataConfig,\n    pub harness: HarnessConfig,\n    pub models: BTreeMap<String, ModelConfig>,\n    pub instructions: Option<InstructionsConfig>,\n    pub runtime: RuntimeConfig,\n    pub environment: Option<EnvironmentConfig>,\n    pub tools: Option<ToolsConfig>,\n    pub skills: Option<SkillConfig>,\n    pub mcp: Option<McpConfig>,\n    pub telemetry: Option<TelemetryConfig>,\n    pub relay: Option<RelayConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Versioned NVIDIA NeMo Fabric agent config. @@ -33,17 +33,13 @@ Harness selection and harness-specific settings. Named model roles. -### `system_prompt: Option` +### `instructions: Option` -Portable system instructions for the selected harness. - -### `max_turns: Option` - -Maximum number of harness turns within one invocation. +Portable agent instructions for the selected harness. ### `runtime: RuntimeConfig` -Runtime input/output contract. +Invocation runtime contract. ### `environment: Option` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionconfig.mdx new file mode 100644 index 000000000..860d3c757 --- /dev/null +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionconfig.mdx @@ -0,0 +1,102 @@ +--- +title: "Struct Instruction Config" +sidebar-title: "InstructionConfig" +description: "One portable instruction value." +position: 17 +--- +{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 */} + +Generated from `cargo doc --no-deps -p nemo-fabric-core`. + +
String,\n    pub mode: InstructionMode,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+ +One portable instruction value. + +## Fields + +### `content: String` + +Instruction text. + +### `mode: InstructionMode` + +How the instruction is applied. + +### `extensions: BTreeMap` + +Additive instruction fields. + +## Trait Implementations + +### `impl Clone for InstructionConfig` + +
Clone for InstructionConfig"}} />
+ +#### `clone` + +
clone(&self) -> InstructionConfig"}} />
+ +#### `clone_from` + +
clone_from(&mut self, source: &Self)"}} />
+ +### `impl Debug for InstructionConfig` + +
Debug for InstructionConfig"}} />
+ +#### `fmt` + +
fmt(&self, f: &mut Formatter<'_>) -> Result"}} />
+ +### `impl<'de> Deserialize<'de> for InstructionConfig` + +
Deserialize<'de> for InstructionConfig"}} />
+ +#### `deserialize` + +
deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where\n    __D: Deserializer<'de>,"}} />
+ +### `impl JsonSchema for InstructionConfig` + +
InstructionConfig"}} />
+ +#### `schema_name` + +
Cow<'static, str>"}} />
+ +#### `schema_id` + +
Cow<'static, str>"}} />
+ +#### `json_schema` + +
+ +#### `inline_schema` + +
bool"}} />
+ +### `impl PartialEq for InstructionConfig` + +
PartialEq for InstructionConfig"}} />
+ +#### `eq` + +
eq(&self, other: &InstructionConfig) -> bool"}} />
+ +#### `ne` + +
ne(&self, other: &Rhs) -> bool"}} />
+ +### `impl Serialize for InstructionConfig` + +
Serialize for InstructionConfig"}} />
+ +#### `serialize` + +
serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>where\n    __S: Serializer,"}} />
+ +### `impl StructuralPartialEq for InstructionConfig` + +
StructuralPartialEq for InstructionConfig"}} />
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionsconfig.mdx similarity index 67% rename from docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx rename to docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionsconfig.mdx index 20c8bc745..dae75c34e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsetconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-instructionsconfig.mdx @@ -1,73 +1,69 @@ --- -title: "Struct Toolset Config" -sidebar-title: "ToolsetConfig" -description: "Harness-defined toolset capability configuration." -position: 34 +title: "Struct Instructions Config" +sidebar-title: "InstructionsConfig" +description: "Harness-neutral agent instruction configuration." +position: 19 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Option<Vec<String>>,\n    pub blocked: Vec<String>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
Option<InstructionConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
-Harness-defined toolset capability configuration. +Harness-neutral agent instruction configuration. ## Fields -### `enabled: Option>` +### `system: Option` -Toolsets to expose. `None` preserves the harness default; an empty list exposes none. - -### `blocked: Vec` - -Toolsets to exclude from the enabled or default harness toolset set. +System instructions for the selected harness. ### `extensions: BTreeMap` -Additive toolset configuration fields. +Additive instruction categories. ## Trait Implementations -### `impl Clone for ToolsetConfig` +### `impl Clone for InstructionsConfig` -
Clone for ToolsetConfig"}} />
+
Clone for InstructionsConfig"}} />
#### `clone` -
clone(&self) -> ToolsetConfig"}} />
+
clone(&self) -> InstructionsConfig"}} />
#### `clone_from`
clone_from(&mut self, source: &Self)"}} />
-### `impl Debug for ToolsetConfig` +### `impl Debug for InstructionsConfig` -
Debug for ToolsetConfig"}} />
+
Debug for InstructionsConfig"}} />
#### `fmt`
fmt(&self, f: &mut Formatter<'_>) -> Result"}} />
-### `impl Default for ToolsetConfig` +### `impl Default for InstructionsConfig` -
Default for ToolsetConfig"}} />
+
Default for InstructionsConfig"}} />
#### `default` -
default() -> ToolsetConfig"}} />
+
default() -> InstructionsConfig"}} />
-### `impl<'de> Deserialize<'de> for ToolsetConfig` +### `impl<'de> Deserialize<'de> for InstructionsConfig` -
Deserialize<'de> for ToolsetConfig"}} />
+
Deserialize<'de> for InstructionsConfig"}} />
#### `deserialize`
deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where\n    __D: Deserializer<'de>,"}} />
-### `impl JsonSchema for ToolsetConfig` +### `impl JsonSchema for InstructionsConfig` -
ToolsetConfig"}} />
+
InstructionsConfig"}} />
#### `schema_name` @@ -85,26 +81,26 @@ Additive toolset configuration fields.
bool"}} />
-### `impl PartialEq for ToolsetConfig` +### `impl PartialEq for InstructionsConfig` -
PartialEq for ToolsetConfig"}} />
+
PartialEq for InstructionsConfig"}} />
#### `eq` -
eq(&self, other: &ToolsetConfig) -> bool"}} />
+
eq(&self, other: &InstructionsConfig) -> bool"}} />
#### `ne`
ne(&self, other: &Rhs) -> bool"}} />
-### `impl Serialize for ToolsetConfig` +### `impl Serialize for InstructionsConfig` -
Serialize for ToolsetConfig"}} />
+
Serialize for InstructionsConfig"}} />
#### `serialize`
serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>where\n    __S: Serializer,"}} />
-### `impl StructuralPartialEq for ToolsetConfig` +### `impl StructuralPartialEq for InstructionsConfig` -
StructuralPartialEq for ToolsetConfig"}} />
+
StructuralPartialEq for InstructionsConfig"}} />
diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx index 7559ad717..40c0c7245 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpconfig.mdx @@ -2,7 +2,7 @@ title: "Struct McpConfig" sidebar-title: "McpConfig" description: "MCP capability configuration." -position: 17 +position: 20 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig.mdx index fe69cc937..a678a04cf 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverconfig.mdx @@ -2,7 +2,7 @@ title: "Struct McpServer Config" sidebar-title: "McpServerConfig" description: "MCP server configuration." -position: 14 +position: 16 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx index 64d174b09..110af3d80 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-mcpserverplan.mdx @@ -2,7 +2,7 @@ title: "Struct McpServer Plan" sidebar-title: "McpServerPlan" description: "Resolved MCP server exposure." -position: 19 +position: 22 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx index 8bf092c3b..2fa649ab6 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-metadataconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Metadata Config" sidebar-title: "MetadataConfig" description: "Human-readable metadata." -position: 20 +position: 23 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx index 1d9b98744..974820a8f 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-modelconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Model Config" sidebar-title: "ModelConfig" description: "Model configuration." -position: 21 +position: 24 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatifconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatifconfig.mdx index 103b49058..b13051b42 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatifconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatifconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Atif Config" sidebar-title: "RelayAtifConfig" description: "Relay ATIF export configuration." -position: 18 +position: 20 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofconfig.mdx index 45315357e..e78abe02e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayatofconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Atof Config" sidebar-title: "RelayAtofConfig" description: "Relay ATOF export configuration." -position: 19 +position: 21 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relaycomponentconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relaycomponentconfig.mdx index 25e5507cb..83149a4ce 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relaycomponentconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relaycomponentconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Component Config" sidebar-title: "RelayComponentConfig" description: "Generic NeMo Relay plugin component configuration." -position: 20 +position: 22 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfig.mdx index 1d076b15e..54d4d066f 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Config" sidebar-title: "RelayConfig" description: "NeMo Relay integration configuration." -position: 21 +position: 23 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfigpolicy.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfigpolicy.mdx index 3c478843a..ae33c3656 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfigpolicy.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayconfigpolicy.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Config Policy" sidebar-title: "RelayConfigPolicy" description: "Relay validation policy." -position: 22 +position: 24 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayobservabilityconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayobservabilityconfig.mdx index ec396f93f..1e31241b6 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayobservabilityconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayobservabilityconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Observability Config" sidebar-title: "RelayObservabilityConfig" description: "NeMo Relay observability component configuration." -position: 23 +position: 25 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayotlpconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayotlpconfig.mdx index a8bccd569..c12913403 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayotlpconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-relayotlpconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Relay Otlp Config" sidebar-title: "RelayOtlpConfig" description: "Relay OpenTelemetry/OpenInference export configuration." -position: 24 +position: 26 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx index d2cf43a1b..d6b5e5afe 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvecontext.mdx @@ -2,7 +2,7 @@ title: "Struct Resolve Context" sidebar-title: "ResolveContext" description: "Source context used when resolving an in-memory NeMo Fabric config." -position: 23 +position: 26 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx index 899bea83b..15ac3e9c2 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-resolvedadapterdescriptor.mdx @@ -2,7 +2,7 @@ title: "Struct Resolved Adapter Descriptor" sidebar-title: "ResolvedAdapterDescriptor" description: "Adapter descriptor selected for a run plan." -position: 24 +position: 27 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx index 68941dc85..4b6d442c7 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runplan.mdx @@ -2,7 +2,7 @@ title: "Struct RunPlan" sidebar-title: "RunPlan" description: "Resolved NeMo Fabric run plan." -position: 25 +position: 28 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx index afd536ee5..77dbf7458 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimecapabilities.mdx @@ -2,7 +2,7 @@ title: "Struct Runtime Capabilities" sidebar-title: "RuntimeCapabilities" description: "Lifecycle behavior implemented by a resolved runtime path." -position: 26 +position: 29 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx index 6443a1367..fe5158125 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-runtimeconfig.mdx @@ -1,17 +1,17 @@ --- title: "Struct Runtime Config" sidebar-title: "RuntimeConfig" -description: "Runtime input/output contract." -position: 27 +description: "Invocation runtime contract." +position: 30 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
String,\n    pub output_schema: String,\n    pub artifacts: Option<PathBuf>,\n    pub timeout_seconds: Option<f64>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
String,\n    pub output_schema: String,\n    pub artifacts: Option<PathBuf>,\n    pub timeout_seconds: Option<f64>,\n    pub max_turns: Option<u32>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
-Runtime input/output contract. +Invocation runtime contract. ## Fields @@ -31,6 +31,10 @@ Artifact directory. Maximum duration of one invocation in seconds. +### `max_turns: Option` + +Maximum number of harness turns within one invocation. + ### `extensions: BTreeMap` Additive normalized runtime fields. diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx index 2fcbd6867..aed265b6a 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-skillconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Skill Config" sidebar-title: "SkillConfig" description: "Skill capability configuration." -position: 28 +position: 31 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx index d4ca6b69d..fb2876211 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Config" sidebar-title: "TelemetryConfig" description: "Telemetry configuration." -position: 29 +position: 32 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx index f58f1b9b3..a7718b157 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryplan.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Plan" sidebar-title: "TelemetryPlan" description: "Resolved telemetry plan." -position: 30 +position: 33 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx index 23b997141..ffd028bff 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-telemetryproviderconfig.mdx @@ -2,7 +2,7 @@ title: "Struct Telemetry Provider Config" sidebar-title: "TelemetryProviderConfig" description: "Provider-specific telemetry configuration." -position: 32 +position: 35 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx index 362d1ca87..acb6ae5dc 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsconfig.mdx @@ -2,26 +2,26 @@ title: "Struct Tools Config" sidebar-title: "ToolsConfig" description: "Harness-neutral tool capability configuration." -position: 33 +position: 36 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Vec<String>,\n    pub toolsets: Option<ToolsetConfig>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
+
Option<Vec<String>>,\n    pub blocked: Vec<String>,\n    pub extensions: BTreeMap<String, Value>,\n}"}} />
Harness-neutral tool capability configuration. ## Fields -### `blocked: Vec` +### `enabled: Option>` -Adapter-native tool names to block. +Adapter-native tool names to expose. `None` preserves the harness default. -### `toolsets: Option` +### `blocked: Vec` -Harness-defined toolset selection and blocking policy. +Adapter-native tool names to block. ### `extensions: BTreeMap` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx index 5893bb8e8..ece23412f 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/config/struct-toolsplan.mdx @@ -2,26 +2,26 @@ title: "Struct Tools Plan" sidebar-title: "ToolsPlan" description: "Normalized tool policy for a run." -position: 35 +position: 37 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} Generated from `cargo doc --no-deps -p nemo-fabric-core`. -
Vec<String>,\n    pub toolsets: Option<ToolsetConfig>,\n}"}} />
+
Option<Vec<String>>,\n    pub blocked: Vec<String>,\n}"}} />
Normalized tool policy for a run. ## Fields -### `blocked: Vec` +### `enabled: Option>` -Adapter-native tool names to block. +Adapter-native tool names to expose. `None` preserves the harness default. -### `toolsets: Option` +### `blocked: Vec` -Harness-defined toolset selection and blocking policy. +Adapter-native tool names to block. ## Trait Implementations diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx index 6eec418f8..92325691e 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/enum-doctorstatus.mdx @@ -2,7 +2,7 @@ title: "Enum Doctor Status" sidebar-title: "DoctorStatus" description: "Diagnostic status." -position: 39 +position: 41 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx index d1ba9024a..b853dc1c3 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/fn-doctor-plan.mdx @@ -2,7 +2,7 @@ title: "Function doctor_plan" sidebar-title: "doctor_plan" description: "Inspect a resolved run plan without mutating the environment." -position: 40 +position: 42 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx index 52a0b34a3..2315d28e1 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/index.mdx @@ -2,7 +2,7 @@ title: "Module doctor" sidebar-title: "doctor" description: "Plan diagnostics for NeMo Fabric." -position: 69 +position: 71 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx index 9ef743e14..677291c5c 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorcheck.mdx @@ -2,7 +2,7 @@ title: "Struct Doctor Check" sidebar-title: "DoctorCheck" description: "Diagnostic check result." -position: 37 +position: 39 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx index e00b15c46..36cb6ffd8 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/doctor/struct-doctorreport.mdx @@ -2,7 +2,7 @@ title: "Struct Doctor Report" sidebar-title: "DoctorReport" description: "Diagnostic report for a resolved run plan." -position: 38 +position: 40 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx index 7cfb8aec4..4281f2a47 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/enum-fabricerror.mdx @@ -2,7 +2,7 @@ title: "Enum Fabric Error" sidebar-title: "FabricError" description: "Errors raised by NeMo Fabric config loading and validation." -position: 41 +position: 43 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx index a5aa4d488..4d130d963 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/index.mdx @@ -2,7 +2,7 @@ title: "Module error" sidebar-title: "error" description: "Error types for NeMo Fabric core." -position: 70 +position: 72 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx index 31a98a30d..5812415f7 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/error/type-result.mdx @@ -2,7 +2,7 @@ title: "Type Alias Result" sidebar-title: "Result" description: "Core NeMo Fabric result type." -position: 42 +position: 44 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx index 4f80bb0d9..7cc834ade 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/fn-version.mdx @@ -2,7 +2,7 @@ title: "Function version" sidebar-title: "version" description: "Returns the crate version compiled into this build." -position: 73 +position: 75 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx index 83a898574..808547014 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/index.mdx @@ -29,6 +29,9 @@ Core config and runtime contract for NeMo Fabric. - `pub use config::EnvironmentPlan;` - `pub use config::FabricConfig;` - `pub use config::HarnessConfig;` +- `pub use config::InstructionConfig;` +- `pub use config::InstructionMode;` +- `pub use config::InstructionsConfig;` - `pub use config::McpConfig;` - `pub use config::McpExposure;` - `pub use config::McpServerPlan;` @@ -46,7 +49,6 @@ Core config and runtime contract for NeMo Fabric. - `pub use config::TelemetryProvider;` - `pub use config::TelemetryProviderConfig;` - `pub use config::ToolsConfig;` -- `pub use config::ToolsetConfig;` - `pub use config::load_adapter_descriptor;` - `pub use config::resolve_run_plan_from_config;` - `pub use doctor::DoctorCheck;` diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx index 22f44b1fd..5131bbd06 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/runtime/index.mdx @@ -2,7 +2,7 @@ title: "Module runtime" sidebar-title: "runtime" description: "Runtime invocation helpers." -position: 71 +position: 73 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx b/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx index 05f2feccd..9f9937838 100644 --- a/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx +++ b/docs/reference/api/rust-library-reference/nemo-fabric-core/schema/index.mdx @@ -2,7 +2,7 @@ title: "Module schema" sidebar-title: "schema" description: "JSON Schema generation for the public NeMo Fabric contract." -position: 72 +position: 74 --- {/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 */} diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index 9684ef3fb..a6ca58fa1 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -40,13 +40,23 @@ from nemo_fabric import ( Fabric, FabricConfig, HarnessConfig, + InstructionConfig, + InstructionsConfig, MetadataConfig, ModelConfig, + RuntimeConfig, ) config = FabricConfig( metadata=MetadataConfig(name="review-agent"), harness=HarnessConfig(adapter_id="nvidia.fabric.hermes"), + instructions=InstructionsConfig( + system=InstructionConfig( + content="Review code for concrete correctness risks.", + mode="replace", + ) + ), + runtime=RuntimeConfig(max_turns=8), models={ "default": ModelConfig( provider="nvidia", @@ -130,30 +140,6 @@ invocations, and attempts to release them during `stop()`. The lifecycle start operation carries the resolved configuration and capability plan. Each subsequent `AdapterInvocation` carries only `runtime_context` and `request`. -### Bundled Adapter Capability Matrix - -The following matrix summarizes the current bundled adapter descriptors. The -descriptor selected in `RunPlan` remains authoritative. Telemetry output names -use the descriptor contract values. - -| Adapter ID | Models | Tools / Blocked Tools | MCP | Skills | Subagents | Telemetry | Persistent Local Host | Remote Service | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `nvidia.fabric.claude` | Native Anthropic or a configured Anthropic Messages-compatible provider | Per-tool blocking through Claude `disallowed_tools`; toolsets unsupported | Normalized | Normalized | Not exposed | NVIDIA NeMo Relay: `atif`, `otel`, and `openinference` through hooks and gateway | Yes: connected `ClaudeSDKClient`, session, and optional NeMo Relay gateway | Not implemented | -| `nvidia.fabric.codex` | Native OpenAI or a configured Responses-compatible provider | Per-tool blocking and toolsets unsupported | Normalized: stdio, HTTP, and streamable HTTP | Normalized: `SKILL.md` directories | Not exposed | NVIDIA NeMo Relay: `atif`, `otel`, and `openinference` through hooks and gateway; native `otel` | Yes: `AsyncCodex` app-server client, thread, and optional NeMo Relay gateway | Not implemented | -| `nvidia.fabric.langchain.deepagents` | NVIDIA, OpenAI, OpenAI-compatible, and other LangChain providers | Per-tool blocking across built-ins, MCP, and local subagents; toolsets unsupported | Normalized | Normalized | Constrained: declarative local subagents inherit parent capabilities | NVIDIA NeMo Relay SDK: `atif`, `otel`, and `openinference`; native `otel` and `openinference` | Yes: compiled graph and async LangGraph checkpointer | Not implemented | -| `nvidia.fabric.hermes` | Configurable provider, model, and base URL | Hermes toolset selection and blocking; per-tool blocking unsupported | Normalized | Normalized | Not exposed | NVIDIA NeMo Relay plugin: `atif`, `otel`, and `openinference` | Yes: `AIAgent`, `SessionDB`, and NeMo Relay plugin context | Not implemented | - -"Normalized" means the adapter accepts the corresponding `FabricConfig` -field and maps it to the harness. "Not normalized" does not mean that the -underlying harness lacks the feature; it means that NeMo Fabric does not expose a -portable configuration surface for it. NeMo Fabric currently normalizes -`tools.blocked`, not a portable tool definition catalog. `tools.toolsets` -selects harness-defined bundles and is intentionally distinct from individual -tool names. Deep Agents supports -only JSON-shaped, in-process subagent definitions through -`harness.settings.deepagents.subagents`; independently configured or remote -subagent capabilities are not exposed. - Third-party local adapters must implement the persistent local-host contract. NeMo Fabric does not currently define a remote-service adapter contract. A crashed persistent host is terminal for that runtime. The same applies when the @@ -194,15 +180,14 @@ by adapter: | `models..base_url` | Yes | Yes | Yes | Yes | | `models..temperature` | No | No | Yes | Yes | | `models..settings.` | No keys declared | No keys declared | No keys declared | No keys declared | -| `system_prompt` | Yes | Yes; maps to Codex base instructions | Yes | Yes | -| `max_turns` | Yes | No | No | Yes; maps to Hermes iterations | +| `instructions.system` | Yes | Yes; maps to Codex base instructions | Yes | Yes | | `runtime.input_schema`, `.output_schema` | Core | Core | Core | Core | | `runtime.artifacts`, `.timeout_seconds` | Core | Core | Core | Core | +| `runtime.max_turns` | Yes | No | No | Yes; maps to Hermes iterations | | `environment.provider`, `.control_location`, `.ownership` | Core | Core | Core | Core | | `environment.workspace`, `.artifacts`, `.env` | Core | Core | Core | Core | | `environment.connection`, `.metadata`, `.settings` | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | Environment-provider-owned | -| `tools.blocked` | Yes | No | Yes | No | -| `tools.toolsets.enabled`, `.blocked` | No | No | No | Yes | +| `tools.enabled`, `.blocked` | Yes | No | Yes | Yes; native selectors are Hermes toolset names | | `skills.paths` | Yes | Yes | Yes | Yes | | `mcp.servers..transport`, `.url` with `harness_native` exposure | Yes | Yes | Yes | Yes | | `mcp.servers..exposure = "fabric_managed"` | No; not implemented | No; not implemented | No; not implemented | No; not implemented | @@ -216,10 +201,12 @@ by adapter: Model selection is deterministic: the `default` role wins; otherwise a single named role is selected. More than one role without `default` fails planning. -If a normalized field has no complete mapping, `plan(...)`, `doctor(...)`, and -runtime start fail with a configuration compatibility error naming the adapter -and field. NeMo Fabric does not silently drop the field or reinterpret a -per-tool name as a toolset. +If a normalized field has no complete mapping, `plan(...)` and runtime start +fail with a configuration compatibility error naming the adapter and field. +`doctor(...)` retains the incompatibility and returns a failed diagnostic check +so callers can inspect all preflight findings. NeMo Fabric does not silently +drop the field. `runtime.max_turns` is optional, so a config that omits it +remains portable across adapters with different native turn-limit support. Build the complete nested `FabricConfig` directly, or start with a base config and use helpers to add capabilities. For example, extend the config from @@ -240,32 +227,30 @@ capability_config.enable_relay( ) ``` -Tool names and toolsets remain separate because adapters expose different -native controls. Configure individual blocked tools only for Claude or Deep -Agents: +Tool names are adapter-native selectors. NeMo Fabric does not define a portable +catalog that translates names between harnesses. Configure an allowlist, a +blocklist, or both: ```python -def with_blocked_tools(claude_or_deepagents_config): - blocked_tools_config = claude_or_deepagents_config.model_copy(deep=True) - blocked_tools_config.block_tools("Bash") - return blocked_tools_config -``` +from nemo_fabric import ToolsConfig -Configure harness-defined toolsets only for Hermes: - -```python -def with_toolsets(hermes_config): - toolset_config = hermes_config.model_copy(deep=True) - toolset_config.configure_toolsets( - enabled=["web", "terminal"], - blocked=["browser"], +def with_tool_policy(base): + tool_config = base.model_copy(deep=True) + tool_config.tools = ToolsConfig( + enabled=["Read", "Edit", "Bash"], + blocked=["WebFetch"], ) - return toolset_config + return tool_config ``` -Config helpers edit the typed config before planning or starting a runtime. They -do not modify already-started runtimes. Use `remove_mcp_server(name)` and -`remove_skill_path(path)` to remove capabilities from a copied config. +`tools.enabled=None` preserves the harness default. An empty list disables every +executable tool. A tool cannot appear in both lists. The selected adapter must +enforce the complete configured policy or planning fails. Hermes interprets +these adapter-native selectors as Hermes toolset names. + +Config helpers edit the typed config before planning or starting a runtime. +They do not modify already-started runtimes. Use `block_tools(...)`, +`remove_mcp_server(name)`, and `remove_skill_path(path)` to edit a copied config. Telemetry is enabled by adding entries to `telemetry.providers`; settings specific to NeMo Relay live in the top-level `relay` block. @@ -626,6 +611,14 @@ report to catch unresolved adapter descriptors, unsupported normalized capabilities, missing declared requirements, and environment problems before starting a runtime. +Capability routes assign execution ownership; they do not describe network +routing. `harness_native` means the selected adapter executes the capability +through its harness. `fabric_managed` means NeMo Fabric executes it outside the +harness-native surface. `unsupported` means neither side can execute it. +Routes apply to tools, skills, and MCP servers. Scalar fields such as +`instructions.system` and `runtime.max_turns` are validated separately against +the adapter descriptor. + ## Install And Runtime Responsibilities In production, the consumer or execution environment is responsible for @@ -650,7 +643,7 @@ Runtime compatibility checks should validate: Use normalized NeMo Fabric fields for portable behavior: models, system instructions, turn limit, runtime, environment, skills, MCP, telemetry, tools, -toolsets, and artifacts. +and artifacts. Supply request context through `RunRequest.context` for each invocation. Request context is not part of `FabricConfig`. diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 96aa938bb..ed0bd4fca 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -77,13 +77,12 @@ container boundary: | `--mcp-config` | `mcp.servers` | | `--ak fabric_telemetry=relay` | `telemetry.providers.relay` and `relay.observability` | | `--ak fabric_model_base_url=` | `models.default.base_url` | -| `--ak fabric_system_prompt=` | `system_prompt` | -| `--ak fabric_max_turns=` | `max_turns` | +| `--ak fabric_system_instruction=` | `instructions.system` | +| `--ak fabric_max_turns=` | `runtime.max_turns` | | `--ak fabric_runtime_timeout_seconds=` | `runtime.timeout_seconds` | | `--ak fabric_environment_env='{...}'` | `environment.env` | | `--ak fabric_blocked_tools='[...]'` | `tools.blocked` | -| `--ak fabric_enabled_toolsets='[...]'` | `tools.toolsets.enabled` | -| `--ak fabric_blocked_toolsets='[...]'` | `tools.toolsets.blocked` | +| `--ak fabric_enabled_tools='[...]'` | `tools.enabled` | | `--ak fabric_harness_settings='{...}'` | `harness.settings` | The result is the complete `FabricConfig` uploaded with the `RunRequest` and diff --git a/skills/integrations/consumer/nemo-fabric-integrate/SKILL.md b/skills/integrations/consumer/nemo-fabric-integrate/SKILL.md index b2b71b918..b3bdf5c06 100644 --- a/skills/integrations/consumer/nemo-fabric-integrate/SKILL.md +++ b/skills/integrations/consumer/nemo-fabric-integrate/SKILL.md @@ -65,42 +65,24 @@ with the public models and helper methods: from nemo_fabric import ( FabricConfig, HarnessConfig, + InstructionConfig, + InstructionsConfig, MetadataConfig, ModelConfig, RuntimeConfig, - ToolsetConfig, ToolsConfig, ) def to_tools_config(job) -> ToolsConfig | None: - blocked_tools = list(job.blocked_tools) - enabled_toolsets = job.enabled_toolsets - blocked_toolsets = list(job.blocked_toolsets) - - if job.adapter_id in { - "nvidia.fabric.claude", - "nvidia.fabric.langchain.deepagents", - }: - if enabled_toolsets is not None or blocked_toolsets: - raise ValueError(f"{job.adapter_id} does not support toolset policy") - return ToolsConfig(blocked=blocked_tools) if blocked_tools else None - - if job.adapter_id == "nvidia.fabric.hermes": - if blocked_tools: - raise ValueError("nvidia.fabric.hermes does not support per-tool blocking") - if enabled_toolsets is None and not blocked_toolsets: - return None - return ToolsConfig( - toolsets=ToolsetConfig( - enabled=enabled_toolsets, - blocked=blocked_toolsets, - ) - ) - - if blocked_tools or enabled_toolsets is not None or blocked_toolsets: - raise ValueError(f"{job.adapter_id} does not support normalized tool policy") - return None + enabled = job.enabled_tools + blocked = list(job.blocked_tools) + if enabled is None and not blocked: + return None + return ToolsConfig( + enabled=None if enabled is None else list(enabled), + blocked=blocked, + ) def to_fabric_config(job) -> FabricConfig: @@ -115,12 +97,18 @@ def to_fabric_config(job) -> FabricConfig: base_url=job.base_url, ) }, - system_prompt=job.system_prompt, - max_turns=job.max_turns, + instructions=( + InstructionsConfig( + system=InstructionConfig(content=job.system_instruction), + ) + if job.system_instruction is not None + else None + ), runtime=RuntimeConfig( input_schema="chat", output_schema="message", timeout_seconds=job.timeout_seconds, + max_turns=job.max_turns, ), tools=to_tools_config(job), ) @@ -134,8 +122,8 @@ def to_fabric_config(job) -> FabricConfig: return config ``` -- Shape capabilities with `block_tools`, `configure_toolsets`, - `add_skill_path`, `remove_skill_path`, +- Shape capabilities with `ToolsConfig`, `block_tools`, `add_skill_path`, + `remove_skill_path`, `add_mcp_server`, `remove_mcp_server`, and `enable_relay`. - Create deployment or evaluation variants with `model_copy(deep=True)` and ordinary Python functions; each copy plans and runs independently. diff --git a/skills/integrations/consumer/nemo-fabric-integrate/references/config-mapping.md b/skills/integrations/consumer/nemo-fabric-integrate/references/config-mapping.md index 2397d5946..e4b1cf706 100644 --- a/skills/integrations/consumer/nemo-fabric-integrate/references/config-mapping.md +++ b/skills/integrations/consumer/nemo-fabric-integrate/references/config-mapping.md @@ -19,9 +19,10 @@ Import these from the top-level `nemo_fabric` package: | `MetadataConfig` | Agent name and description. | | `HarnessConfig` | `adapter_id`, `resolution`, and adapter-owned `settings`. | | `ModelConfig` | Provider, model, credentials (`api_key_env`), endpoint, and sampling. | -| `RuntimeConfig` | Input/output labels, artifact location, and invocation timeout. | +| `InstructionsConfig` / `InstructionConfig` | Portable agent instructions and replacement mode. | +| `RuntimeConfig` | Input/output labels, artifact location, invocation timeout, and harness turn limit. | | `EnvironmentConfig` | Execution environment, workspace, and harness-visible variables. | -| `ToolsConfig` / `ToolsetConfig` | Per-tool blocking and harness-defined toolset policy. | +| `ToolsConfig` | Adapter-native tool selection and blocking policy. | | `McpConfig` / `McpServerConfig` | MCP servers and exposure. | | `SkillConfig` | Skill directories. | | `TelemetryConfig` | Telemetry providers. | @@ -40,17 +41,27 @@ methods that edit the typed config in place and return it: - `add_skill_path(path)` / `remove_skill_path(path)` - `add_mcp_server(name, *, transport, url, exposure, ...)` / `remove_mcp_server(name)` - `enable_relay(...)` for NVIDIA NeMo Relay observability in the `relay` block -- `block_tools(...)` for per-tool deny policy -- `configure_toolsets(enabled=..., blocked=...)` for toolset selection +- `ToolsConfig(enabled=..., blocked=...)` for tool policy +- `block_tools(...)` for additive deny policy ```python config = FabricConfig( metadata=MetadataConfig(name=job.name), harness=HarnessConfig(adapter_id=job.adapter_id, resolution="preinstalled"), models={"default": ModelConfig(provider=job.provider, model=job.model, api_key_env=job.api_key_env, base_url=job.base_url)}, - system_prompt=job.system_prompt, - max_turns=job.max_turns, - runtime=RuntimeConfig(input_schema="chat", output_schema="message", timeout_seconds=job.timeout_seconds), + instructions=( + InstructionsConfig( + system=InstructionConfig(content=job.system_instruction), + ) + if job.system_instruction is not None + else None + ), + runtime=RuntimeConfig( + input_schema="chat", + output_schema="message", + timeout_seconds=job.timeout_seconds, + max_turns=job.max_turns, + ), ) config.add_skill_path(job.skill_dir) ``` @@ -83,8 +94,8 @@ package or job layout, so nothing depends on the process working directory. ## Adapter-Owned And Caller-Owned Data -- Use normalized fields for portable behavior: models, system prompt, turn - limit, runtime, environment, tools and toolsets, skills, MCP, and telemetry. +- Use normalized fields for portable behavior: models, instructions, turn + limit, runtime, environment, tools, skills, MCP, and telemetry. - Supply request context through `RunRequest.context` for each invocation; request context is not part of `FabricConfig`. - Use `harness.settings` for adapter-owned configuration the selected adapter From a1bbcc6b61b9a5207a5e8c2df9ff137c907a0ce7 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 10:48:35 -0700 Subject: [PATCH 18/21] fix: keep generated Rust scaffold warning-free Signed-off-by: Ajay Thorve --- crates/fabric-cli/src/scaffold.rs | 2 +- crates/fabric-cli/templates/rust/main.rs.tmpl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fabric-cli/src/scaffold.rs b/crates/fabric-cli/src/scaffold.rs index 97b387ee7..7973f3d3b 100644 --- a/crates/fabric-cli/src/scaffold.rs +++ b/crates/fabric-cli/src/scaffold.rs @@ -288,7 +288,7 @@ fn render_rust(config: &FabricConfig) -> String { .and_then(|instructions| instructions.system.as_ref()) .map(|instruction| { format!( - "Some(InstructionsConfig {{ system: Some(InstructionConfig {{ content: {}.to_string(), mode: InstructionMode::Replace, extensions: BTreeMap::new() }}), extensions: BTreeMap::new() }})", + "Some(nemo_fabric_core::InstructionsConfig {{ system: Some(nemo_fabric_core::InstructionConfig {{ content: {}.to_string(), mode: nemo_fabric_core::InstructionMode::Replace, extensions: BTreeMap::new() }}), extensions: BTreeMap::new() }})", rust_string(&instruction.content) ) }) diff --git a/crates/fabric-cli/templates/rust/main.rs.tmpl b/crates/fabric-cli/templates/rust/main.rs.tmpl index ddb1caa4d..930c5003f 100644 --- a/crates/fabric-cli/templates/rust/main.rs.tmpl +++ b/crates/fabric-cli/templates/rust/main.rs.tmpl @@ -6,8 +6,8 @@ use std::path::PathBuf; use nemo_fabric_core::{ ControlLocation, EnvironmentConfig, EnvironmentOwnership, FabricConfig, HarnessConfig, - InstructionConfig, InstructionMode, InstructionsConfig, MetadataConfig, ResolutionStrategy, - ResolveContext, RunRequest, RuntimeConfig, SkillConfig, resolve_run_plan_from_config, run_plan, + MetadataConfig, ResolutionStrategy, ResolveContext, RunRequest, RuntimeConfig, SkillConfig, + resolve_run_plan_from_config, run_plan, }; use serde_json::Map; From 5d8ab7275b8964ea45dcfefe5d9adb70247ca7bf Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 10:56:12 -0700 Subject: [PATCH 19/21] test: align Hermes endpoint expectation Signed-off-by: Ajay Thorve --- tests/adapters/test_hermes_adapter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index e69d25dd3..d18b4e7df 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -352,7 +352,6 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( assert config["model"] == { "provider": "nvidia", "default": "nvidia/review-model", - "base_url": "https://integrate.api.nvidia.com/v1", } assert config["terminal"]["cwd"] == str(tmp_path / "workspace") assert config["skills"]["external_dirs"] == [str(tmp_path / "skills" / "review")] From dc3cf23b44940c8f182f815a01e39bd37ce16e07 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 11:42:50 -0700 Subject: [PATCH 20/21] fix: enforce normalized configuration boundaries Signed-off-by: Ajay Thorve --- .../src/nemo_fabric_adapters/codex/adapter.py | 1 - .../nemo_fabric_adapters/hermes/adapter.py | 12 +++++-- crates/fabric-cli/src/app.rs | 2 +- crates/fabric-core/src/config.rs | 34 +++++++++++++++++++ crates/fabric-core/src/runtime.rs | 25 ++++++++++++++ crates/fabric-core/src/schema.rs | 4 +++ docs/integrations/harness/codex.mdx | 4 +-- .../nemo_fabric.models.md | 4 +-- examples/notebooks/02_variations.ipynb | 5 +-- python/src/nemo_fabric/models.py | 14 ++++++-- python/src/nemo_fabric/types.py | 4 +-- schemas/agent.schema.json | 1 + schemas/run-plan.schema.json | 1 + tests/adapters/test_hermes_adapter.py | 11 ++++++ tests/python/test_sdk_contract.py | 13 +++++++ tests/python/test_typed_config.py | 2 +- 16 files changed, 121 insertions(+), 16 deletions(-) diff --git a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py index c8c109925..7a13fc9d7 100644 --- a/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py +++ b/adapters/codex/src/nemo_fabric_adapters/codex/adapter.py @@ -730,7 +730,6 @@ def validate_runtime_payload(payload: dict[str, Any]) -> str: approval_mode(payload) timeout_seconds(payload) for name in ( - "base_instructions", "developer_instructions", "service_tier", ): diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index c4ae1f252..fe9f6b2ca 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -207,7 +207,10 @@ def _artifact_root(payload: dict[str, Any]) -> Path: artifacts = common_utils.runtime_context(payload).get("artifacts") or {} root = artifacts.get("root") if isinstance(artifacts, dict) else None if root: - return Path(str(root)).resolve() + artifact_root = Path(str(root)) + if not artifact_root.is_absolute(): + artifact_root = Path(common_utils.base_dir(payload)) / artifact_root + return artifact_root.resolve() return Path(common_utils.base_dir(payload)).resolve() / "artifacts" @@ -305,6 +308,7 @@ async def start(self, payload: dict[str, Any]) -> None: max_iterations = common_utils.max_turns(payload) if max_iterations is None: max_iterations = DEFAULT_MAX_ITERATIONS + temperature = self._model_config.get("temperature") self._agent = AIAgent( **filter_supported_kwargs( AIAgent, @@ -322,7 +326,11 @@ async def start(self, payload: dict[str, Any]) -> None: self._settings.get("save_trajectories", False) ), max_tokens=self._settings.get("max_tokens", 512), - temperature=self._model_config.get("temperature", 0.0), + request_overrides=( + {"temperature": temperature} + if temperature is not None + else None + ), reasoning_config=self._settings.get( "reasoning_config", {"effort": "none"} ), diff --git a/crates/fabric-cli/src/app.rs b/crates/fabric-cli/src/app.rs index d9faf9c26..032df62eb 100644 --- a/crates/fabric-cli/src/app.rs +++ b/crates/fabric-cli/src/app.rs @@ -48,7 +48,7 @@ enum Command { #[arg(long, default_value = "")] input: String, }, - /// Print the NeMo Fabric core version. + /// Print the NVIDIA NeMo Fabric core version. Version, } diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index aa7211144..24c1d39d5 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -73,6 +73,7 @@ pub enum InstructionMode { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct InstructionConfig { /// Instruction text. + #[schemars(length(min = 1))] pub content: String, /// How the instruction is applied. #[serde(default)] @@ -1110,6 +1111,14 @@ fn validate_config(config: &FabricConfig) -> Result<()> { "must be a finite number greater than zero", ); } + if let Some(system) = config + .instructions + .as_ref() + .and_then(|instructions| instructions.system.as_ref()) + && system.content.trim().is_empty() + { + return invalid_config("instructions.system.content", "must be a non-empty string"); + } for (role, model) in &config.models { if model.provider.trim().is_empty() || model.provider.trim() != model.provider @@ -2209,6 +2218,31 @@ mod tests { })); } + #[test] + fn empty_system_instruction_is_rejected() { + let mut config = typed_config("nvidia.fabric.hermes"); + config.instructions = Some(InstructionsConfig { + system: Some(InstructionConfig { + content: " ".to_string(), + mode: InstructionMode::Replace, + extensions: BTreeMap::new(), + }), + extensions: BTreeMap::new(), + }); + + let error = resolve_run_plan_from_config( + config, + ResolveContext::new("/tmp/fabric-empty-instruction"), + ) + .expect_err("blank instruction must be rejected"); + + assert!(matches!( + error, + FabricError::InvalidConfig { field, .. } + if field == "instructions.system.content" + )); + } + #[test] fn unsupported_normalized_scalar_reports_adapter_config_incompatibility() { for adapter_id in ["nvidia.fabric.codex", "nvidia.fabric.langchain.deepagents"] { diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index 5197763af..c7190842c 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -908,6 +908,12 @@ fn run_local_host_adapter( request: RunRequest, ) -> Result { let timeout = match plan.config.runtime.timeout_seconds { + Some(seconds) if seconds <= 0.0 => { + return Err(FabricError::InvalidConfig { + field: "runtime.timeout_seconds".to_string(), + reason: "must be a finite number greater than zero".to_string(), + }); + } Some(seconds) => { Duration::try_from_secs_f64(seconds).map_err(|_| FabricError::InvalidConfig { field: "runtime.timeout_seconds".to_string(), @@ -2745,6 +2751,25 @@ for line in sys.stdin: let _ = fs::remove_dir_all(root); } + #[test] + fn local_host_rejects_zero_timeout_from_deserialized_plan() { + let (root, plan) = local_host_plan("success"); + let runtime = start_runtime(&plan).expect("start local host"); + let mut invalid_plan = plan.clone(); + invalid_plan.config.runtime.timeout_seconds = Some(0.0); + + let error = run_local_host_adapter(&invalid_plan, &runtime, RunRequest::text("first")) + .expect_err("zero timeout must be rejected"); + + assert!(matches!( + error, + FabricError::InvalidConfig { field, .. } + if field == "runtime.timeout_seconds" + )); + stop_runtime(&plan, &runtime).expect("stop local host"); + let _ = fs::remove_dir_all(root); + } + #[test] fn local_host_timeout_prevents_waiting_invocation_from_reusing_host() { let (root, plan) = local_host_plan("invoke_timeout"); diff --git a/crates/fabric-core/src/schema.rs b/crates/fabric-core/src/schema.rs index 53fb983b9..0a4d3e12c 100644 --- a/crates/fabric-core/src/schema.rs +++ b/crates/fabric-core/src/schema.rs @@ -216,6 +216,10 @@ mod tests { fn agent_schema_enforces_positive_invocation_limits() { let schema = generate_schema(SchemaName::Agent).expect("schema generation"); + assert_eq!( + schema["$defs"]["InstructionConfig"]["properties"]["content"]["minLength"], + 1 + ); assert_eq!( schema["$defs"]["RuntimeConfig"]["properties"]["max_turns"]["minimum"], 1 diff --git a/docs/integrations/harness/codex.mdx b/docs/integrations/harness/codex.mdx index dbd31204f..0ab031770 100644 --- a/docs/integrations/harness/codex.mdx +++ b/docs/integrations/harness/codex.mdx @@ -31,8 +31,8 @@ harness = HarnessConfig(adapter_id="nvidia.fabric.codex") Use normalized `FabricConfig` fields to configure the model, workspace, skills, MCP servers, and telemetry. The Codex adapter does not support normalized `tools.enabled` or `tools.blocked` policy. Use `harness.settings` for supported Codex-specific -options such as `sandbox`, `approval_mode`, `base_instructions`, -`developer_instructions`, and `output_schema`. +options such as `sandbox`, `approval_mode`, `developer_instructions`, and +`output_schema`. Configure base instructions through `instructions.system`. ## Configure MCP and Skills diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index d1f15e70d..f011f1d7f 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -233,7 +233,7 @@ The model defines the following fields: | Field | Type | Required | Default | Constraints | Description | | --- | --- | --- | --- | --- | --- | -| `content` | `str` | Yes | — | — | — | +| `content` | `str` | Yes | — | `MinLen(min_length=1)` | — | | `mode` | `Literal['replace']` | No | `'replace'` | — | — | --- @@ -1863,7 +1863,7 @@ The model defines the following fields: | Field | Type | Required | Default | Constraints | Description | | --- | --- | --- | --- | --- | --- | | `enabled` | `list[str] \| None` | No | `None` | — | Adapter-native tools to expose. None preserves the harness default; an empty list exposes no tools. | -| `blocked` | `list[str]` | No | `list()` | — | — | +| `blocked` | `list[str]` | No | `list()` | — | Adapter-native tool names to deny. | --- diff --git a/examples/notebooks/02_variations.ipynb b/examples/notebooks/02_variations.ipynb index d3cac2f18..593e8123f 100644 --- a/examples/notebooks/02_variations.ipynb +++ b/examples/notebooks/02_variations.ipynb @@ -154,8 +154,9 @@ " (Hermes Agent and Deep Agents use an NVIDIA-hosted model; Codex uses OpenAI;\n", " Claude uses Anthropic).\n", "- **Input schema** — the Codex adapter takes `text`; the others take `chat`.\n", - "- **Instruction delivery** — Hermes Agent, Deep Agents, and Claude take the system\n", - " prompt via `harness.settings`; Codex takes its instruction from the input.\n", + "- **Instruction delivery** — all four harnesses receive normalized system\n", + " instructions through `instructions.system`; Codex maps them to base instructions\n", + " while its per-turn prompt remains `text` input.\n", "- **Capabilities** — `base_config` ships a code-review skill, but not every\n", " harness accepts the same capabilities, so `for_harness` drops it here. That\n", " difference across harnesses is exactly why the\n", diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index feb0cc315..66fde0041 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -108,9 +108,16 @@ class HarnessConfig(FabricBaseModel): class InstructionConfig(FabricBaseModel): """One portable instruction value.""" - content: str + content: str = Field(min_length=1) mode: Literal["replace"] = "replace" + @field_validator("content") + @classmethod + def _validate_content(cls, value: str) -> str: + if not value.strip(): + raise ValueError("instruction content must be a non-empty string") + return value + class InstructionsConfig(FabricBaseModel): """Harness-neutral agent instructions.""" @@ -480,7 +487,10 @@ class ToolsConfig(FabricBaseModel): "an empty list exposes no tools." ), ) - blocked: list[str] = Field(default_factory=list) + blocked: list[str] = Field( + default_factory=list, + description="Adapter-native tool names to deny.", + ) @field_validator("enabled", "blocked") @classmethod diff --git a/python/src/nemo_fabric/types.py b/python/src/nemo_fabric/types.py index 295370ede..18518a60c 100644 --- a/python/src/nemo_fabric/types.py +++ b/python/src/nemo_fabric/types.py @@ -230,9 +230,7 @@ def __init__( raise FabricConfigError("instruction mode must be replace") super().__init__( { - "content": content - if isinstance(content, str) - else _required_text(content, "instruction content"), + "content": _required_text(content, "instruction content"), "mode": mode, }, extra_fields=extra_fields, diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 198efcd6a..6ea412cd7 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -126,6 +126,7 @@ "properties": { "content": { "description": "Instruction text.", + "minLength": 1, "type": "string" }, "mode": { diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index cb118bbd9..6177fd41e 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -699,6 +699,7 @@ "properties": { "content": { "description": "Instruction text.", + "minLength": 1, "type": "string" }, "mode": { diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index d18b4e7df..9284e2719 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -492,6 +492,15 @@ def stop_after_environment_setup(*_args, **_kwargs): await adapter.HermesRuntime().start(payload) +def test_artifact_root_resolves_relative_to_base_dir(tmp_path: Path): + payload = { + "base_dir": str(tmp_path), + "runtime_context": {"artifacts": {"root": "run-artifacts"}}, + } + + assert adapter._artifact_root(payload) == (tmp_path / "run-artifacts").resolve() + + async def test_persistent_runtime_reuses_hermes_agent_session_and_history( monkeypatch, tmp_path: Path, @@ -568,6 +577,7 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( "provider": "test-provider", "model": "test-model", "api_key_env": "TEST_API_KEY", + "temperature": 0.2, } }, }, @@ -616,6 +626,7 @@ async def test_persistent_runtime_reuses_hermes_agent_session_and_history( skip_memory=True, save_trajectories=False, max_tokens=512, + request_overrides={"temperature": 0.2}, reasoning_config={"effort": "none"}, platform="fabric", session_id="runtime-fabric-123", diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index 14e07a6a9..d78d41f15 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -250,6 +250,19 @@ def test_typed_config_serializes_normalized_execution_fields(): ToolsConfig(enabled=["browser"], blocked=["browser"]) +@pytest.mark.parametrize("content", ["", " "]) +def test_instruction_content_must_be_non_empty(content: str): + with pytest.raises(ValidationError): + InstructionConfig(content=content) + + raw = _plan()["config"] + raw["instructions"] = { + "system": {"content": content, "mode": "replace"} + } + with pytest.raises(FabricConfigError, match="non-empty string"): + _FabricConfigSnapshot.from_mapping(raw) + + def test_run_plan_config_block_tools_emits_canonical_shape(): config = _FabricConfigSnapshot.from_mapping(_plan()["config"]) diff --git a/tests/python/test_typed_config.py b/tests/python/test_typed_config.py index 40a7167f2..74c22e520 100644 --- a/tests/python/test_typed_config.py +++ b/tests/python/test_typed_config.py @@ -144,7 +144,7 @@ async def diagnoses_adapter_incompatibility_without_weakening_plan(client: Fabri } ) - with pytest.raises(FabricConfigError, match="runtime.max_turns"): + with pytest.raises(FabricConfigError, match=r"runtime\.max_turns"): client.plan(config, base_dir=ROOT) report = await client.doctor(config, base_dir=ROOT) From 779e157faf9750d3bde4e12c88d165578103b032 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Tue, 28 Jul 2026 12:08:51 -0700 Subject: [PATCH 21/21] fix: validate deserialized configuration boundaries Signed-off-by: Ajay Thorve --- crates/fabric-core/src/config.rs | 18 +++++++--- crates/fabric-core/src/runtime.rs | 35 ++++++++++++++++++- crates/fabric-core/src/schema.rs | 12 +++++++ .../nemo_fabric.models.md | 4 +-- python/src/nemo_fabric/models.py | 5 +-- python/src/nemo_fabric/types.py | 5 ++- schemas/agent.schema.json | 5 +++ schemas/run-plan.schema.json | 5 +++ tests/python/test_sdk_contract.py | 16 +++++++++ 9 files changed, 95 insertions(+), 10 deletions(-) diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 24c1d39d5..7a9a6ca1d 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; -use schemars::JsonSchema; +use schemars::{JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -73,7 +73,7 @@ pub enum InstructionMode { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct InstructionConfig { /// Instruction text. - #[schemars(length(min = 1))] + #[schemars(length(min = 1), regex(pattern = r"\S"))] pub content: String, /// How the instruction is applied. #[serde(default)] @@ -514,7 +514,7 @@ pub struct RuntimeConfig { pub timeout_seconds: Option, /// Maximum number of harness turns within one invocation. #[serde(default, skip_serializing_if = "Option::is_none")] - #[schemars(range(min = 1))] + #[schemars(range(min = 1, max = u32::MAX))] pub max_turns: Option, /// Additive normalized runtime fields. #[serde(default, flatten)] @@ -552,6 +552,7 @@ pub struct EnvironmentConfig { /// or plans are logged or persisted. Prefer `api_key_env`-style /// environment-variable-name indirection for credentials. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[schemars(schema_with = "environment_variables_schema")] pub env: BTreeMap, /// Provider connection metadata, such as server URL, credential reference, or namespace. #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] @@ -567,6 +568,15 @@ pub struct EnvironmentConfig { pub extensions: BTreeMap, } +fn environment_variables_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = BTreeMap::::json_schema(generator); + schema.insert( + "propertyNames".into(), + serde_json::json!({"pattern": r"\S"}), + ); + schema +} + fn default_control_location() -> ControlLocation { ControlLocation::InEnvControl } @@ -1327,7 +1337,7 @@ fn validate_capability_plan_compatibility( }) } -fn validate_adapter_config_compatibility( +pub(crate) fn validate_adapter_config_compatibility( config: &FabricConfig, descriptor: Option<&AdapterDescriptor>, ) -> Result<()> { diff --git a/crates/fabric-core/src/runtime.rs b/crates/fabric-core/src/runtime.rs index c7190842c..e89cd5535 100644 --- a/crates/fabric-core/src/runtime.rs +++ b/crates/fabric-core/src/runtime.rs @@ -21,7 +21,7 @@ use serde_json::{Map, Value}; use crate::config::{ AdapterKind, CapabilityPlan, CapabilityTarget, ControlLocation, EnvironmentOwnership, - FabricConfig, RunPlan, TelemetryPlan, + FabricConfig, RunPlan, TelemetryPlan, validate_adapter_config_compatibility, }; use crate::error::{FabricError, Result}; @@ -568,6 +568,12 @@ pub fn invoke_runtime( } fn validate_adapter_compatibility(plan: &RunPlan) -> Result<()> { + validate_adapter_config_compatibility( + &plan.config, + plan.adapter_descriptor + .as_ref() + .map(|adapter| &adapter.descriptor), + )?; if let Some(route) = plan .capability_plan .routes @@ -2770,6 +2776,33 @@ for line in sys.stdin: let _ = fs::remove_dir_all(root); } + #[test] + fn local_host_revalidates_scalar_compatibility_at_runtime_boundaries() { + let (root, plan) = local_host_plan("success"); + let mut incompatible_plan = plan.clone(); + incompatible_plan.config.runtime.max_turns = Some(1); + + let start_error = + start_runtime(&incompatible_plan).expect_err("start must reject incompatibility"); + assert!(matches!( + start_error, + FabricError::AdapterCompatibility { field, .. } + if field == "runtime.max_turns" + )); + + let runtime = start_runtime(&plan).expect("start local host"); + let invoke_error = invoke_runtime(&incompatible_plan, &runtime, RunRequest::text("first")) + .expect_err("invoke must reject incompatibility"); + assert!(matches!( + invoke_error, + FabricError::AdapterCompatibility { field, .. } + if field == "runtime.max_turns" + )); + + stop_runtime(&plan, &runtime).expect("stop local host"); + let _ = fs::remove_dir_all(root); + } + #[test] fn local_host_timeout_prevents_waiting_invocation_from_reusing_host() { let (root, plan) = local_host_plan("invoke_timeout"); diff --git a/crates/fabric-core/src/schema.rs b/crates/fabric-core/src/schema.rs index 0a4d3e12c..3bb2ac261 100644 --- a/crates/fabric-core/src/schema.rs +++ b/crates/fabric-core/src/schema.rs @@ -220,10 +220,22 @@ mod tests { schema["$defs"]["InstructionConfig"]["properties"]["content"]["minLength"], 1 ); + assert_eq!( + schema["$defs"]["InstructionConfig"]["properties"]["content"]["pattern"], + r"\S" + ); + assert_eq!( + schema["$defs"]["EnvironmentConfig"]["properties"]["env"]["propertyNames"]["pattern"], + r"\S" + ); assert_eq!( schema["$defs"]["RuntimeConfig"]["properties"]["max_turns"]["minimum"], 1 ); + assert_eq!( + schema["$defs"]["RuntimeConfig"]["properties"]["max_turns"]["maximum"], + u32::MAX + ); assert_eq!( schema["$defs"]["RuntimeConfig"]["properties"]["timeout_seconds"]["exclusiveMinimum"], 0.0 diff --git a/docs/reference/api/python-library-reference/nemo_fabric.models.md b/docs/reference/api/python-library-reference/nemo_fabric.models.md index f011f1d7f..d0408bd34 100644 --- a/docs/reference/api/python-library-reference/nemo_fabric.models.md +++ b/docs/reference/api/python-library-reference/nemo_fabric.models.md @@ -233,7 +233,7 @@ The model defines the following fields: | Field | Type | Required | Default | Constraints | Description | | --- | --- | --- | --- | --- | --- | -| `content` | `str` | Yes | — | `MinLen(min_length=1)` | — | +| `content` | `str` | Yes | — | `MinLen(min_length=1), _PydanticGeneralMetadata(pattern='\\S')` | — | | `mode` | `Literal['replace']` | No | `'replace'` | — | — | --- @@ -378,7 +378,7 @@ The model defines the following fields: | `output_schema` | `str \| None` | No | `None` | — | — | | `artifacts` | `str \| Path \| None` | No | `None` | — | — | | `timeout_seconds` | `float \| None` | No | `None` | `Gt(gt=0), _PydanticGeneralMetadata(allow_inf_nan=False)` | — | -| `max_turns` | `int \| None` | No | `None` | `Gt(gt=0)` | — | +| `max_turns` | `int \| None` | No | `None` | `Gt(gt=0), Le(le=4294967295)` | — | --- diff --git a/python/src/nemo_fabric/models.py b/python/src/nemo_fabric/models.py index 66fde0041..7a33095bc 100644 --- a/python/src/nemo_fabric/models.py +++ b/python/src/nemo_fabric/models.py @@ -108,7 +108,7 @@ class HarnessConfig(FabricBaseModel): class InstructionConfig(FabricBaseModel): """One portable instruction value.""" - content: str = Field(min_length=1) + content: str = Field(min_length=1, pattern=r"\S") mode: Literal["replace"] = "replace" @field_validator("content") @@ -136,7 +136,7 @@ class RuntimeConfig(FabricBaseModel): gt=0, allow_inf_nan=False, ) - max_turns: int | None = Field(default=None, gt=0) + max_turns: int | None = Field(default=None, gt=0, le=(1 << 32) - 1) class EnvironmentConfig(FabricBaseModel): @@ -172,6 +172,7 @@ class EnvironmentConfig(FabricBaseModel): "serialized into configuration and run plans; prefer api_key_env-style " "environment-variable-name indirection for credentials." ), + json_schema_extra={"propertyNames": {"pattern": r"\S"}}, ) settings: dict[str, Any] = Field( default_factory=dict, diff --git a/python/src/nemo_fabric/types.py b/python/src/nemo_fabric/types.py index 18518a60c..ad8a66e8f 100644 --- a/python/src/nemo_fabric/types.py +++ b/python/src/nemo_fabric/types.py @@ -325,8 +325,11 @@ def __init__( isinstance(max_turns, bool) or not isinstance(max_turns, int) or max_turns <= 0 + or max_turns > (1 << 32) - 1 ): - raise FabricConfigError("runtime max_turns must be greater than zero") + raise FabricConfigError( + "runtime max_turns must be between 1 and 4294967295" + ) values["max_turns"] = max_turns super().__init__(values, extra_fields=extra_fields) diff --git a/schemas/agent.schema.json b/schemas/agent.schema.json index 6ea412cd7..f42c2a570 100644 --- a/schemas/agent.schema.json +++ b/schemas/agent.schema.json @@ -41,6 +41,9 @@ "type": "string" }, "description": "Environment variables visible to the harness and its tools.\n\nValues are serialized into the run plan and can appear wherever configs\nor plans are logged or persisted. Prefer `api_key_env`-style\nenvironment-variable-name indirection for credentials.", + "propertyNames": { + "pattern": "\\S" + }, "type": "object" }, "metadata": { @@ -127,6 +130,7 @@ "content": { "description": "Instruction text.", "minLength": 1, + "pattern": "\\S", "type": "string" }, "mode": { @@ -956,6 +960,7 @@ "max_turns": { "description": "Maximum number of harness turns within one invocation.", "format": "uint32", + "maximum": 4294967295, "minimum": 1, "type": [ "integer", diff --git a/schemas/run-plan.schema.json b/schemas/run-plan.schema.json index 6177fd41e..957d2b5b0 100644 --- a/schemas/run-plan.schema.json +++ b/schemas/run-plan.schema.json @@ -442,6 +442,9 @@ "type": "string" }, "description": "Environment variables visible to the harness and its tools.\n\nValues are serialized into the run plan and can appear wherever configs\nor plans are logged or persisted. Prefer `api_key_env`-style\nenvironment-variable-name indirection for credentials.", + "propertyNames": { + "pattern": "\\S" + }, "type": "object" }, "metadata": { @@ -700,6 +703,7 @@ "content": { "description": "Instruction text.", "minLength": 1, + "pattern": "\\S", "type": "string" }, "mode": { @@ -1611,6 +1615,7 @@ "max_turns": { "description": "Maximum number of harness turns within one invocation.", "format": "uint32", + "maximum": 4294967295, "minimum": 1, "type": [ "integer", diff --git a/tests/python/test_sdk_contract.py b/tests/python/test_sdk_contract.py index d78d41f15..6dca975f6 100644 --- a/tests/python/test_sdk_contract.py +++ b/tests/python/test_sdk_contract.py @@ -263,6 +263,21 @@ def test_instruction_content_must_be_non_empty(content: str): _FabricConfigSnapshot.from_mapping(raw) +def test_max_turns_matches_rust_u32_range(): + maximum = (1 << 32) - 1 + + assert RuntimeConfig(max_turns=maximum).max_turns == maximum + with pytest.raises(ValidationError): + RuntimeConfig(max_turns=maximum + 1) + + raw = _plan()["config"] + raw["runtime"] = {"max_turns": maximum} + assert _FabricConfigSnapshot.from_mapping(raw).runtime.max_turns == maximum + raw["runtime"]["max_turns"] = maximum + 1 + with pytest.raises(FabricConfigError, match="between 1 and 4294967295"): + _FabricConfigSnapshot.from_mapping(raw) + + def test_run_plan_config_block_tools_emits_canonical_shape(): config = _FabricConfigSnapshot.from_mapping(_plan()["config"]) @@ -521,6 +536,7 @@ def test_environment_model_defines_extension_field_ownership(): assert "existing environment" in properties["connection"]["description"] assert "environment teardown" in properties["ownership"]["description"] assert "outside or inside" in properties["control_location"]["description"] + assert properties["env"]["propertyNames"]["pattern"] == r"\S" def test_inspection_models_are_typed_read_only_mappings():