mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 13:55:17 +08:00
* fix: handle Windows path separators in Policies ToolGuard Guard mode (#13727) Guard mode read generated guard files back from the node template with str(file_name), where file_name is a pathlib.Path from the toolguard result model. On Windows str() yields backslashes, but the files are stored (by sync_generated_guard_code_inputs) under their POSIX relative path via Path.as_posix() (forward slashes). Every lookup therefore missed on Windows, attrs.get(...) returned None, and None["value"] crashed with "'NoneType' object is not subscriptable". - Add PoliciesComponent._template_field_key() to normalize a file name to the POSIX key the sync step writes; route all reads in make_toolguard_result() through it so lookups match on every platform. - Raise a clear "re-run Generate" error when a generated field is missing instead of subscripting None. - Relax validate_before_generate(): api_key is optional (required=False, advanced=True) and credentials can come from the model connection/env, so only the model selection is required. Fixes the spurious "model or api_key cannot be empty!" block. - Regenerate the bundled component index for the updated source. The separate "Generate emits the pass # FIXME stub" and Windows charmap-decode defects are in the upstream toolguard package (latest 0.2.19) and are out of scope here; this component is ready to consume a fixed toolguard release once available. Fixes #13727 * [autofix.ci] apply automated fixes * Update templates * [autofix.ci] apply automated fixes * fix: address review feedback on Policies component - Add `str | Path` type hints to `_template_field_key` and the inner `read_content` helper (mypy compliance). - Give the empty-template `ValueError` in `make_toolguard_result` a descriptive message instead of a bare raise. - Regenerate the bundled component index for the updated source. * Update component_index.json * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -703,7 +703,7 @@
|
||||
},
|
||||
{
|
||||
"name": "cryptography",
|
||||
"version": "49.0.0"
|
||||
"version": "48.0.1"
|
||||
},
|
||||
{
|
||||
"name": "langchain_chroma",
|
||||
|
||||
@ -464,7 +464,7 @@
|
||||
},
|
||||
{
|
||||
"name": "OpenDsStar",
|
||||
"version": null
|
||||
"version": "1.0.26"
|
||||
}
|
||||
],
|
||||
"total_dependencies": 4
|
||||
|
||||
@ -1620,7 +1620,7 @@
|
||||
},
|
||||
{
|
||||
"name": "cryptography",
|
||||
"version": "49.0.0"
|
||||
"version": "48.0.1"
|
||||
},
|
||||
{
|
||||
"name": "langchain_chroma",
|
||||
|
||||
@ -265,7 +265,7 @@ async def test_generate_mode_validation_errors(mock_component):
|
||||
# Test missing model
|
||||
mock_component.in_tools = [MagicMock()]
|
||||
mock_component.model = None
|
||||
with pytest.raises(ValueError, match="model or api_key cannot be empty"):
|
||||
with pytest.raises(ValueError, match="model cannot be empty"):
|
||||
await mock_component.guard_tools()
|
||||
|
||||
# # Test non-recommended model
|
||||
@ -368,4 +368,145 @@ async def test_verify_cached_guards_error_messages(mock_component):
|
||||
assert "Unexpected error" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_template_field_key_normalizes_separators():
|
||||
"""Node-template keys are POSIX; OS-separator file names must normalize to match.
|
||||
|
||||
Regression for #13727: on Windows the toolguard result stores ``file_name`` as
|
||||
a Path whose ``str()`` uses backslashes, while ``sync_generated_guard_code_inputs``
|
||||
keys the template by ``Path.as_posix()`` (forward slashes). The lookup must
|
||||
normalize so it matches on every platform.
|
||||
"""
|
||||
# Forward-slash input is unchanged.
|
||||
assert PoliciesComponent._template_field_key("proj/fetch_content/guard.py") == "proj/fetch_content/guard.py"
|
||||
# Backslash input (what str(WindowsPath(...)) yields) normalizes to forward slashes.
|
||||
assert PoliciesComponent._template_field_key("proj\\fetch_content\\guard.py") == "proj/fetch_content/guard.py"
|
||||
# Path inputs normalize too.
|
||||
assert PoliciesComponent._template_field_key(Path("proj/fetch_content/guard.py")) == "proj/fetch_content/guard.py"
|
||||
# Single-segment names (e.g. result.json) are unaffected.
|
||||
assert PoliciesComponent._template_field_key("result.json") == "result.json"
|
||||
|
||||
|
||||
def _fake_file_twin(file_name):
|
||||
"""A stand-in for toolguard's FileTwin: a ``file_name`` plus assignable ``content``."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
return SimpleNamespace(file_name=file_name, content=None)
|
||||
|
||||
|
||||
def test_make_toolguard_result_reads_posix_keyed_fields(mock_component):
|
||||
"""make_toolguard_result must read fields the sync step keyed by POSIX path.
|
||||
|
||||
Regression for #13727 (the 'NoneType' object is not subscriptable crash on
|
||||
Windows). The toolguard result hands back ``file_name`` values using OS
|
||||
separators (here simulated with backslashes, exactly as ``str(WindowsPath)``
|
||||
produces), while the node template is keyed by forward-slash POSIX paths. The
|
||||
read path must normalize and match instead of returning ``None``.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
# file_name values as they arrive from the result model on Windows (backslashes).
|
||||
types_fn = "proj\\proj_types.py"
|
||||
api_fn = "proj\\proj_api.py"
|
||||
impl_fn = "proj\\proj_api_impl.py"
|
||||
guard_fn = "proj\\fetch_content\\guard.py"
|
||||
item_fn = "proj\\fetch_content\\guard_allowed_url_domains.py"
|
||||
|
||||
fake_result = SimpleNamespace(
|
||||
domain=SimpleNamespace(
|
||||
app_types=_fake_file_twin(types_fn),
|
||||
app_api=_fake_file_twin(api_fn),
|
||||
app_api_impl=_fake_file_twin(impl_fn),
|
||||
),
|
||||
tools={
|
||||
"fetch_content": SimpleNamespace(
|
||||
guard_file=_fake_file_twin(guard_fn),
|
||||
item_guard_files=[_fake_file_twin(item_fn)],
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# Template keyed by POSIX paths, exactly as sync_generated_guard_code_inputs writes them.
|
||||
attrs = {
|
||||
"result.json": {"value": "{}"},
|
||||
"proj/proj_types.py": {"value": "types-content"},
|
||||
"proj/proj_api.py": {"value": "api-content"},
|
||||
"proj/proj_api_impl.py": {"value": "impl-content"},
|
||||
"proj/fetch_content/guard.py": {"value": "guard-content"},
|
||||
"proj/fetch_content/guard_allowed_url_domains.py": {"value": "item-content"},
|
||||
}
|
||||
|
||||
fake_tg = _make_fake_tg(RESULTS_FILENAME="result.json")
|
||||
fake_tg["ToolGuardsCodeGenerationResult"].model_validate_json.return_value = fake_result
|
||||
|
||||
mock_component.get_vertex = MagicMock(return_value=SimpleNamespace(data={"node": {"template": attrs}}))
|
||||
|
||||
with patch.object(PoliciesComponent, "_import_toolguard", return_value=fake_tg):
|
||||
result = mock_component.make_toolguard_result()
|
||||
|
||||
assert result is fake_result
|
||||
assert result.domain.app_types.content == "types-content"
|
||||
assert result.domain.app_api.content == "api-content"
|
||||
assert result.domain.app_api_impl.content == "impl-content"
|
||||
assert result.tools["fetch_content"].guard_file.content == "guard-content"
|
||||
assert result.tools["fetch_content"].item_guard_files[0].content == "item-content"
|
||||
|
||||
|
||||
def test_make_toolguard_result_missing_field_raises_clear_error(mock_component):
|
||||
"""A missing generated field yields a clear ValueError, not a NoneType subscript.
|
||||
|
||||
Before #13727's fix a missing key surfaced as
|
||||
``'NoneType' object is not subscriptable``; it must now point the user at
|
||||
Generate mode instead.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
fake_result = SimpleNamespace(
|
||||
domain=SimpleNamespace(
|
||||
app_types=_fake_file_twin("proj/proj_types.py"),
|
||||
app_api=_fake_file_twin("proj/proj_api.py"),
|
||||
app_api_impl=_fake_file_twin("proj/proj_api_impl.py"),
|
||||
),
|
||||
tools={},
|
||||
)
|
||||
|
||||
# app_types key is intentionally absent from the template.
|
||||
attrs = {
|
||||
"result.json": {"value": "{}"},
|
||||
"proj/proj_api.py": {"value": "api-content"},
|
||||
"proj/proj_api_impl.py": {"value": "impl-content"},
|
||||
}
|
||||
|
||||
fake_tg = _make_fake_tg(RESULTS_FILENAME="result.json")
|
||||
fake_tg["ToolGuardsCodeGenerationResult"].model_validate_json.return_value = fake_result
|
||||
|
||||
mock_component.get_vertex = MagicMock(return_value=SimpleNamespace(data={"node": {"template": attrs}}))
|
||||
|
||||
with (
|
||||
patch.object(PoliciesComponent, "_import_toolguard", return_value=fake_tg),
|
||||
pytest.raises(ValueError, match="missing from the component"),
|
||||
):
|
||||
mock_component.make_toolguard_result()
|
||||
|
||||
|
||||
def test_validate_before_generate_allows_empty_api_key(mock_component):
|
||||
"""api_key is optional: validation passes when only the model is set.
|
||||
|
||||
The field is declared required=False/advanced=True and credentials often come
|
||||
from the model connection or environment, so requiring api_key here wrongly
|
||||
blocked valid setups (the "model or api_key cannot be empty!" wart in #13727).
|
||||
"""
|
||||
mock_component.api_key = ""
|
||||
mock_component.model = [{"name": "gpt-5.1", "provider": "OpenAI"}]
|
||||
# Should not raise.
|
||||
mock_component.validate_before_generate()
|
||||
|
||||
|
||||
def test_validate_before_generate_still_requires_model(mock_component):
|
||||
"""A missing model is still rejected after relaxing the api_key requirement."""
|
||||
mock_component.api_key = ""
|
||||
mock_component.model = None
|
||||
with pytest.raises(ValueError, match="model cannot be empty"):
|
||||
mock_component.validate_before_generate()
|
||||
|
||||
|
||||
# Made with Bob
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -265,8 +265,14 @@ Powered by [ALTK ToolGuard](https://github.com/AgentToolkit/toolguard )"""
|
||||
msg = "Policies: in_tools cannot be empty!"
|
||||
raise ValueError(msg)
|
||||
|
||||
if not self.model or not self.api_key:
|
||||
msg = "Policies: model or api_key cannot be empty!"
|
||||
# Only the model selection is mandatory. ``api_key`` is declared optional
|
||||
# (required=False, advanced=True) and is frequently supplied by the model
|
||||
# connection, an environment variable, or a global variable rather than by
|
||||
# this field — requiring it here wrongly blocked valid setups with
|
||||
# "model or api_key cannot be empty!". When credentials really are missing,
|
||||
# ``build_model`` -> ``get_llm`` raises a clear provider-specific error.
|
||||
if not self.model:
|
||||
msg = "Policies: model cannot be empty!"
|
||||
raise ValueError(msg)
|
||||
|
||||
# uncomment if willing to enforce certain models for buildtime
|
||||
@ -316,23 +322,64 @@ Powered by [ALTK ToolGuard](https://github.com/AgentToolkit/toolguard )"""
|
||||
|
||||
self._verify_cached_guards(code_dir)
|
||||
|
||||
@staticmethod
|
||||
def _template_field_key(file_name: str | Path) -> str:
|
||||
r"""Normalize a generated file name to its node-template field key.
|
||||
|
||||
``sync_generated_guard_code_inputs`` keys every generated CodeInput by the
|
||||
file's POSIX relative path (``Path.relative_to(...).as_posix()``), so the
|
||||
keys always use forward slashes. The toolguard result model stores
|
||||
``file_name`` as a :class:`pathlib.Path`, whose ``str()`` uses the OS
|
||||
separator — backslashes on Windows. Reading back with ``str(file_name)``
|
||||
therefore misses every key on Windows, ``attrs.get(...)`` returns ``None``
|
||||
and the subsequent ``["value"]`` raised the cryptic
|
||||
``'NoneType' object is not subscriptable`` (issue #13727).
|
||||
|
||||
Normalizing through ``as_posix()`` is the exact inverse of how the keys are
|
||||
written, so lookups match on every platform. ``replace("\\", "/")`` is a
|
||||
belt-and-suspenders guard for the rare case where ``file_name`` is already a
|
||||
string carrying Windows separators (e.g. a flow generated on Windows and
|
||||
opened on POSIX, where ``PurePosixPath`` would not split on backslashes).
|
||||
"""
|
||||
return Path(str(file_name).replace("\\", "/")).as_posix()
|
||||
|
||||
def make_toolguard_result(self) -> ToolGuardsCodeGenerationResult:
|
||||
tg = self._import_toolguard()
|
||||
attrs = self.get_vertex().data["node"]["template"]
|
||||
if not attrs:
|
||||
raise ValueError
|
||||
msg = "Policies: component template data is missing. This may indicate a corrupted flow state."
|
||||
raise ValueError(msg)
|
||||
|
||||
result_str = attrs[str(tg["RESULTS_FILENAME"])]["value"]
|
||||
def read_content(file_name: str | Path) -> str:
|
||||
"""Fetch a generated file's stored source from the node template.
|
||||
|
||||
Raises a clear, actionable error when the field is absent instead of
|
||||
letting a missing key surface as ``'NoneType' object is not
|
||||
subscriptable``. A missing field means the guard code was never
|
||||
generated (or only the ``pass # FIXME`` scaffold was produced), so the
|
||||
fix is to re-run Generate.
|
||||
"""
|
||||
key = self._template_field_key(file_name)
|
||||
field = attrs.get(key)
|
||||
if field is None:
|
||||
msg = (
|
||||
f"Policies: generated guard file '{key}' is missing from the component. "
|
||||
f"Re-run in 'Generate' mode to (re)build the guard code before guarding."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return field["value"]
|
||||
|
||||
result_str = read_content(tg["RESULTS_FILENAME"])
|
||||
result = tg["ToolGuardsCodeGenerationResult"].model_validate_json(result_str)
|
||||
|
||||
result.domain.app_types.content = attrs.get(str(result.domain.app_types.file_name))["value"]
|
||||
result.domain.app_api.content = attrs.get(str(result.domain.app_api.file_name))["value"]
|
||||
result.domain.app_api_impl.content = attrs.get(str(result.domain.app_api_impl.file_name))["value"]
|
||||
result.domain.app_types.content = read_content(result.domain.app_types.file_name)
|
||||
result.domain.app_api.content = read_content(result.domain.app_api.file_name)
|
||||
result.domain.app_api_impl.content = read_content(result.domain.app_api_impl.file_name)
|
||||
|
||||
for tool in result.tools.values():
|
||||
tool.guard_file.content = attrs.get(str(tool.guard_file.file_name))["value"]
|
||||
tool.guard_file.content = read_content(tool.guard_file.file_name)
|
||||
for tool_item in tool.item_guard_files:
|
||||
tool_item.content = attrs.get(str(tool_item.file_name))["value"]
|
||||
tool_item.content = read_content(tool_item.file_name)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user