Conflict API
Table of Contents
upsquad/conflict/v1/conflict.proto
AcknowledgeConflictRequest
AcknowledgeConflictRequest moves status to 'acknowledged'.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict_id | string | conflict_id is the UUID primary key. |
AcknowledgeConflictResponse
AcknowledgeConflictResponse returns the updated conflict.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict | Conflict | conflict is the updated row state. |
Conflict
Conflict is a single policy_conflicts row with its associated notification delivery ledger.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict_id | string | conflict_id is the UUID primary key of the policy_conflicts row. | |
| org_id | string | org_id scopes the conflict to a single tenant. | |
| action_type | string | action_type is the governance action_type that produced the conflict (e.g. 'tool_call', 'llm_request'). | |
| target | string | target is the fine-grained target within action_type ('*' for wildcard, e.g. specific tool slug). | |
| member_id | string | member_id is the member the cascade was evaluated against. Empty string when the conflict is org-wide (no member context). | |
| winning_policy | ConflictLayer | winning_policy describes the layer + policy id that was returned to the caller (sticky-deny + tightest-first). | |
| losing_policy | ConflictLayer | losing_policy is the layer + policy id whose verdict was suppressed. losing_policy.policy_id is always populated; losing_policy.layer may be empty when the losing verdict was the platform default. | |
| clearance_at_decision | int32 | clearance_at_decision captures the numeric clearance (L1-L5) that applied when the decision was taken. | |
| suggested_merge_json | string | suggested_merge_json is populated by the MergeSynthesisWorker once the LLM has proposed a merged policy. Empty string == pending synthesis; non-empty is a JSON object literal. | |
| notified_parties | NotifiedParty | repeated | notified_parties is the recipient set computed at conflict creation. |
| status | ConflictStatus | status is the lifecycle state. | |
| resolved_by | string | resolved_by is the member id that transitioned status to resolved or dismissed. Empty string until that transition occurs. | |
| resolved_at | google.protobuf.Timestamp | resolved_at is the timestamp of the resolve/dismiss transition. | |
| created_at | google.protobuf.Timestamp | created_at is the UTC timestamp of the conflict row insertion. | |
| deliveries | NotificationDelivery | repeated | deliveries is the per-channel delivery ledger. Populated on reads for the detail view; omitted on stream events for payload size. |
| sides | ConflictSide | repeated | sides is the side-by-side diff view of the cascade trace (GAP-26, #1104). The server-side translator derives sides[] from the existing cascade-trace structure (winning_policy + losing_policy + clearance_at_decision + suggested_merge_json metadata) so existing readers that don't consume sides[] keep working unchanged. |
For the v1 translator each conflict produces exactly two sides — winning + losing. The side_id values are stable strings ("winning", "losing") that ResolveConflict.SIDE_WITH callers pass back via ResolveConflictRequest.side_id (GAP-28, #1106). | | cached_recommendation | Recommendation | | cached_recommendation is the most-recent GetRecommendation result (GAP-27, #1105) persisted on the conflict row. Empty until the first GetRecommendation call. Surfaced here so the redesigned Conflicts page can render "Quad recommends:" without an additional round-trip after the conflict listing. |
ConflictLayer
ConflictLayer pairs a cascade layer identifier with the policy id that matched at that layer.
| Field | Type | Label | Description |
|---|---|---|---|
| layer | string | layer is one of 'platform', 'org', 'org_unit', 'member'. | |
| policy_id | string | policy_id is the UUID of the matching policy row. Empty string only for platform-default "policies" which do not have a row. |
ConflictSide
ConflictSide is a single side of the side-by-side diff view rendered
by the redesigned Conflicts page (GAP-26, #1104). The shape mirrors
the prototype (upsquad-client/docs/design/redesign-2026-04/project/ conflicts.jsx) so the JSX consumer can render with no field rename.
| Field | Type | Label | Description |
|---|---|---|---|
| side_id | string | side_id is a stable, non-localised identifier used by ResolveConflict.SIDE_WITH (#1106) to refer back to this side. The v1 translator emits the constants "winning" / "losing" mirroring the cascade outcome. | |
| source | string | source is the human-readable origin of the side (e.g. "GR-01 · Refund ceiling", "Marcus R · Finance Lead"). | |
| verdict | string | verdict is the short summary the side delivers (e.g. "Block", "Allow", "Approved", "Pause agent"). | |
| reason | string | reason is the longer-form justification rendered in the diff body. | |
| color | SideColor | color is the rendering hint for the side card. Mirrors the prototype's color enum. | |
| weight | string | weight is the rendering hint for the side's "weight" footer (e.g. "Hard rule", "Pattern match · 87% conf", "Step 3 · final"). It is a string to keep the contract free-form — the translator picks a phrasing that matches the cascade context. Empty when not known. | |
| who | Person | who is populated when the side originates from a named individual approver (vs. a policy rule). Optional — left null for hard-rule sides where the source IS the rule. |
GetConflictRequest
GetConflictRequest fetches a single row by id.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict_id | string | conflict_id is the UUID primary key. |
GetConflictResponse
GetConflictResponse wraps the fetched conflict.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict | Conflict | conflict is the fetched row with its delivery ledger. |
GetRecommendationRequest
GetRecommendationRequest fetches (or generates + caches) the platform "Quad recommends:" recommendation for a single conflict (GAP-27, #1105).
| Field | Type | Label | Description |
|---|---|---|---|
| conflict_id | string | conflict_id is the UUID primary key of the conflict. | |
| force_refresh | bool | force_refresh ignores any cached recommendation and re-runs the LLM call. Defaults to false so the typical UI flow doesn't re-bill. |
GetRecommendationResponse
GetRecommendationResponse returns the recommendation and a flag indicating whether it was served from cache.
| Field | Type | Label | Description |
|---|---|---|---|
| recommendation | Recommendation | recommendation is the verdict + target + reasoning + confidence bundle. Same shape that is also surfaced on Conflict.cached_recommendation after the first GetRecommendation call so the listing endpoint can render the recommendation card without an extra round-trip. | |
| cached | bool | cached is true when the response was served from the recommendation cache on the conflict row, false when a fresh LLM call was made. |
ListConflictsRequest
ListConflictsRequest drives the paginated admin inbox query.
| Field | Type | Label | Description |
|---|---|---|---|
| status | ConflictStatus | status filters by ConflictStatus. Zero value returns all statuses. | |
| page_size | int32 | page_size caps the number of rows returned (default 50, max 100). | |
| page_token | string | page_token is an opaque cursor returned by the previous call. | |
| member_id | string | member_id filters to conflicts involving the given member. Empty string means "any member". |
ListConflictsResponse
ListConflictsResponse is the paginated result set.
| Field | Type | Label | Description |
|---|---|---|---|
| conflicts | Conflict | repeated | conflicts are the page results ordered created_at DESC. |
| next_page_token | string | next_page_token is opaque; empty string on the final page. |
NotificationDelivery
NotificationDelivery is a single row from conflict_notifications — one per (conflict, recipient, channel) fan-out attempt.
| Field | Type | Label | Description |
|---|---|---|---|
| channel | string | channel is one of 'quad', 'bell', 'audit', 'inbox', 'email', 'webhook'. | |
| status | string | status is one of 'pending', 'delivered', 'failed', 'skipped'. | |
| delivered_at | google.protobuf.Timestamp | delivered_at is set when status transitions to 'delivered'. | |
| failed_at | google.protobuf.Timestamp | failed_at is set when status transitions to 'failed'. | |
| failure_reason | string | failure_reason is the operator-actionable error message when the channel adapter fails. | |
| kind | string | kind differentiates 'conflict.new', 'conflict.merge_ready', and 'conflict.resolved' dispatches. | |
| recipient_id | string | recipient_id is the member the delivery targeted. |
NotifiedParty
NotifiedParty is a single recipient the 6-channel notifier fanned out to.
| Field | Type | Label | Description |
|---|---|---|---|
| member_id | string | member_id is the recipient's member UUID. | |
| scope | string | scope describes why the member was notified (e.g. 'policy_author', 'supervisor', 'admin'). | |
| reason | string | reason is a free-form human-readable annotation. |
Person
Person identifies a named individual approver associated with a side. Only used by ConflictSide.who today; kept top-level so future GAPs can re-use the same wire shape.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | name is the display name (e.g. "Marcus R"). | |
| role | string | role is the person's role at decision time (e.g. "Finance Lead"). |
Recommendation
Recommendation is the cached "Quad recommends:" verdict surfaced by GetRecommendation (GAP-27, #1105) and cached on the conflict row.
Shape matches recommendation in the JSX prototype so the front-end
can render verdict + optional target ("to <role>") + reasoning
without remapping. confidence is a 0..1 float supplied by the LLM
router and surfaced for the "87% conf" badge variant.
| Field | Type | Label | Description |
|---|---|---|---|
| verdict | string | verdict is the short imperative the recommendation advocates (e.g. "Escalate", "Adopt Priya's verdict", "Request budget extension"). | |
| target | string | target is the optional addressee for the recommendation (e.g. "Finance Lead", "CFO"). Empty when the recommendation does not target a named role. | |
| reasoning | string | reasoning is the longer-form justification rendered under the verdict in the recommendation card. | |
| confidence | float | confidence is the LLM router's 0..1 confidence score for the recommendation. Zero when the underlying router does not surface a confidence signal. | |
| model_slug | string | model_slug is the resolved router model slug used to generate the recommendation (e.g. "claude-sonnet-4-6"). Surfaced so audit tooling can correlate recommendation drift with router changes. | |
| generated_at | google.protobuf.Timestamp | generated_at is the UTC timestamp the recommendation was generated. |
RecordConflictRequest
RecordConflictRequest is the internal RPC payload the governance Engine sends when cascade evaluation detects opposing layer verdicts.
| Field | Type | Label | Description |
|---|---|---|---|
| org_id | string | org_id is the tenant the conflict belongs to. | |
| action_type | string | action_type is the governance action_type (e.g. 'tool_call'). | |
| target | string | target is the fine-grained target within action_type. | |
| member_id | string | member_id is the member the cascade was evaluated against. Empty string is allowed for org-wide conflicts. | |
| winning_policy | ConflictLayer | winning_policy describes the layer + policy that won the cascade. | |
| losing_policy | ConflictLayer | losing_policy describes the layer + policy whose verdict was suppressed by sticky-deny / tightest-first precedence. | |
| clearance_at_decision | int32 | clearance_at_decision is the numeric clearance captured at decision. | |
| notified_parties | NotifiedParty | repeated | notified_parties is the optional initial recipient set — when empty the service derives recipients from the policy authors + supervisors. |
RecordConflictResponse
RecordConflictResponse returns the newly-persisted conflict row.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict | Conflict | conflict is the persisted row (with conflict_id set). |
ResolveConflictRequest
ResolveConflictRequest finalises a conflict.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict_id | string | conflict_id is the UUID primary key. | |
| action | ResolveAction | action selects the resolution pathway. | |
| note | string | note is a free-form human-readable annotation captured on the row. | |
| side_id | string | side_id MUST be set when action == RESOLVE_ACTION_SIDE_WITH and MUST match a Conflict.sides[].side_id (GAP-28, #1106). Ignored for all other actions. |
ResolveConflictResponse
ResolveConflictResponse returns the terminal conflict state.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict | Conflict | conflict is the updated row state. |
StreamConflictsRequest
StreamConflictsRequest subscribes to the lifecycle feed.
| Field | Type | Label | Description |
|---|---|---|---|
| include_backlog | bool | include_backlog replays currently-open conflicts the caller can see before streaming new events. Defaults to true for admin inboxes. |
StreamConflictsResponse
StreamConflictsResponse is a single lifecycle event on the stream.
| Field | Type | Label | Description |
|---|---|---|---|
| conflict | Conflict | conflict is the conflict row state at event time. | |
| event_kind | string | event_kind is one of 'conflict.new', 'conflict.merge_ready', 'conflict.acknowledged', 'conflict.resolved'. Matches the pg_notify channel name minus the 'conflict_' prefix conversion. |
ConflictStatus
ConflictStatus mirrors the policy_conflicts.status lifecycle enum.
| Name | Number | Description |
|---|---|---|
| CONFLICT_STATUS_UNSPECIFIED | 0 | CONFLICT_STATUS_UNSPECIFIED is the zero value reserved by protobuf. |
| CONFLICT_STATUS_OPEN | 1 | CONFLICT_STATUS_OPEN is the initial state — freshly recorded. |
| CONFLICT_STATUS_ACKNOWLEDGED | 2 | CONFLICT_STATUS_ACKNOWLEDGED indicates a human has opened the conflict. |
| CONFLICT_STATUS_RESOLVED | 3 | CONFLICT_STATUS_RESOLVED indicates the conflict was actioned (merge accepted, override granted). |
| CONFLICT_STATUS_DISMISSED | 4 | CONFLICT_STATUS_DISMISSED is the soft-delete — dismissed without action. |
ResolveAction
ResolveAction enumerates the resolution pathways ResolveConflict supports.
| Name | Number | Description |
|---|---|---|
| RESOLVE_ACTION_UNSPECIFIED | 0 | RESOLVE_ACTION_UNSPECIFIED is the zero value reserved by protobuf. |
| RESOLVE_ACTION_ACCEPT_MERGE | 1 | RESOLVE_ACTION_ACCEPT_MERGE accepts the MergeSynthesisWorker's suggested_merge_json and marks the conflict resolved. |
| RESOLVE_ACTION_OVERRIDE | 2 | RESOLVE_ACTION_OVERRIDE grants an explicit override keeping the losing layer's verdict and marks the conflict resolved. |
| RESOLVE_ACTION_DISMISS | 3 | RESOLVE_ACTION_DISMISS dismisses the conflict without action. |
| RESOLVE_ACTION_ADOPT_RECOMMENDATION | 4 | RESOLVE_ACTION_ADOPT_RECOMMENDATION adopts the cached Recommendation (GAP-28, #1106) and marks the conflict resolved. Requires a cached recommendation to be present on the row; callers MUST run GetRecommendation first if absent. |
| RESOLVE_ACTION_SIDE_WITH | 5 | RESOLVE_ACTION_SIDE_WITH selects one of Conflict.sides[] as the resolution outcome (GAP-28, #1106) and marks the conflict resolved. Requires ResolveConflictRequest.side_id to be set to a side_id that exists on the conflict. |
SideColor
SideColor is the rendering hint enum for ConflictSide. Mirrors the
prototype's color field which is consumed via CSS variable lookup
(var(--${color})).
| Name | Number | Description |
|---|---|---|
| SIDE_COLOR_UNSPECIFIED | 0 | SIDE_COLOR_UNSPECIFIED is the zero value reserved by protobuf. |
| SIDE_COLOR_OK | 1 | SIDE_COLOR_OK indicates a permissive / approving side (green). |
| SIDE_COLOR_DANGER | 2 | SIDE_COLOR_DANGER indicates a blocking / hard-rule side (red). |
| SIDE_COLOR_WARN | 3 | SIDE_COLOR_WARN indicates a cautionary side (amber). |
| SIDE_COLOR_INFO | 4 | SIDE_COLOR_INFO indicates a neutral / informational side (blue). |
ConflictService
ConflictService serves the conflict inbox + streaming + resolution lifecycle. See file-level doc for architectural context.
| Method Name | Request Type | Response Type | Description |
|---|---|---|---|
| StreamConflicts | StreamConflictsRequest | StreamConflictsResponse stream | StreamConflicts provides a server-streaming feed of conflict lifecycle events (created / merge_ready / acknowledged / resolved) scoped to the caller's tenant and member-level visibility. |
The stream is backed by a LISTEN/NOTIFY consumer on the Postgres connection pool; the backend filters events to recipients derived from policy_conflicts.notified_parties. | | ListConflicts | ListConflictsRequest | ListConflictsResponse | ListConflicts returns a paginated conflict ledger for the admin inbox. Supports filters on status and creation window. | | GetConflict | GetConflictRequest | GetConflictResponse | GetConflict returns a single conflict with its full notification delivery ledger. | | AcknowledgeConflict | AcknowledgeConflictRequest | AcknowledgeConflictResponse | AcknowledgeConflict transitions status 'open' -> 'acknowledged'. Idempotent — re-acknowledging an already-acknowledged row is a no-op. | | ResolveConflict | ResolveConflictRequest | ResolveConflictResponse | ResolveConflict transitions status to 'resolved' or 'dismissed'. The Action enum distinguishes accept-merge / grant-override / dismiss. | | RecordConflict | RecordConflictRequest | RecordConflictResponse | RecordConflict is an internal RPC called by the governance Engine when cascade evaluation detects opposing non-default verdicts across layers. Returns the conflict_id which the Engine stamps onto Decision.ConflictID so the caller can render "see conflict #<id>".
This RPC is NOT RBAC-gated (no HTTP exposure); callers authenticate via the internal service mesh and provide org_id + member_id directly. | | GetRecommendation | GetRecommendationRequest | GetRecommendationResponse | GetRecommendation returns the platform's "Quad recommends:" verdict for a single conflict (GAP-27, #1105). The first call generates the recommendation by routing through the platform AI router with a structured prompt assembled from Conflict.sides[] + the tenant's recent comparable decisions; the result is cached on the conflict row so subsequent reads do NOT re-bill an LLM call.
RBAC: caller must be able to view the conflict (conflicts.view). |
Scalar Value Types
| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby |
|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | double | float | Float | |
| float | float | float | float | float32 | float | float | Float | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) |
| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum |
| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| bool | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) |
| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) |