Skip to content

Commit 1925e77

Browse files
authored
feat(backend): Include default input values in graph export (#11173)
Since #10323, one-time graph inputs are no longer stored on the input nodes (#9139), so we can reasonably assume that the default value set by the graph creator will be safe to export. ### Changes 🏗️ - Don't strip the default value from input nodes in `NodeModel.stripped_for_export(..)`, except for inputs marked as `secret` - Update and expand tests for graph export secrets stripping mechanism ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Expanded tests pass - Relatively simple change with good test coverage, no manual test needed
1 parent 9bc9b53 commit 1925e77

File tree

2 files changed

+89
-14
lines changed

2 files changed

+89
-14
lines changed

autogpt_platform/backend/backend/data/graph.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,20 @@ def stripped_for_export(self) -> "NodeModel":
129129
Returns a copy of the node model, stripped of any non-transferable properties
130130
"""
131131
stripped_node = self.model_copy(deep=True)
132-
# Remove credentials from node input
132+
133+
# Remove credentials and other (possible) secrets from node input
133134
if stripped_node.input_default:
134135
stripped_node.input_default = NodeModel._filter_secrets_from_node_input(
135136
stripped_node.input_default, self.block.input_schema.jsonschema()
136137
)
137138

139+
# Remove default secret value from secret input nodes
138140
if (
139141
stripped_node.block.block_type == BlockType.INPUT
142+
and stripped_node.input_default.get("secret", False) is True
140143
and "value" in stripped_node.input_default
141144
):
142-
stripped_node.input_default["value"] = ""
145+
del stripped_node.input_default["value"]
143146

144147
# Remove webhook info
145148
stripped_node.webhook_id = None
@@ -156,8 +159,10 @@ def _filter_secrets_from_node_input(
156159
result = {}
157160
for key, value in input_data.items():
158161
field_schema: dict | None = field_schemas.get(key)
159-
if (field_schema and field_schema.get("secret", False)) or any(
160-
sensitive_key in key.lower() for sensitive_key in sensitive_keys
162+
if (field_schema and field_schema.get("secret", False)) or (
163+
any(sensitive_key in key.lower() for sensitive_key in sensitive_keys)
164+
# Prevent removing `secret` flag on input nodes
165+
and type(value) is not bool
161166
):
162167
# This is a secret value -> filter this key-value pair out
163168
continue

autogpt_platform/backend/backend/data/graph_test.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -201,25 +201,56 @@ class ExpectedOutputSchema(BlockSchema):
201201
@pytest.mark.asyncio(loop_scope="session")
202202
async def test_clean_graph(server: SpinTestServer):
203203
"""
204-
Test the clean_graph function that:
205-
1. Clears input block values
206-
2. Removes credentials from nodes
204+
Test the stripped_for_export function that:
205+
1. Removes sensitive/secret fields from node inputs
206+
2. Removes webhook information
207+
3. Preserves non-sensitive data including input block values
207208
"""
208-
# Create a graph with input blocks and credentials
209+
# Create a graph with input blocks containing both sensitive and normal data
209210
graph = Graph(
210211
id="test_clean_graph",
211212
name="Test Clean Graph",
212213
description="Test graph cleaning",
213214
nodes=[
214215
Node(
215-
id="input_node",
216216
block_id=AgentInputBlock().id,
217217
input_default={
218+
"_test_id": "input_node",
218219
"name": "test_input",
219-
"value": "test value",
220+
"value": "test value", # This should be preserved
220221
"description": "Test input description",
221222
},
222223
),
224+
Node(
225+
block_id=AgentInputBlock().id,
226+
input_default={
227+
"_test_id": "input_node_secret",
228+
"name": "secret_input",
229+
"value": "another value",
230+
"secret": True, # This makes the input secret
231+
},
232+
),
233+
Node(
234+
block_id=StoreValueBlock().id,
235+
input_default={
236+
"_test_id": "node_with_secrets",
237+
"input": "normal_value",
238+
"control_test_input": "should be preserved",
239+
"api_key": "secret_api_key_123", # Should be filtered
240+
"password": "secret_password_456", # Should be filtered
241+
"token": "secret_token_789", # Should be filtered
242+
"credentials": { # Should be filtered
243+
"id": "fake-github-credentials-id",
244+
"provider": "github",
245+
"type": "api_key",
246+
},
247+
"anthropic_credentials": { # Should be filtered
248+
"id": "fake-anthropic-credentials-id",
249+
"provider": "anthropic",
250+
"type": "api_key",
251+
},
252+
},
253+
),
223254
],
224255
links=[],
225256
)
@@ -231,15 +262,54 @@ async def test_clean_graph(server: SpinTestServer):
231262
)
232263

233264
# Clean the graph
234-
created_graph = await server.agent_server.test_get_graph(
265+
cleaned_graph = await server.agent_server.test_get_graph(
235266
created_graph.id, created_graph.version, DEFAULT_USER_ID, for_export=True
236267
)
237268

238-
# # Verify input block value is cleared
269+
# Verify sensitive fields are removed but normal fields are preserved
239270
input_node = next(
240-
n for n in created_graph.nodes if n.block_id == AgentInputBlock().id
271+
n for n in cleaned_graph.nodes if n.input_default["_test_id"] == "input_node"
272+
)
273+
274+
# Non-sensitive fields should be preserved
275+
assert input_node.input_default["name"] == "test_input"
276+
assert input_node.input_default["value"] == "test value" # Should be preserved now
277+
assert input_node.input_default["description"] == "Test input description"
278+
279+
# Sensitive fields should be filtered out
280+
assert "api_key" not in input_node.input_default
281+
assert "password" not in input_node.input_default
282+
283+
# Verify secret input node preserves non-sensitive fields but removes secret value
284+
secret_node = next(
285+
n
286+
for n in cleaned_graph.nodes
287+
if n.input_default["_test_id"] == "input_node_secret"
288+
)
289+
assert secret_node.input_default["name"] == "secret_input"
290+
assert "value" not in secret_node.input_default # Secret default should be removed
291+
assert secret_node.input_default["secret"] is True
292+
293+
# Verify sensitive fields are filtered from nodes with secrets
294+
secrets_node = next(
295+
n
296+
for n in cleaned_graph.nodes
297+
if n.input_default["_test_id"] == "node_with_secrets"
241298
)
242-
assert input_node.input_default["value"] == ""
299+
# Normal fields should be preserved
300+
assert secrets_node.input_default["input"] == "normal_value"
301+
assert secrets_node.input_default["control_test_input"] == "should be preserved"
302+
# Sensitive fields should be filtered out
303+
assert "api_key" not in secrets_node.input_default
304+
assert "password" not in secrets_node.input_default
305+
assert "token" not in secrets_node.input_default
306+
assert "credentials" not in secrets_node.input_default
307+
assert "anthropic_credentials" not in secrets_node.input_default
308+
309+
# Verify webhook info is removed (if any nodes had it)
310+
for node in cleaned_graph.nodes:
311+
assert node.webhook_id is None
312+
assert node.webhook is None
243313

244314

245315
@pytest.mark.asyncio(loop_scope="session")

0 commit comments

Comments
 (0)