Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2c3fe7f
docs: update component links to individual pages (#10706)
mendonk Nov 25, 2025
168b84c
fix: avoid updating Message if ChatOutput is connected to ChatInput (…
keval718 Nov 25, 2025
0833dfb
Feat: Runflow optimization and improved dropdown behavior (#10720)
HzaRashid Nov 25, 2025
5040efc
fix: Add dynamic tool mode descriptions for agent integration (#10744)
Cristhianzl Nov 27, 2025
c46d31c
fix: Add profile picture management and API endpoints (#10763)
Cristhianzl Dec 1, 2025
2d735a7
deps: upgrade altk (#10804)
jordanrfrazier Dec 1, 2025
17aff85
fix: use running event loop to fix asyncio error when calling mcp too…
jordanrfrazier Dec 1, 2025
956baf6
fix: Improve file processing robustness and error feedback (#10781)
Cristhianzl Dec 1, 2025
4529e92
fix: resolve merge conflict (#10831)
keval718 Dec 1, 2025
e89bee0
fix: fixed warning on console for nested button (#10724) (#10832)
olayinkaadelakun Dec 1, 2025
f7a82f3
fix: fixed warning on console (#10745) (#10830)
olayinkaadelakun Dec 1, 2025
73120ad
fix: mask value to hide null field being returned (#10778) (#10829)
olayinkaadelakun Dec 1, 2025
e321e3e
Fix: Allow refresh list button to stay stagnant while zoom (Safari) (…
olayinkaadelakun Dec 1, 2025
423419e
feat: Add superuser support for running any user flow (#10808)
Cristhianzl Dec 1, 2025
a562670
Revert "feat: Add superuser support for running any user flow (#10808)"
Cristhianzl Dec 2, 2025
4df89f1
fix: Ollama models list in Agent component (#10814)
HimavarshaVS Dec 2, 2025
c66c679
Fix: Ensure Default Tab is Credential (#10779) (#10826)
olayinkaadelakun Dec 2, 2025
2a3d424
chore: update cuga version (#10737) (#10738)
jordanrfrazier Dec 2, 2025
731cc8b
chore: Remove DataFrameToToolsetComponent and related tests (#10845)
edwinjosechittilappilly Dec 3, 2025
3a2395b
fix: Handle GCP JSON parsing credentials (#10859)
erichare Dec 3, 2025
9be8720
fix: anthropic constants (#10862)
edwinjosechittilappilly Dec 3, 2025
5b09d60
fix: Add feature flag check to simplified_run_flow_session (#10863)
edwinjosechittilappilly Dec 3, 2025
97164d8
fix: Improve the debugging messages on startup (#10864)
erichare Dec 3, 2025
c22ebff
fix: Don't fail if doc column is missing (#10746) (#10872)
erichare Dec 3, 2025
7f5940e
add x-api-key auth option
Cristhianzl Dec 4, 2025
d04142b
fix(auth): Disallow refresh token access to API endpoints
mpawlow Dec 2, 2025
a1ce944
fix: Properly support the Batch Run component for watsonX models (#10…
erichare Dec 4, 2025
a9ef7fb
fix: Image upload for Gemini/Anthropic (#10880)
erichare Dec 4, 2025
05d5a1e
fix: Improve the default startup logging for readability (#10894)
erichare Dec 4, 2025
efcae53
Fix: lfx serve aysncio event loop error (#10888)
HzaRashid Dec 4, 2025
1174a6a
fix: Update LangflowCounts component to format star and Discord count…
viktoravelino Dec 4, 2025
99e73b6
Fix: update lfx serve tests to mock the .serve() to prevent hanging …
HzaRashid Dec 5, 2025
3beffea
Fix: lfx run agent _noopresult not iterable error (#10893)
HzaRashid Dec 5, 2025
f312f22
Fix: lfx run agent _noopresult not iterable error (#10911)
HzaRashid Dec 5, 2025
5ccd44e
fix: Add graceful subprocess cleanup during shutdown (#10906)
Cristhianzl Dec 5, 2025
a62b851
fix(workflows): include src/lfx/uv.lock in git add command to ensure …
Cristhianzl Dec 7, 2025
8245904
chore(nightly_build.yml): remove unnecessary directory change for lfx…
Cristhianzl Dec 7, 2025
44a9f70
chore(release_nightly): update build command to include --no-sources …
Cristhianzl Dec 7, 2025
5b7e332
chore(chat.py): remove unused future annotations import to clean up code
Cristhianzl Dec 7, 2025
3c1d0d2
fix(chat.py): add future annotations import for better type hinting s…
Cristhianzl Dec 7, 2025
6566275
chore: print version
Adam-Aghili Dec 8, 2025
22c9237
chore: use release_tag as version
Adam-Aghili Dec 8, 2025
5cffeae
fix: --prerelease=allow
Adam-Aghili Dec 8, 2025
6a0fd4d
fix: correctly raise file not found errors in File GET endpoints (#1…
jordanrfrazier Dec 8, 2025
2f157c9
fix: image pathing to operate with s3 storage (#10919) (#10929)
jordanrfrazier Dec 8, 2025
5550861
Feat: migrate MCP transport from SSE to streamable http (#10934)
HzaRashid Dec 8, 2025
5e54758
refactor(deps.py): reorganize imports for clarity and compliance with…
Cristhianzl Dec 8, 2025
af529b3
fix: update sidebar icon styles to maintain backward compatibility (#…
viktoravelino Dec 10, 2025
b6ed2bc
fix: Add empty input check in ALTKAgent for Anthropic (#10926)
jordanrfrazier Dec 10, 2025
f184989
fix: add condition to not make folder download fail when flow has Not…
lucaseduoli Dec 10, 2025
7b66dfb
fix: Enhance error handling for langchain-core version compatibility …
ogabrielluiz Dec 10, 2025
76af6b9
fix: Restrict message and session access to flow owners (#10973)
Cristhianzl Dec 11, 2025
c160933
Fix: lfx run with agent component throws '_NoopResult' object is not …
HzaRashid Dec 11, 2025
3200a2e
fix: Support tool mode for components that have no inputs (#10982)
erichare Dec 11, 2025
ac38023
fix: (Cherry Pick) default Ollama base url (#10981)
erichare Dec 11, 2025
7ba8c73
fix: Add authentication to various endpoints (#10977) (#10985)
erichare Dec 12, 2025
b17adfa
Fix: cuga integration (#10976)
sami-marreed Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: Handle GCP JSON parsing credentials (#10859)
fix: Proper parsing of GCP credentials JSON (#10828)

* fix: Proper parsing of GCP credentials JSON

* Update save_file.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Update test_save_file_component.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Fix GCP issues

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update test_save_file_component.py

* Update save_file.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update save_file.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Update save_file.py

* Fix ruff errors

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* [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>
  • Loading branch information
erichare and autofix-ci[bot] authored Dec 3, 2025
commit 3a2395b9b6e08a57e85dd111147f7a0f2d3e52d9
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,141 @@ async def test_append_mode_txt_file(self, component_class):
# Clean up temp file
if tmp_path.exists():
tmp_path.unlink()

@pytest.mark.asyncio
async def test_google_drive_credential_parsing_with_control_characters(self, component_class):
"""Test that GCP service account JSON with literal newlines (control characters) can be parsed.

This tests the fix for the bug where pasted GCP service account JSON fails with:
'Invalid control character at: line 1 column 183 (char 182)'
"""
component = component_class(_user_id=str(uuid4()))

# Simulate a GCP service account JSON with literal newlines in the private_key field.
# Use a clearly fake, short key to avoid tripping secret scanners while preserving the newline pattern.
fake_private_key = "-----BEGIN KEY-----\nFAKE\n-----END KEY-----\n"
service_account_json = (
f'{{"type": "service_account", "project_id": "test-project-123", "private_key": "{fake_private_key}"}}'
)

message = Message(text="test content")
component.set_attributes(
{
"input": message,
"file_name": "test_gdrive_file",
"gdrive_format": "txt",
"storage_location": [{"name": "Google Drive"}],
"service_account_key": service_account_json,
"folder_id": "test_folder_id_123",
}
)

# Mock Google Drive dependencies
with (
patch("google.oauth2.service_account.Credentials.from_service_account_info") as mock_creds,
patch("googleapiclient.discovery.build") as mock_build,
):
mock_drive_service = MagicMock()
mock_build.return_value = mock_drive_service

# Mock the file upload response
mock_drive_service.files().create().execute.return_value = {"id": "file123"}

result = await component.save_to_file()

# Verify credentials were parsed successfully (should not raise JSONDecodeError)
mock_creds.assert_called_once()
creds_dict = mock_creds.call_args[0][0]

# Verify the parsed credentials have the expected structure
assert creds_dict["type"] == "service_account"
assert creds_dict["project_id"] == "test-project-123"
assert "private_key" in creds_dict
assert "BEGIN KEY" in creds_dict["private_key"]

# Verify successful upload message
assert "successfully uploaded to Google Drive" in result.text
assert "file123" in result.text

@pytest.mark.asyncio
async def test_google_drive_credential_parsing_strategies(self, component_class):
"""Test various GCP credential parsing strategies."""
component = component_class(_user_id=str(uuid4()))

test_cases = [
# Case 1: Normal JSON (should work)
('{"type": "service_account", "project_id": "test"}', "Normal JSON"),
# Case 2: JSON with literal newlines (the bug case)
('{"type": "service_account", "private_key": "-----BEGIN\nKEY\n-----END"}', "With control chars"),
# Case 3: JSON with extra whitespace
(' \n{"type": "service_account", "project_id": "test"} \n', "With whitespace"),
]

for service_account_json, test_name in test_cases:
message = Message(text="test")
component.set_attributes(
{
"input": message,
"file_name": "test_file",
"gdrive_format": "txt",
"storage_location": [{"name": "Google Drive"}],
"service_account_key": service_account_json,
"folder_id": "test_folder",
}
)

with (
patch("google.oauth2.service_account.Credentials.from_service_account_info") as mock_creds,
patch("googleapiclient.discovery.build") as mock_build,
):
mock_drive_service = MagicMock()
mock_build.return_value = mock_drive_service
mock_drive_service.files().create().execute.return_value = {"id": f"file_{test_name}"}

# Should not raise JSONDecodeError for any case
await component.save_to_file()

# Verify credentials were parsed
mock_creds.assert_called_once()
creds_dict = mock_creds.call_args[0][0]
assert isinstance(creds_dict, dict)
assert creds_dict["type"] == "service_account"

mock_creds.reset_mock()

def test_append_mode_hidden_for_cloud_storage(self, component_class):
"""Test that append_mode is hidden for AWS and Google Drive storage."""
component = component_class()

# Test Local storage - append_mode should be visible
build_config = {
"file_name": {"show": False},
"append_mode": {"show": False},
"local_format": {"show": False},
}
result = component.update_build_config(build_config, [{"name": "Local"}], "storage_location")
assert result["append_mode"]["show"] is True, "append_mode should be visible for Local storage"
assert result["file_name"]["show"] is True
assert result["local_format"]["show"] is True

# Test AWS storage - append_mode should be hidden
build_config = {
"file_name": {"show": False},
"append_mode": {"show": False},
"aws_format": {"show": False},
}
result = component.update_build_config(build_config, [{"name": "AWS"}], "storage_location")
assert result["append_mode"]["show"] is False, "append_mode should be hidden for AWS storage"
assert result["file_name"]["show"] is True
assert result["aws_format"]["show"] is True

# Test Google Drive storage - append_mode should be hidden
build_config = {
"file_name": {"show": False},
"append_mode": {"show": False},
"gdrive_format": {"show": False},
}
result = component.update_build_config(build_config, [{"name": "Google Drive"}], "storage_location")
assert result["append_mode"]["show"] is False, "append_mode should be hidden for Google Drive storage"
assert result["file_name"]["show"] is True
assert result["gdrive_format"]["show"] is True
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

91 changes: 79 additions & 12 deletions src/lfx/src/lfx/components/files_and_knowledge/save_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ class SaveToFileComponent(Component):
BoolInput(
name="append_mode",
display_name="Append",
info="Append to file if it exists (only for plain text formats). Disabled for binary formats like Excel.",
info=(
"Append to file if it exists (only for Local storage with plain text formats). "
"Not supported for cloud storage (AWS/Google Drive)."
),
value=False,
show=False,
),
Expand Down Expand Up @@ -157,6 +160,7 @@ class SaveToFileComponent(Component):
"The Google Drive folder ID where the file will be uploaded. "
"The folder must be shared with the service account email."
),
required=True,
show=False,
advanced=True,
),
Expand Down Expand Up @@ -196,11 +200,13 @@ def update_build_config(self, build_config, field_value, field_name=None):
if len(selected) == 1:
location = selected[0]

# Show file_name and append_mode when any storage location is selected
# Show file_name when any storage location is selected
if "file_name" in build_config:
build_config["file_name"]["show"] = True

# Show append_mode only for Local storage (not supported for cloud storage)
if "append_mode" in build_config:
build_config["append_mode"]["show"] = True
build_config["append_mode"]["show"] = location == "Local"

if location == "Local":
if "local_format" in build_config:
Expand Down Expand Up @@ -575,7 +581,9 @@ async def _save_to_aws(self) -> Message:
# Create temporary file
import tempfile

with tempfile.NamedTemporaryFile(mode="w", suffix=f".{file_format}", delete=False) as temp_file:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", suffix=f".{file_format}", delete=False
) as temp_file:
temp_file.write(content)
temp_file_path = temp_file.name

Expand Down Expand Up @@ -611,16 +619,57 @@ async def _save_to_google_drive(self) -> Message:
msg = "Google API client libraries are not installed. Please install them."
raise ImportError(msg) from e

# Parse credentials
# Parse credentials with multiple fallback strategies
credentials_dict = None
parse_errors = []

# Strategy 1: Parse as-is with strict=False to allow control characters
try:
credentials_dict = json.loads(self.service_account_key)
credentials_dict = json.loads(self.service_account_key, strict=False)
except json.JSONDecodeError as e:
msg = f"Invalid JSON in service account key: {e!s}"
raise ValueError(msg) from e
parse_errors.append(f"Standard parse: {e!s}")

# Strategy 2: Strip whitespace and try again
if credentials_dict is None:
try:
cleaned_key = self.service_account_key.strip()
credentials_dict = json.loads(cleaned_key, strict=False)
except json.JSONDecodeError as e:
parse_errors.append(f"Stripped parse: {e!s}")

# Strategy 3: Check if it's double-encoded (JSON string of a JSON string)
if credentials_dict is None:
try:
decoded_once = json.loads(self.service_account_key, strict=False)
if isinstance(decoded_once, str):
credentials_dict = json.loads(decoded_once, strict=False)
else:
credentials_dict = decoded_once
except json.JSONDecodeError as e:
parse_errors.append(f"Double-encoded parse: {e!s}")

# Strategy 4: Try to fix common issues with newlines in the private_key field
if credentials_dict is None:
try:
# Replace literal \n with actual newlines which is common in pasted JSON
fixed_key = self.service_account_key.replace("\\n", "\n")
credentials_dict = json.loads(fixed_key, strict=False)
except json.JSONDecodeError as e:
parse_errors.append(f"Newline-fixed parse: {e!s}")

if credentials_dict is None:
error_details = "; ".join(parse_errors)
msg = (
f"Unable to parse service account key JSON. Tried multiple strategies: {error_details}. "
"Please ensure you've copied the entire JSON content from your service account key file. "
"The JSON should start with '{' and contain fields like 'type', 'project_id', 'private_key', etc."
)
raise ValueError(msg)

# Create Google Drive service
# Create Google Drive service with appropriate scopes
# Use drive scope for folder access, file scope is too restrictive for folder verification
credentials = service_account.Credentials.from_service_account_info(
credentials_dict, scopes=["https://www.googleapis.com/auth/drive.file"]
credentials_dict, scopes=["https://www.googleapis.com/auth/drive"]
)
drive_service = build("drive", "v3", credentials=credentials)

Expand All @@ -634,16 +683,34 @@ async def _save_to_google_drive(self) -> Message:

# Create temporary file
file_path = f"{self.file_name}.{file_format}"
with tempfile.NamedTemporaryFile(mode="w", suffix=f".{file_format}", delete=False) as temp_file:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
suffix=f".{file_format}",
delete=False,
) as temp_file:
temp_file.write(content)
temp_file_path = temp_file.name

try:
# Upload to Google Drive
# Note: We skip explicit folder verification since it requires broader permissions.
# If the folder doesn't exist or isn't accessible, the create() call will fail with a clear error.
file_metadata = {"name": file_path, "parents": [self.folder_id]}
media = MediaFileUpload(temp_file_path, resumable=True)

uploaded_file = drive_service.files().create(body=file_metadata, media_body=media, fields="id").execute()
try:
uploaded_file = (
drive_service.files().create(body=file_metadata, media_body=media, fields="id").execute()
)
except Exception as e:
msg = (
f"Unable to upload file to Google Drive folder '{self.folder_id}'. "
f"Error: {e!s}. "
"Please ensure: 1) The folder ID is correct, 2) The folder exists, "
"3) The service account has been granted access to this folder."
)
raise ValueError(msg) from e

file_id = uploaded_file.get("id")
file_url = f"https://drive.google.com/file/d/{file_id}/view"
Expand Down