Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 68 additions & 0 deletions nodes/src/nodes/core/gcp_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import base64
import json
from typing import Optional, Tuple, Any

class GCPAuthError(Exception):
"""Raised when GCP authentication fails or config is invalid."""
pass

def get_gcp_credentials(config: dict, scopes: Optional[list[str]] = None) -> Tuple[Any, Optional[str]]:
"""
Resolves GCP credentials from the given node configuration.

Returns:
tuple[google.auth.credentials.Credentials, str]: The credentials and the resolved project ID.

Raises:
GCPAuthError: If configuration is invalid or missing.
"""
try:
import google.auth
from google.oauth2 import service_account
except ImportError:
raise GCPAuthError("google-auth library is not installed. Ensure node requirements include 'google-auth'.")

auth_type = config.get("gcp.authType", "adc")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: engine delivers node config with the dotted namespace stripped — keys arrive as authType, projectId, serviceAccountKey (see tool_gmail/IGlobal.py:64 reading cfg.get('authType') for field google.authType, and the comment in db_postgres/IGlobal.py:41). config.get("gcp.authType") never matches → always falls back to adc; the service_account branch is dead code and gcp.projectId is ignored. Fix: read authType / serviceAccountKey / projectId.

project_id = config.get("gcp.projectId")

if auth_type == "service_account":
key_data = config.get("gcp.serviceAccountKey")
Comment on lines +25 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use single quotes for string literals.

Double quotes are used for several string literals across the Python files. As per path instructions, use single quotes for string literals in nodes/**/*.py unless the string itself contains a single quote.

  • nodes/src/nodes/core/gcp_auth.py#L25-L29: replace double quotes with single quotes for gcp.authType, adc, gcp.projectId, service_account, gcp.serviceAccountKey.
  • nodes/src/nodes/core/gcp_auth.py#L35-L36: replace double quotes with single quotes for the , character.
  • nodes/src/nodes/core/gcp_auth.py#L42-L43: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/core/gcp_auth.py#L49-L53: replace double quotes with single quotes for the f-string and project_id.
  • nodes/src/nodes/core/gcp_auth.py#L57-L57: replace double quotes with single quotes for adc.
  • nodes/src/nodes/core/gcp_auth.py#L64-L68: replace double quotes with single quotes for the f-strings.
  • nodes/src/nodes/tool_gcs/IGlobal.py#L23-L24: replace double quotes with single quotes for the ImportError message.
  • nodes/src/nodes/tool_gcs/IGlobal.py#L34-L36: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/tool_gcs/IGlobal.py#L50-L52: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/tool_gcs/IGlobal.py#L60-L61: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L24-L25: replace double quotes with single quotes for the ImportError message.
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L33-L34: replace double quotes with single quotes for the warning string.
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L39-L41: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L56-L58: replace double quotes with single quotes for the f-string.
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L68-L69: replace double quotes with single quotes for the f-string.
📍 Affects 3 files
  • nodes/src/nodes/core/gcp_auth.py#L25-L29 (this comment)
  • nodes/src/nodes/core/gcp_auth.py#L35-L36
  • nodes/src/nodes/core/gcp_auth.py#L42-L43
  • nodes/src/nodes/core/gcp_auth.py#L49-L53
  • nodes/src/nodes/core/gcp_auth.py#L57-L57
  • nodes/src/nodes/core/gcp_auth.py#L64-L68
  • nodes/src/nodes/tool_gcs/IGlobal.py#L23-L24
  • nodes/src/nodes/tool_gcs/IGlobal.py#L34-L36
  • nodes/src/nodes/tool_gcs/IGlobal.py#L50-L52
  • nodes/src/nodes/tool_gcs/IGlobal.py#L60-L61
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L24-L25
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L33-L34
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L39-L41
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L56-L58
  • nodes/src/nodes/vectordb_vertex/IGlobal.py#L68-L69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nodes/src/nodes/core/gcp_auth.py` around lines 25 - 29, Use single-quoted
string literals throughout the affected Python code unless a literal contains a
single quote: in nodes/src/nodes/core/gcp_auth.py ranges 25-29, 35-36, 42-43,
49-53, 57-57, and 64-68, update the config keys, auth values, comma, and
f-strings; in nodes/src/nodes/tool_gcs/IGlobal.py ranges 23-24, 34-36, 50-52,
and 60-61, update the ImportError message and f-strings; in
nodes/src/nodes/vectordb_vertex/IGlobal.py ranges 24-25, 33-34, 39-41, 56-58,
and 68-69, update the ImportError message, warning string, and f-strings.
Preserve interpolation and runtime behavior.

Source: Path instructions

if not key_data:
raise GCPAuthError("Service Account JSON key is required when authType is 'service_account', but missing.")

