fix(backend): Remove _credentials_id key on fork instead of setting to None

Setting _credentials_id to None on fork was ambiguous — both "forked,
needs re-auth" and "chained data from upstream" were represented as None.
This caused _acquire_auto_credentials to silently skip credential
acquisition for forked agents, leading to confusing TypeErrors at runtime.

Now the key is deleted entirely, making the three states unambiguous:
- Present with value: user-selected credentials
- Present as None: chained data from upstream block
- Absent: forked/needs re-authentication

Also adds pre-run validation for the missing key case and makes error
messages provider-agnostic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-02-06 17:34:16 -06:00
parent c5abc01f25
commit f4f81bc4fc
3 changed files with 17 additions and 7 deletions

View File

@@ -683,7 +683,7 @@ class GraphModel(Graph, GraphMeta):
continue
for key, value in node.input_default.items():
if isinstance(value, dict) and "_credentials_id" in value:
value["_credentials_id"] = None
del value["_credentials_id"]
def validate_graph(
self,

View File

@@ -560,8 +560,9 @@ def test_reassign_ids_clears_credentials_id():
GraphModel._reassign_ids(graph, user_id="new-user", graph_id_map={})
# _credentials_id should be cleared
assert graph.nodes[0].input_default["spreadsheet"]["_credentials_id"] is None
# _credentials_id key should be removed (not set to None) so that
# _acquire_auto_credentials correctly errors instead of treating it as chained data
assert "_credentials_id" not in graph.nodes[0].input_default["spreadsheet"]
def test_reassign_ids_preserves_non_credential_fields():
@@ -668,8 +669,8 @@ def test_reassign_ids_handles_multiple_credential_fields():
GraphModel._reassign_ids(graph, user_id="new-user", graph_id_map={})
assert graph.nodes[0].input_default["spreadsheet"]["_credentials_id"] is None
assert graph.nodes[0].input_default["doc_file"]["_credentials_id"] is None
assert "_credentials_id" not in graph.nodes[0].input_default["spreadsheet"]
assert "_credentials_id" not in graph.nodes[0].input_default["doc_file"]
assert graph.nodes[0].input_default["plain_input"] == "not a dict"

View File

@@ -353,6 +353,15 @@ async def _validate_node_input_credentials(
)
if field_value and isinstance(field_value, dict):
if "_credentials_id" not in field_value:
# Key removed (e.g., on fork) — needs re-auth
has_missing_credentials = True
credential_errors[node.id][field_name] = (
"Authentication missing for the selected file. "
"Please re-select the file to authenticate with "
"your own account."
)
continue
cred_id = field_value.get("_credentials_id")
if cred_id and isinstance(cred_id, str):
try:
@@ -367,9 +376,9 @@ async def _validate_node_input_credentials(
if not creds:
has_missing_credentials = True
credential_errors[node.id][field_name] = (
"The saved Google credentials are not available "
"The saved credentials are not available "
"for your account. Please re-select the file to "
"authenticate with your own Google account."
"authenticate with your own account."
)
# If node has optional credentials and any are missing, mark for skipping