Skip to content

fix(server): require control data on create#139

Open
lan17 wants to merge 4 commits intomainfrom
fix/issue-138-atomic-control-create
Open

fix(server): require control data on create#139
lan17 wants to merge 4 commits intomainfrom
fix/issue-138-atomic-control-create

Conversation

@lan17
Copy link
Contributor

@lan17 lan17 commented Mar 20, 2026

Summary

  • require data on PUT /api/v1/controls instead of allowing name-only shell control creation
  • validate the control definition before insert so invalid create requests return 422 and persist nothing
  • update Python SDK, TypeScript SDK, UI API types, tests, and shipped examples to use the single-request atomic create flow
  • keep legacy empty-control behavior only for existing rows/read paths covered by targeted compatibility tests

Bug Explanation

For a create-with-definition flow, the client did:

  1. PUT /api/v1/controls with just name
  2. the server inserted an empty shell control
  3. PUT /api/v1/controls/{id}/data with the definition
  4. if step 3 returned 422, the empty shell from step 2 was still in the DB

That is the concrete bug this PR fixes. Invalid create requests were not atomic, so callers could end up with persisted empty {} controls even though the overall create-with-definition flow had failed.

One nuance: the agent association part was caller-flow dependent. The root server-side invariant failure was the persisted shell control after invalid data, and that is what this change removes.

Testing

  • cd server && uv run pytest tests/test_controls.py tests/test_controls_additional.py tests/test_controls_validation.py tests/test_agents_additional.py tests/test_auth.py tests/test_control_compatibility.py tests/test_error_handling.py tests/test_evaluation_e2e.py tests/test_evaluation_error_handling.py tests/test_init_agent.py tests/test_init_agent_conflict_mode.py tests/test_new_features.py tests/test_policies.py tests/test_policy_integration.py -q
  • make lint
  • make typecheck
  • make sdk-ts-build
  • make ui-typecheck (still fails on pre-existing unrelated UI dependency/type issues)

Closes #138

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
sdks/python/src/agent_control/controls.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@lan17 lan17 changed the title fix(server): create controls atomically when data is provided fix(server): require control data on create Mar 20, 2026
Comment on lines -3338 to -3341
/** Context */
ctx?: Record<string, never>;
/** Input */
input?: unknown;
Copy link
Collaborator

Choose a reason for hiding this comment

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

The server OpenAPI spec still exposes optional ctx and input fields on ValidationError, but this regenerated type drops both. This makes the client contract narrower than the real 422 payloads. If any UI code accesses error.ctx or error.input, it would break at runtime without a type error. Consider regenerating from the current OpenAPI output or restoring these optional fields.

control_exists = resp.status_code == 409
if control_exists:
# Control exists, get its ID
resp = await client.get("/api/v1/controls", params={"name": control_name})
Copy link
Collaborator

Choose a reason for hiding this comment

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

This GET endpoint is a partial, case-insensitive filter, but the code blindly uses controls[0]. If multiple controls match the substring, this could update/attach the wrong control. Consider filtering the returned list to an exact name == control_name match.

)

# Then: a validation or not-found error is returned
# This will fail because the agent doesn't exist yet
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is inaccurate - the agent is created above (line 28-34), so it does exist. The missing piece is the evaluator, not the agent. Since the agent exists, the response should deterministically be 422 EVALUATOR_NOT_FOUND. Allowing 404 here would mask an AGENT_NOT_FOUND regression. Consider asserting specifically on 422.

Comment on lines 41 to 42
payload = VALID_CONTROL_PAYLOAD.copy()
payload["description"] = f"Name: {control_name}, Data: {data}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

The data parameter is only embedded in the description string - the actual control definition always uses VALID_CONTROL_PAYLOAD. Callers passing different data values aren't actually varying the control definition. Consider using data or VALID_CONTROL_PAYLOAD as the payload to strengthen coverage of the new atomic create flow.

Copy link
Collaborator

@abhinav-galileo abhinav-galileo left a comment

Choose a reason for hiding this comment

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

Approving - but let's address this:
#139 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(server): when creating invalid control, an empty control is created in DB

2 participants