# RocketRide UI usually uploads files as base64 data-url: data:application/json;base64,...
try:
if "," in key_data:
_, b64_data = key_data.split(",", 1)
json_bytes = base64.b64decode(b64_data)
key_info = json.loads(json_bytes.decode('utf-8'))
else:
# Fallback if raw JSON string is passed directly
key_info = json.loads(key_data)
except Exception as e:
raise GCPAuthError(f"Failed to parse Service Account JSON key: {e}")

try:
creds = service_account.Credentials.from_service_account_info(key_info)
if scopes:
creds = creds.with_scopes(scopes)
except Exception as e:
raise GCPAuthError(f"Invalid Service Account format: {e}")

if not project_id:
project_id = key_info.get("project_id")

return creds, project_id

elif auth_type == "adc":
# Application Default Credentials
try:
creds, default_project_id = google.auth.default(scopes=scopes)
if not project_id:
project_id = default_project_id
return creds, project_id
except Exception as e:
raise GCPAuthError(f"Failed to acquire Application Default Credentials: {e}")

else:
raise GCPAuthError(f"Unknown gcp.authType: {auth_type}")
32 changes: 32 additions & 0 deletions nodes/src/nodes/core/services.common.gcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"fields": {
"gcp.authType": {
"type": "string",
"title": "Authentication Type",
"default": "adc",
"enum": ["adc", "service_account"],
"enumNames": ["Application Default Credentials", "Service Account JSON"],
"description": "Choose how to authenticate to Google Cloud Platform.",
"conditional": [
{
"value": "service_account",
"properties": ["gcp.serviceAccountKey"]
}
]
},
"gcp.serviceAccountKey": {
"type": "string",
"format": "data-url",
"title": "Service Account Key JSON",
"description": "Upload the JSON key file for your Google Cloud service account.",
"ui:options:accept": ".json",
"secure": true
},
"gcp.projectId": {
"type": "string",
"title": "Project ID",
"description": "Optional: Specify the Google Cloud Project ID explicitly. Leave blank to infer from credentials.",
"optional": true
}
}
}
67 changes: 67 additions & 0 deletions nodes/src/nodes/db_firestore/IGlobal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations

from ai.common.config import Config
from rocketlib import IGlobalBase, OPEN_MODE, debug, warning
from nodes.core.gcp_auth import get_gcp_credentials, GCPAuthError

try:
from google.cloud import firestore
except ImportError:
firestore = None

class IGlobal(IGlobalBase):
"""Global state for Firestore node."""

client: firestore.Client | None = None
database: str = '(default)'
collection: str = ''

def beginGlobal(self) -> None:
if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG:
return

if firestore is None:
raise ImportError("google-cloud-firestore is not installed.")

cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)

self.database = str((cfg.get('database') or '(default)')).strip() or '(default)'
self.collection = str((cfg.get('collection') or '')).strip()

# Auth
try:
creds, project_id = get_gcp_credentials(cfg)
except Exception as e:
warning(f"Firestore authentication failed: {e}")
raise

self.client = firestore.Client(
project=project_id,
credentials=creds,
database=self.database
)

# Fail fast connection check
try:
# simple check to verify connectivity/auth
next(self.client.collections(page_size=1), None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: Client.collections() has no page_size parameter (google-cloud-firestore 2.28.0 signature: collections(retry, timeout, *, read_time)) → TypeError on every beginGlobal; the node can never start. Use next(self.client.collections(), None). Also note: listing collections needs broader IAM than document get/set — a narrowly-scoped SA will fail this check even though the actual tools would work. Consider a cheaper probe (e.g. a no-op doc get).

debug(f'db_firestore: connected to project {self.client.project}, database={self.database}')
except Exception as e:
warning(f"Firestore connection check failed: {e}")
raise

def validateConfig(self) -> None:
try:
cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)
get_gcp_credentials(cfg) # Validates credentials parse successfully
if not str(cfg.get('collection') or '').strip():
warning('collection is recommended')
except GCPAuthError as e:
warning(f"Auth configuration error: {e}")
except Exception as e:
warning(str(e))

def endGlobal(self) -> None:
if self.client is not None:
self.client.close()
self.client = None
58 changes: 58 additions & 0 deletions nodes/src/nodes/db_firestore/IInstance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from ai.common.tool import tool_function
from rocketlib import IInstanceBase
from typing import Dict, Any

class IInstance(IInstanceBase):
"""Firestore instance, providing tool functions for reading and writing documents."""

@tool_function(
description="Insert or update a document in a Firestore collection. If the document exists, it merges the data.",
args={
"collection": "The collection name.",
"document_id": "The ID of the document to set. If empty, a new ID will be generated.",
"data": "The JSON dictionary to store in the document."
}
)
def set_document(self, collection: str, document_id: str, data: Dict[str, Any]) -> str:
client = self.glb.client
if not client:
return "Error: Firestore client is not connected."

