Skip to content

approvals/v1: add auto-approve config and evidence types#56

Merged
haasonsaas merged 3 commits intomainfrom
feat/auto-approve-proto
Apr 14, 2026
Merged

approvals/v1: add auto-approve config and evidence types#56
haasonsaas merged 3 commits intomainfrom
feat/auto-approve-proto

Conversation

@haasonsaas
Copy link
Copy Markdown
Contributor

Summary

  • Add AutoApproveConfig message to control per-workspace auto-approval (enabled flag, confidence threshold, min observations, excluded risk levels)
  • Add AutoApproveEvidence message to capture the data behind an automatic approval decision
  • Add DECISION_TYPE_AUTO_APPROVED (value 5) to DecisionType enum
  • Add auto_approve_config field to ApprovalPolicy (field 3)
  • Add auto_approve_evidence field to RequestApprovalResponse (field 2)
  • Add approved_count field to ApprovalHabit (field 4)

Closes #55

Test plan

  • buf lint passes
  • buf generate regenerates Go/TS/Python bindings without errors
  • go test ./... passes (3 test suites pass, generated code compiles cleanly)

🤖 Generated with Claude Code

Add AutoApproveConfig message for controlling automatic approval behavior
per workspace, AutoApproveEvidence for capturing the data behind auto
decisions, DECISION_TYPE_AUTO_APPROVED enum value, approved_count to
ApprovalHabit, and auto_approve_evidence to RequestApprovalResponse.

Closes #55

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 14, 2026

PR Summary

Medium Risk
Extends the public approvals.v1 protobuf API with new messages/fields and a new DecisionType enum value, which may require coordinated client/server rollouts and handling of unknown enum values in older clients.

Overview
Adds first-class protobuf support for automatic approvals in approvals/v1.

Introduces AutoApproveConfig (per-workspace auto-approval settings) and AutoApproveEvidence (metadata explaining an auto-approval), wires auto_approve_config into ApprovalPolicy and returns auto_approve_evidence from RequestApprovalResponse, and extends decision tracking with DECISION_TYPE_AUTO_APPROVED plus approved_count on ApprovalHabit.

Regenerates Go, Python, and TypeScript bindings to reflect the updated schema.

Reviewed by Cursor Bugbot for commit 3d957fa. Bugbot is set up for automated code reviews on this repo. Configure here.

@haasonsaas
Copy link
Copy Markdown
Contributor Author

Review

Clean proto changes. A few notes:

Looks good

  • AutoApproveConfig as a nested message on ApprovalPolicy — clean nil-check, independently passable, extensible
  • DECISION_TYPE_AUTO_APPROVED = 5 — critical for downstream switch exhaustiveness and audit
  • AutoApproveEvidence on response — proper audit trail with the threshold that was applied
  • approved_count on ApprovalHabit — needed for UI confidence display

Heads-up: conflicts with #53

PR #53 (codex/proto-approvals-auto-approve-habits) addresses the same issue with flat fields on ApprovalPolicy. I've recommended closing #53 in favor of this approach. The field numbers conflict (both use 3+ on ApprovalPolicy and 2+ on RequestApprovalResponse), so only one should merge.

One thing to consider for a follow-up

