Skip to content

Harden behavior capabilities and provider selection fields#424

Merged
mocha06 merged 2 commits into
devfrom
rc-dev/feat/behavior-caps-providers
Jul 17, 2026
Merged

Harden behavior capabilities and provider selection fields#424
mocha06 merged 2 commits into
devfrom
rc-dev/feat/behavior-caps-providers

Conversation

@mocha06

@mocha06 mocha06 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Motivation

capabilitiesAttributes was described three contradictory ways (tool docstrings said a list of strings, a unit test blessed {"type": ...}, the API only accepts {capabilityType, enabled}), and the toolkit passed every shape through — wrong payloads surfaced as opaque server errors or were silently ignored. Behavior LLM provider fields were not modeled, and the GET/update selections didn't request capabilities or provider IDs, so a GET → update round-trip silently discarded them.

Outcome

  • capabilitiesAttributes entries must be exactly {"capabilityType": "<type>", "enabled": true|false}; legacy shapes (string lists, {"type": ...}) and unknown keys are rejected with actionable errors at the SDK, MCP, and CLI boundaries — declared on the typed models from 0. Prep: parse behavior actionParams into typed models #416, no dict-walking.
  • capabilityType is deliberately not checked against a fixed set: any value passes through and the API validates the enum on write, so new capabilities work without a toolkit update. Docs name the common types with product aliases (IDP → advanced_ocr, Calculations & Analysis → math_operations) and state that shape validation is not plan entitlement — capabilities may require organization-level enablement.
  • Behaviors accept optional providerId / systemProviderId with at most one set (reads resolve a single active provider, so co-presence is unverifiable); blank-when-set provider IDs are rejected before they can dodge that check and reach the wire.
  • GET and update selections round-trip capabilitiesAttributes { capabilityType enabled }, providerId, systemProviderId.
  • Provider IDs are discoverable via the organization's AI settings in the Pipefy UI.

Testing