if not collection:
collection = self.glb.collection

try:
coll_ref = client.collection(collection)
if document_id:
doc_ref = coll_ref.document(document_id)
doc_ref.set(data, merge=True)
return f"Successfully set document {document_id} in {collection}."
else:
_, doc_ref = coll_ref.add(data)
return f"Successfully added document with ID {doc_ref.id} to {collection}."
except Exception as e:
return f"Failed to set document: {e}"

@tool_function(
description="Get a single document from a Firestore collection.",
args={
"collection": "The collection name.",
"document_id": "The document ID."
}
)
def get_document(self, collection: str, document_id: str) -> Dict[str, Any]:
client = self.glb.client
if not client:
return {"error": "Firestore client is not connected."}

if not collection:
collection = self.glb.collection

try:
doc = client.collection(collection).document(document_id).get()
if doc.exists:
return doc.to_dict()
else:
return {"error": f"Document {document_id} not found."}
except Exception as e:
return {"error": f"Failed to get document: {e}"}
16 changes: 16 additions & 0 deletions nodes/src/nodes/db_firestore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Firestore Node

A standard node for interacting with Google Cloud Firestore (Datastore mode or Native mode).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: google-cloud-firestore only works against Native-mode databases; Datastore mode requires google-cloud-datastore. Drop the "Datastore mode" claim here and in the services.json description.


## Authentication
This node uses the shared RocketRide Google Cloud authentication logic. You can authenticate by either:
1. Uploading a **Service Account JSON Key**.
2. Leaving the key blank and relying on **Application Default Credentials (ADC)** if your RocketRide server is hosted on Google Cloud Run, GKE, or a Compute Engine VM with attached service accounts.

## Usage
When placed in a pipeline as a tool node, the LLM can:
- **`get_document`**: Fetch an existing document by its collection and document ID.
- **`set_document`**: Create or update (merge) a document with JSON data.

<!-- ROCKETRIDE:GENERATED:PARAMS START -->
<!-- ROCKETRIDE:GENERATED:PARAMS END -->
10 changes: 10 additions & 0 deletions nodes/src/nodes/db_firestore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import os
from depends import depends # type: ignore

requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt'
depends(requirements)

from .IGlobal import IGlobal # noqa: E402
from .IInstance import IInstance # noqa: E402

__all__ = ['IGlobal', 'IInstance']
2 changes: 2 additions & 0 deletions nodes/src/nodes/db_firestore/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
google-auth>=2.23.3
google-cloud-firestore>=2.13.0
74 changes: 74 additions & 0 deletions nodes/src/nodes/db_firestore/services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"title": "Firestore",
"protocol": "db_firestore://",
"classType": ["database", "tool"],
"capabilities": ["noremote", "invoke"],
"register": "filter",
"node": "python",
"path": "nodes.db_firestore",
"prefix": "firestore",
"description": ["A processing component that connects to Google Cloud Firestore (Datastore Mode or Native)."],
"icon": "db_firestore.svg",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: db_firestore.svg referenced but not added (same for tool_gcs.svg, vertex.svg). Icon SVG goes next to services.json — docs/README-nodes.md "Adding a New Node" step 4.

"documentation": "https://docs.rocketride.org",
"invoke": {
"llm": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: invoke.llm ("craft NoSQL queries") declared but IInstance never uses an LLM. The db_* nodes get question→query machinery from DatabaseGlobalBase/DatabaseInstanceBase (see db_postgres); this node extends raw IGlobalBase. Same for lanes.questions: ["table", "text", "answers"] below — no writeQuestions handler exists. Either derive from the database base classes or make this a pure tool node: "invoke": {}, "lanes": {} (cf. tool_tavily, tool_slack).

"description": "LLM to use to craft NoSQL queries",
"min": 1,
"optional": true
}
},
"lanes": {
"answers": [],
"questions": ["table", "text", "answers"]
},
"preconfig": {
"default": "default",
"profiles": {
"default": {
"database": "(default)"
}
}
},
"fields": {
"firestore.database": {
"type": "string",
"title": "Database ID",
"default": "(default)",
"description": "The Firestore database ID. Usually '(default)'."
},
"firestore.collection": {
"type": "string",
"title": "Collection",
"description": "The Firestore collection to query or insert into."
},
"firestore.default": {
"object": "default",
"properties": [
"gcp.authType",
"gcp.serviceAccountKey",
"gcp.projectId",
"firestore.database",
"firestore.collection"
]
},
"firestore.profile": {
"hidden": true,
"type": "string",
"default": "default",
"enum": [["default", "Default"]],
"conditional": [
{
"value": "default",
"properties": ["firestore.default"]
}
]
}
},
"shape": [
{
"section": "Pipe",
"title": "Firestore",
"properties": ["firestore.profile"]
}
]
}
Loading
Loading