The AssignLabelRequest in prompts/v1 will eventually need an eval gate (fermata#9223). When that happens, the pattern established here — evidence message on the response — could be reused: AssignLabelResponse could carry EvalGateEvidence the same way RequestApprovalResponse carries AutoApproveEvidence. Worth keeping consistent.

Downstream consumers that need updating

Once merged, these services need proto vendor bumps and new case handling:

  • evalops/approvals — implement the threshold check (approvals#10)
  • evalops/agent-mcp — handle AUTO_APPROVED in toolCheckAction
  • evalops/objectives — handle AUTO_APPROVED in workflow delegation
  • evalops/ensemble — can retire local habit system in favor of centralized auto-approve

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Proto3 zero defaults undermine safety-critical threshold fields
    • Marked the threshold and min_observations fields optional so consumers can detect omission and apply safe defaults instead of inheriting proto3 zero values.
Preview (c080deb7f5)
diff --git a/gen/go/approvals/v1/approvals.pb.go b/gen/go/approvals/v1/approvals.pb.go
--- a/gen/go/approvals/v1/approvals.pb.go
+++ b/gen/go/approvals/v1/approvals.pb.go
@@ -82,11 +82,12 @@
 type DecisionType int32
 
 const (
-	DecisionType_DECISION_TYPE_UNSPECIFIED DecisionType = 0
-	DecisionType_DECISION_TYPE_APPROVED    DecisionType = 1
-	DecisionType_DECISION_TYPE_DENIED      DecisionType = 2
-	DecisionType_DECISION_TYPE_ESCALATED   DecisionType = 3
-	DecisionType_DECISION_TYPE_EXPIRED     DecisionType = 4
+	DecisionType_DECISION_TYPE_UNSPECIFIED   DecisionType = 0
+	DecisionType_DECISION_TYPE_APPROVED      DecisionType = 1
+	DecisionType_DECISION_TYPE_DENIED        DecisionType = 2
+	DecisionType_DECISION_TYPE_ESCALATED     DecisionType = 3
+	DecisionType_DECISION_TYPE_EXPIRED       DecisionType = 4
+	DecisionType_DECISION_TYPE_AUTO_APPROVED DecisionType = 5
 )
 
 // Enum value maps for DecisionType.
@@ -97,13 +98,15 @@
 		2: "DECISION_TYPE_DENIED",
 		3: "DECISION_TYPE_ESCALATED",
 		4: "DECISION_TYPE_EXPIRED",
+		5: "DECISION_TYPE_AUTO_APPROVED",
 	}
 	DecisionType_value = map[string]int32{
-		"DECISION_TYPE_UNSPECIFIED": 0,
-		"DECISION_TYPE_APPROVED":    1,
-		"DECISION_TYPE_DENIED":      2,
-		"DECISION_TYPE_ESCALATED":   3,
-		"DECISION_TYPE_EXPIRED":     4,
+		"DECISION_TYPE_UNSPECIFIED":   0,
+		"DECISION_TYPE_APPROVED":      1,
+		"DECISION_TYPE_DENIED":        2,
+		"DECISION_TYPE_ESCALATED":     3,
+		"DECISION_TYPE_EXPIRED":       4,
+		"DECISION_TYPE_AUTO_APPROVED": 5,
 	}
 )
 
@@ -187,6 +190,144 @@
 	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{2}
 }
 