uv run pytest -m "not integration" + ruff green. Exercised end-to-end against a dev org (pipe with the MCP server running this branch's code):

Scenario Result
Blank providerId alongside systemProviderId Rejected — "must be a non-empty string when set"
Unknown capability key (foo) Rejected — "unknown key(s): foo; the API accepts exactly capabilityType and enabled"
Legacy {"type": "advanced_ocr"} Rejected — actionable capabilityType message
Legacy capability string list (["advanced_ocr", ...]) Rejected — "must be an object, not str … canonical shape {"capabilityType": …, "enabled": …}"
Both providerId and systemProviderId Rejected — "at most one of providerId / systemProviderId"
Multi-capability create (advanced_ocr: true, web_search: false) → GET Both round-trip, enabled: false faithful
GET → edit → update (flip web_search: true, null provider fields present) → GET Update accepted, flip persisted
Unknown capabilityType (future_capability) Client passes (enum-soft); API rejects on write with the authoritative enum list
Canonical single capability create → GET capabilitiesAttributes + providerId/systemProviderId round-trip
image

Stacked on #416 (typed behavior params). Closes #410.

@mocha06 mocha06 self-assigned this Jul 17, 2026
Enforce the canonical capabilitiesAttributes shape ({capabilityType,
enabled}, no extra keys) and reject legacy string-list / {type: ...} shapes
at the SDK, MCP, and CLI boundaries via the typed BehaviorInput model.
capabilityType is not checked against a fixed set: any value passes through
and the API validates the enum on write, so new capabilities work without a
toolkit update.

Accept optional providerId / systemProviderId with at most one set per
behavior (reads resolve a single active provider), rejecting blank-when-set
values before they can reach the wire.

Extend the GET and update GraphQL selections so capabilitiesAttributes,
providerId, and systemProviderId round-trip.

Closes #410.
@mocha06
mocha06 force-pushed the rc-dev/feat/behavior-caps-providers branch from e04f815 to 6a7321f Compare July 17, 2026 04:53
adriannoes
adriannoes previously approved these changes Jul 17, 2026

@adriannoes adriannoes left a comment

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.

Summary

Reviewed with a local checkout of the PR head (checkout+tests): targeted SDK/MCP/CLI suites passed (229) and ruff was clean.

This lands the Phase 1 harden from #410 on top of the typed behavior models: canonical capabilitiesAttributes, provider exclusivity, and GET/update selections that round-trip capabilities and provider IDs. Ready to approve.

What worked well

  • Validation sits on typed BehaviorInput rather than dict-walking, with clear errors for {type: ...}, unknown capability keys, and dual provider IDs. Enum-soft capabilityType plus the skill/docs note that shape checks are not entitlement match the ticket.
  • GET and update GraphQL selections were extended together, with a service AST assert covering both documents.

Also noted

  • capabilitiesAttributes: ["advanced_ocr"] is correctly rejected, but the failure is still a generic Pydantic model_type coercion error, so callers never see the canonical-shape guidance that _validate_capability_entries already writes for {type: ...}. A small boundary parse (for example a BeforeValidator on the list) plus match= on test_behavior_input_rejects_capability_string_list would align that legacy path with the PR claim of actionable errors.

Base automatically changed from rc-dev/feat/typed-behavior-params to dev July 17, 2026 14:21
@mocha06
mocha06 dismissed adriannoes’s stale review July 17, 2026 14:21

The base branch was changed.

A legacy capabilitiesAttributes string list failed AiBehaviorCapabilityAttributes coercion with an opaque Pydantic model_type error before _validate_capability_entries ran, so callers never saw the canonical-shape guidance the other legacy shapes get.

Add a BeforeValidator that surfaces the actionable message for non-object entries; object entries (including {"type": ...}) still pass through to the after-validator unchanged. Tighten the string-list test with match=.
@mocha06

mocha06 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Summary

Reviewed with a local checkout of the PR head (checkout+tests): targeted SDK/MCP/CLI suites passed (229) and ruff was clean.

This lands the Phase 1 harden from #410 on top of the typed behavior models: canonical capabilitiesAttributes, provider exclusivity, and GET/update selections that round-trip capabilities and provider IDs. Ready to approve.

What worked well

  • Validation sits on typed BehaviorInput rather than dict-walking, with clear errors for {type: ...}, unknown capability keys, and dual provider IDs. Enum-soft capabilityType plus the skill/docs note that shape checks are not entitlement match the ticket.
  • GET and update GraphQL selections were extended together, with a service AST assert covering both documents.

Also noted

  • capabilitiesAttributes: ["advanced_ocr"] is correctly rejected, but the failure is still a generic Pydantic model_type coercion error, so callers never see the canonical-shape guidance that _validate_capability_entries already writes for {type: ...}. A small boundary parse (for example a BeforeValidator on the list) plus match= on test_behavior_input_rejects_capability_string_list would align that legacy path with the PR claim of actionable errors.

Good catch — fixed in 47173d8.

You were right: the string-list path failed `AiBehaviorCapabilityAttributes` coercion with the opaque `model_type` error before `_validate_capability_entries` could run, so it never got the canonical-shape guidance the `{type: ...}` path does. Went with the `BeforeValidator` you suggested — `_reject_non_mapping_capability_entries` intercepts non-object entries and raises the same actionable message; object entries (including legacy `{type: ...}`) pass through untouched to the after-validator, so nothing else changes. Tightened `test_behavior_input_rejects_capability_string_list` with `match="must be an object, not str"` to lock it in.

Now:

capabilitiesAttributes[0] must be an object, not str. Use the canonical shape {"capabilityType": "<type>", "enabled": true|false}; legacy shapes (string lists, {"type": ...}) are not accepted.

uv run pytest -m "not integration" + ruff green.

@mocha06
mocha06 requested a review from adriannoes July 17, 2026 17:09

@adriannoes adriannoes left a comment

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.

Summary

Re-checked after 47173d86. Local checkout+tests still green (229 on the targeted agent suites); CI lint/test/skills are green on this head.

The follow-up BeforeValidator on capabilitiesAttributes closes the string-list message gap from the earlier review: non-object entries now get the canonical-shape error, and test_behavior_input_rejects_capability_string_list locks it with match=. No remaining findings on my side.

What worked well

Tight, review-driven fix that preserves the {type: ...} path through the existing after-validator and only intercepts non-mapping entries. Approve.

@mocha06
mocha06 merged commit ba08ee9 into dev Jul 17, 2026
3 checks passed
@adriannoes
adriannoes deleted the rc-dev/feat/behavior-caps-providers branch July 18, 2026 03:12
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.

2 participants