+// AutoApproveConfig controls automatic approval behavior for a workspace.
+type AutoApproveConfig struct {
+	state              protoimpl.MessageState `protogen:"open.v1"`
+	Enabled            bool                   `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
+	Threshold          *float32               `protobuf:"fixed32,2,opt,name=threshold,proto3,oneof" json:"threshold,omitempty"`                                                                           // minimum confidence (default 0.95)
+	MinObservations    *int32                 `protobuf:"varint,3,opt,name=min_observations,json=minObservations,proto3,oneof" json:"min_observations,omitempty"`                                         // minimum decisions before activation (default 20)
+	ExcludedRiskLevels []RiskLevel            `protobuf:"varint,4,rep,packed,name=excluded_risk_levels,json=excludedRiskLevels,proto3,enum=approvals.v1.RiskLevel" json:"excluded_risk_levels,omitempty"` // always require human
+	unknownFields      protoimpl.UnknownFields
+	sizeCache          protoimpl.SizeCache
+}
+
+func (x *AutoApproveConfig) Reset() {
+	*x = AutoApproveConfig{}
+	mi := &file_approvals_v1_approvals_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *AutoApproveConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AutoApproveConfig) ProtoMessage() {}
+
+func (x *AutoApproveConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_approvals_v1_approvals_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AutoApproveConfig.ProtoReflect.Descriptor instead.
+func (*AutoApproveConfig) Descriptor() ([]byte, []int) {
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *AutoApproveConfig) GetEnabled() bool {
+	if x != nil {
+		return x.Enabled
+	}
+	return false
+}
+
+func (x *AutoApproveConfig) GetThreshold() float32 {
+	if x != nil && x.Threshold != nil {
+		return *x.Threshold
+	}
+	return 0
+}
+
+func (x *AutoApproveConfig) GetMinObservations() int32 {
+	if x != nil && x.MinObservations != nil {
+		return *x.MinObservations
+	}
+	return 0
+}
+
+func (x *AutoApproveConfig) GetExcludedRiskLevels() []RiskLevel {
+	if x != nil {
+		return x.ExcludedRiskLevels
+	}
+	return nil
+}
+
+// AutoApproveEvidence captures the data behind an automatic approval decision.
+type AutoApproveEvidence struct {
+	state            protoimpl.MessageState `protogen:"open.v1"`
+	Pattern          string                 `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"`
+	Confidence       float32                `protobuf:"fixed32,2,opt,name=confidence,proto3" json:"confidence,omitempty"`
+	ObservationCount int32                  `protobuf:"varint,3,opt,name=observation_count,json=observationCount,proto3" json:"observation_count,omitempty"`
+	ThresholdApplied float32                `protobuf:"fixed32,4,opt,name=threshold_applied,json=thresholdApplied,proto3" json:"threshold_applied,omitempty"`
+	unknownFields    protoimpl.UnknownFields
+	sizeCache        protoimpl.SizeCache
+}
+
+func (x *AutoApproveEvidence) Reset() {
+	*x = AutoApproveEvidence{}
+	mi := &file_approvals_v1_approvals_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *AutoApproveEvidence) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AutoApproveEvidence) ProtoMessage() {}
+
+func (x *AutoApproveEvidence) ProtoReflect() protoreflect.Message {
+	mi := &file_approvals_v1_approvals_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AutoApproveEvidence.ProtoReflect.Descriptor instead.
+func (*AutoApproveEvidence) Descriptor() ([]byte, []int) {
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AutoApproveEvidence) GetPattern() string {
+	if x != nil {
+		return x.Pattern
+	}
+	return ""
+}
+
+func (x *AutoApproveEvidence) GetConfidence() float32 {
+	if x != nil {
+		return x.Confidence
+	}
+	return 0
+}
+
+func (x *AutoApproveEvidence) GetObservationCount() int32 {
+	if x != nil {
+		return x.ObservationCount
+	}
+	return 0
+}
+
+func (x *AutoApproveEvidence) GetThresholdApplied() float32 {
+	if x != nil {
+		return x.ThresholdApplied
+	}
+	return 0
+}
+
 // ApprovalRequest is the canonical approval request record.
 type ApprovalRequest struct {
 	state          protoimpl.MessageState `protogen:"open.v1"`
@@ -207,7 +348,7 @@
 
 func (x *ApprovalRequest) Reset() {
 	*x = ApprovalRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[0]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[2]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -219,7 +360,7 @@
 func (*ApprovalRequest) ProtoMessage() {}
 
 func (x *ApprovalRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[0]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[2]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -232,7 +373,7 @@
 
 // Deprecated: Use ApprovalRequest.ProtoReflect.Descriptor instead.
 func (*ApprovalRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{0}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *ApprovalRequest) GetId() string {
@@ -326,7 +467,7 @@
 
 func (x *ApprovalRule) Reset() {
 	*x = ApprovalRule{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[1]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[3]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -338,7 +479,7 @@
 func (*ApprovalRule) ProtoMessage() {}
 
 func (x *ApprovalRule) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[1]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[3]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -351,7 +492,7 @@
 
 // Deprecated: Use ApprovalRule.ProtoReflect.Descriptor instead.
 func (*ApprovalRule) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{1}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *ApprovalRule) GetId() string {
@@ -391,16 +532,17 @@
 
 // ApprovalPolicy defines the set of rules for a workspace.
 type ApprovalPolicy struct {
-	state         protoimpl.MessageState `protogen:"open.v1"`
-	WorkspaceId   string                 `protobuf:"bytes,1,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"`
-	Rules         []*ApprovalRule        `protobuf:"bytes,2,rep,name=rules,proto3" json:"rules,omitempty"`
-	unknownFields protoimpl.UnknownFields
-	sizeCache     protoimpl.SizeCache
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	WorkspaceId       string                 `protobuf:"bytes,1,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"`
+	Rules             []*ApprovalRule        `protobuf:"bytes,2,rep,name=rules,proto3" json:"rules,omitempty"`
+	AutoApproveConfig *AutoApproveConfig     `protobuf:"bytes,3,opt,name=auto_approve_config,json=autoApproveConfig,proto3" json:"auto_approve_config,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
 }
 
 func (x *ApprovalPolicy) Reset() {
 	*x = ApprovalPolicy{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[2]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[4]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -412,7 +554,7 @@
 func (*ApprovalPolicy) ProtoMessage() {}
 
 func (x *ApprovalPolicy) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[2]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[4]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -425,7 +567,7 @@
 
 // Deprecated: Use ApprovalPolicy.ProtoReflect.Descriptor instead.
 func (*ApprovalPolicy) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{2}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *ApprovalPolicy) GetWorkspaceId() string {
@@ -442,6 +584,13 @@
 	return nil
 }
 
+func (x *ApprovalPolicy) GetAutoApproveConfig() *AutoApproveConfig {
+	if x != nil {
+		return x.AutoApproveConfig
+	}
+	return nil
+}
+
 // ApprovalDecision records a decision made on an approval request.
 type ApprovalDecision struct {
 	state             protoimpl.MessageState `protogen:"open.v1"`
@@ -457,7 +606,7 @@
 
 func (x *ApprovalDecision) Reset() {
 	*x = ApprovalDecision{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[3]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[5]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -469,7 +618,7 @@
 func (*ApprovalDecision) ProtoMessage() {}
 
 func (x *ApprovalDecision) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[3]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[5]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -482,7 +631,7 @@
 
 // Deprecated: Use ApprovalDecision.ProtoReflect.Descriptor instead.
 func (*ApprovalDecision) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{3}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{5}
 }
 
 func (x *ApprovalDecision) GetId() string {
@@ -533,13 +682,14 @@
 	Pattern               string                 `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"`
 	AutoApproveConfidence float32                `protobuf:"fixed32,2,opt,name=auto_approve_confidence,json=autoApproveConfidence,proto3" json:"auto_approve_confidence,omitempty"`
 	ObservationCount      int32                  `protobuf:"varint,3,opt,name=observation_count,json=observationCount,proto3" json:"observation_count,omitempty"`
+	ApprovedCount         int32                  `protobuf:"varint,4,opt,name=approved_count,json=approvedCount,proto3" json:"approved_count,omitempty"`
 	unknownFields         protoimpl.UnknownFields
 	sizeCache             protoimpl.SizeCache
 }
 
 func (x *ApprovalHabit) Reset() {
 	*x = ApprovalHabit{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[4]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[6]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -551,7 +701,7 @@
 func (*ApprovalHabit) ProtoMessage() {}
 
 func (x *ApprovalHabit) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[4]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[6]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -564,7 +714,7 @@
 
 // Deprecated: Use ApprovalHabit.ProtoReflect.Descriptor instead.
 func (*ApprovalHabit) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{4}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{6}
 }
 
 func (x *ApprovalHabit) GetPattern() string {
@@ -588,6 +738,13 @@
 	return 0
 }
 
+func (x *ApprovalHabit) GetApprovedCount() int32 {
+	if x != nil {
+		return x.ApprovedCount
+	}
+	return 0
+}
+
 type RequestApprovalRequest struct {
 	state          protoimpl.MessageState `protogen:"open.v1"`
 	WorkspaceId    string                 `protobuf:"bytes,1,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"`
@@ -604,7 +761,7 @@
 
 func (x *RequestApprovalRequest) Reset() {
 	*x = RequestApprovalRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[5]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[7]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -616,7 +773,7 @@
 func (*RequestApprovalRequest) ProtoMessage() {}
 
 func (x *RequestApprovalRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[5]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[7]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -629,7 +786,7 @@
 
 // Deprecated: Use RequestApprovalRequest.ProtoReflect.Descriptor instead.
 func (*RequestApprovalRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{5}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{7}
 }
 
 func (x *RequestApprovalRequest) GetWorkspaceId() string {
@@ -689,15 +846,16 @@
 }
 
 type RequestApprovalResponse struct {
-	state           protoimpl.MessageState `protogen:"open.v1"`
-	ApprovalRequest *ApprovalRequest       `protobuf:"bytes,1,opt,name=approval_request,json=approvalRequest,proto3" json:"approval_request,omitempty"`
-	unknownFields   protoimpl.UnknownFields
-	sizeCache       protoimpl.SizeCache
+	state               protoimpl.MessageState `protogen:"open.v1"`
+	ApprovalRequest     *ApprovalRequest       `protobuf:"bytes,1,opt,name=approval_request,json=approvalRequest,proto3" json:"approval_request,omitempty"`
+	AutoApproveEvidence *AutoApproveEvidence   `protobuf:"bytes,2,opt,name=auto_approve_evidence,json=autoApproveEvidence,proto3" json:"auto_approve_evidence,omitempty"`
+	unknownFields       protoimpl.UnknownFields
+	sizeCache           protoimpl.SizeCache
 }
 
 func (x *RequestApprovalResponse) Reset() {
 	*x = RequestApprovalResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[6]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[8]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -709,7 +867,7 @@
 func (*RequestApprovalResponse) ProtoMessage() {}
 
 func (x *RequestApprovalResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[6]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[8]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -722,7 +880,7 @@
 
 // Deprecated: Use RequestApprovalResponse.ProtoReflect.Descriptor instead.
 func (*RequestApprovalResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{6}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{8}
 }
 
 func (x *RequestApprovalResponse) GetApprovalRequest() *ApprovalRequest {
@@ -732,6 +890,13 @@
 	return nil
 }
 
+func (x *RequestApprovalResponse) GetAutoApproveEvidence() *AutoApproveEvidence {
+	if x != nil {
+		return x.AutoApproveEvidence
+	}
+	return nil
+}
+
 type ResolveApprovalRequest struct {
 	state             protoimpl.MessageState `protogen:"open.v1"`
 	ApprovalRequestId string                 `protobuf:"bytes,1,opt,name=approval_request_id,json=approvalRequestId,proto3" json:"approval_request_id,omitempty"`
@@ -744,7 +909,7 @@
 
 func (x *ResolveApprovalRequest) Reset() {
 	*x = ResolveApprovalRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[7]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[9]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -756,7 +921,7 @@
 func (*ResolveApprovalRequest) ProtoMessage() {}
 
 func (x *ResolveApprovalRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[7]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[9]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -769,7 +934,7 @@
 
 // Deprecated: Use ResolveApprovalRequest.ProtoReflect.Descriptor instead.
 func (*ResolveApprovalRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{7}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{9}
 }
 
 func (x *ResolveApprovalRequest) GetApprovalRequestId() string {
@@ -809,7 +974,7 @@
 
 func (x *ResolveApprovalResponse) Reset() {
 	*x = ResolveApprovalResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[8]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[10]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -821,7 +986,7 @@
 func (*ResolveApprovalResponse) ProtoMessage() {}
 
 func (x *ResolveApprovalResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[8]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[10]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -834,7 +999,7 @@
 
 // Deprecated: Use ResolveApprovalResponse.ProtoReflect.Descriptor instead.
 func (*ResolveApprovalResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{8}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{10}
 }
 
 func (x *ResolveApprovalResponse) GetDecision() *ApprovalDecision {
@@ -853,7 +1018,7 @@
 
 func (x *GetPolicyRequest) Reset() {
 	*x = GetPolicyRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[9]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[11]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -865,7 +1030,7 @@
 func (*GetPolicyRequest) ProtoMessage() {}
 
 func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[9]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[11]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -878,7 +1043,7 @@
 
 // Deprecated: Use GetPolicyRequest.ProtoReflect.Descriptor instead.
 func (*GetPolicyRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{9}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{11}
 }
 
 func (x *GetPolicyRequest) GetWorkspaceId() string {
@@ -897,7 +1062,7 @@
 
 func (x *GetPolicyResponse) Reset() {
 	*x = GetPolicyResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[10]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[12]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -909,7 +1074,7 @@
 func (*GetPolicyResponse) ProtoMessage() {}
 
 func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[10]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[12]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -922,7 +1087,7 @@
 
 // Deprecated: Use GetPolicyResponse.ProtoReflect.Descriptor instead.
 func (*GetPolicyResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{10}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{12}
 }
 
 func (x *GetPolicyResponse) GetPolicy() *ApprovalPolicy {
@@ -941,7 +1106,7 @@
 
 func (x *SetPolicyRequest) Reset() {
 	*x = SetPolicyRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[11]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[13]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -953,7 +1118,7 @@
 func (*SetPolicyRequest) ProtoMessage() {}
 
 func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[11]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[13]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -966,7 +1131,7 @@
 
 // Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead.
 func (*SetPolicyRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{11}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{13}
 }
 
 func (x *SetPolicyRequest) GetPolicy() *ApprovalPolicy {
@@ -985,7 +1150,7 @@
 
 func (x *SetPolicyResponse) Reset() {
 	*x = SetPolicyResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[12]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[14]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -997,7 +1162,7 @@
 func (*SetPolicyResponse) ProtoMessage() {}
 
 func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[12]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[14]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1010,7 +1175,7 @@
 
 // Deprecated: Use SetPolicyResponse.ProtoReflect.Descriptor instead.
 func (*SetPolicyResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{12}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{14}
 }
 
 func (x *SetPolicyResponse) GetPolicy() *ApprovalPolicy {
@@ -1031,7 +1196,7 @@
 
 func (x *ListPendingRequest) Reset() {
 	*x = ListPendingRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[13]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[15]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1043,7 +1208,7 @@
 func (*ListPendingRequest) ProtoMessage() {}
 
 func (x *ListPendingRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[13]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[15]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1056,7 +1221,7 @@
 
 // Deprecated: Use ListPendingRequest.ProtoReflect.Descriptor instead.
 func (*ListPendingRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{13}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{15}
 }
 
 func (x *ListPendingRequest) GetWorkspaceId() string {
@@ -1090,7 +1255,7 @@
 
 func (x *ListPendingResponse) Reset() {
 	*x = ListPendingResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[14]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[16]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1102,7 +1267,7 @@
 func (*ListPendingResponse) ProtoMessage() {}
 
 func (x *ListPendingResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[14]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[16]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1115,7 +1280,7 @@
 
 // Deprecated: Use ListPendingResponse.ProtoReflect.Descriptor instead.
 func (*ListPendingResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{14}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{16}
 }
 
 func (x *ListPendingResponse) GetRequests() []*ApprovalRequest {
@@ -1141,7 +1306,7 @@
 
 func (x *GetHabitsRequest) Reset() {
 	*x = GetHabitsRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[15]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[17]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1153,7 +1318,7 @@
 func (*GetHabitsRequest) ProtoMessage() {}
 
 func (x *GetHabitsRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[15]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[17]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1166,7 +1331,7 @@
 
 // Deprecated: Use GetHabitsRequest.ProtoReflect.Descriptor instead.
 func (*GetHabitsRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{15}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{17}
 }
 
 func (x *GetHabitsRequest) GetWorkspaceId() string {
@@ -1185,7 +1350,7 @@
 
 func (x *GetHabitsResponse) Reset() {
 	*x = GetHabitsResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[16]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[18]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1197,7 +1362,7 @@
 func (*GetHabitsResponse) ProtoMessage() {}
 
 func (x *GetHabitsResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[16]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[18]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1210,7 +1375,7 @@
 
 // Deprecated: Use GetHabitsResponse.ProtoReflect.Descriptor instead.
 func (*GetHabitsResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{16}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{18}
 }
 
 func (x *GetHabitsResponse) GetHabits() []*ApprovalHabit {
@@ -1230,7 +1395,7 @@
 
 func (x *EscalateRequest) Reset() {
 	*x = EscalateRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[17]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[19]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1242,7 +1407,7 @@
 func (*EscalateRequest) ProtoMessage() {}
 
 func (x *EscalateRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[17]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[19]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1255,7 +1420,7 @@
 
 // Deprecated: Use EscalateRequest.ProtoReflect.Descriptor instead.
 func (*EscalateRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{17}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{19}
 }
 
 func (x *EscalateRequest) GetApprovalRequestId() string {
@@ -1281,7 +1446,7 @@
 
 func (x *EscalateResponse) Reset() {
 	*x = EscalateResponse{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[18]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[20]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1293,7 +1458,7 @@
 func (*EscalateResponse) ProtoMessage() {}
 
 func (x *EscalateResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[18]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[20]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1306,7 +1471,7 @@
 
 // Deprecated: Use EscalateResponse.ProtoReflect.Descriptor instead.
 func (*EscalateResponse) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{18}
+	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{20}
 }
 
 func (x *EscalateResponse) GetApprovalRequest() *ApprovalRequest {
@@ -1326,7 +1491,7 @@
 
 func (x *GetApprovalRequest) Reset() {
 	*x = GetApprovalRequest{}
-	mi := &file_approvals_v1_approvals_proto_msgTypes[19]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[21]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -1338,7 +1503,7 @@
 func (*GetApprovalRequest) ProtoMessage() {}
 
 func (x *GetApprovalRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_approvals_v1_approvals_proto_msgTypes[19]
+	mi := &file_approvals_v1_approvals_proto_msgTypes[21]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1351,7 +1516,7 @@
 
 // Deprecated: Use GetApprovalRequest.ProtoReflect.Descriptor instead.
 func (*GetApprovalRequest) Descriptor() ([]byte, []int) {
-	return file_approvals_v1_approvals_proto_rawDescGZIP(), []int{19}
... diff truncated: showing 800 of 1583 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 96ceb21. Configure here.

cursoragent and others added 2 commits April 14, 2026 18:39
…fety bypass

Document safe defaults (0.95 threshold, 20 min_observations) in field
comments so consumers know to nil-check and apply these when fields are
unset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@haasonsaas haasonsaas merged commit 1cc1a50 into main Apr 14, 2026
9 checks passed
@haasonsaas haasonsaas deleted the feat/auto-approve-proto branch April 14, 2026 19:30
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.

approvals/v1: add auto-approve config and habit-auto-approved event type

2 participants