mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 09:11:44 +08:00
184 lines
5.4 KiB
Go
184 lines
5.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package semantic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
const maxModelResponseBytes = 1 << 20
|
|
|
|
type Review struct {
|
|
Verdict string `json:"verdict"`
|
|
Findings []Finding `json:"findings"`
|
|
}
|
|
|
|
type Finding struct {
|
|
Category string `json:"category"`
|
|
Severity string `json:"severity"`
|
|
Evidence []string `json:"evidence"`
|
|
Message string `json:"message"`
|
|
SuggestedAction string `json:"suggested_action"`
|
|
ReviewAction string `json:"review_action,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
RolloutGroups []string `json:"rollout_groups,omitempty"`
|
|
WaiverID string `json:"waiver_id,omitempty"`
|
|
WaiverKeys []string `json:"waiver_keys,omitempty"`
|
|
}
|
|
|
|
const (
|
|
ReviewActionMustFix = "must_fix"
|
|
ReviewActionConfirm = "confirm"
|
|
ReviewActionObserve = "observe"
|
|
)
|
|
|
|
type SystemWarning struct {
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message"`
|
|
SuggestedAction string `json:"suggested_action,omitempty"`
|
|
}
|
|
|
|
type Policy struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
DefaultEnforcement string `json:"default_enforcement"`
|
|
BlockCategories []string `json:"block_categories"`
|
|
RolloutGroups []RolloutGroup `json:"rollout_groups,omitempty"`
|
|
}
|
|
|
|
type RolloutGroup struct {
|
|
ID string `json:"id"`
|
|
Enforcement string `json:"enforcement"`
|
|
Scope ScopeSelector `json:"scope,omitempty"`
|
|
Categories []string `json:"categories"`
|
|
Owner string `json:"owner"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type ScopeSelector struct {
|
|
ChangedOnly bool `json:"changed_only,omitempty"`
|
|
Domains []string `json:"domains,omitempty"`
|
|
FactKinds []string `json:"fact_kinds,omitempty"`
|
|
Sources []string `json:"sources,omitempty"`
|
|
}
|
|
|
|
type Decision struct {
|
|
BlockMode bool `json:"block_mode"`
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
Degraded bool `json:"degraded,omitempty"`
|
|
InfrastructureFailure bool `json:"infrastructure_failure,omitempty"`
|
|
Blockers []Finding `json:"blockers,omitempty"`
|
|
Warnings []Finding `json:"warnings,omitempty"`
|
|
SystemWarnings []SystemWarning `json:"system_warnings,omitempty"`
|
|
}
|
|
|
|
func DefaultPolicy() Policy {
|
|
return Policy{
|
|
SchemaVersion: 1,
|
|
DefaultEnforcement: "observe",
|
|
BlockCategories: []string{"error_hint", "default_output", "naming", "skill_quality"},
|
|
RolloutGroups: []RolloutGroup{{
|
|
ID: "all",
|
|
Enforcement: "blocking",
|
|
Categories: []string{"error_hint", "default_output", "naming", "skill_quality"},
|
|
Owner: "test",
|
|
Reason: "default in-memory policy",
|
|
}},
|
|
}
|
|
}
|
|
|
|
func DecodeReview(r io.Reader) (Review, error) {
|
|
return decodeReview(r, true)
|
|
}
|
|
|
|
// Model responses are normalized through modelReview for compatibility; the
|
|
// local gatekeeper still recomputes blocking evidence from facts.
|
|
func DecodeModelReview(r io.Reader) (Review, error) {
|
|
dec := json.NewDecoder(io.LimitReader(r, maxModelResponseBytes))
|
|
var raw modelReview
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return Review{}, err
|
|
}
|
|
review := Review{
|
|
Verdict: raw.Verdict,
|
|
Findings: make([]Finding, 0, len(raw.Findings)),
|
|
}
|
|
for _, finding := range raw.Findings {
|
|
review.Findings = append(review.Findings, Finding{
|
|
Category: finding.Category,
|
|
Severity: finding.Severity,
|
|
Evidence: []string(finding.Evidence),
|
|
Message: finding.Message,
|
|
SuggestedAction: finding.SuggestedAction,
|
|
RolloutGroups: finding.RolloutGroups,
|
|
WaiverID: finding.WaiverID,
|
|
WaiverKeys: finding.WaiverKeys,
|
|
})
|
|
}
|
|
if err := validateReview(review); err != nil {
|
|
return Review{}, err
|
|
}
|
|
return review, nil
|
|
}
|
|
|
|
func decodeReview(r io.Reader, strict bool) (Review, error) {
|
|
dec := json.NewDecoder(io.LimitReader(r, maxModelResponseBytes))
|
|
if strict {
|
|
dec.DisallowUnknownFields()
|
|
}
|
|
var review Review
|
|
if err := dec.Decode(&review); err != nil {
|
|
return Review{}, err
|
|
}
|
|
if err := validateReview(review); err != nil {
|
|
return Review{}, err
|
|
}
|
|
return review, nil
|
|
}
|
|
|
|
type modelReview struct {
|
|
Verdict string `json:"verdict"`
|
|
Findings []modelFinding `json:"findings"`
|
|
}
|
|
|
|
type modelFinding struct {
|
|
Category string `json:"category"`
|
|
Severity string `json:"severity"`
|
|
Evidence modelEvidence `json:"evidence"`
|
|
Message string `json:"message"`
|
|
SuggestedAction string `json:"suggested_action"`
|
|
RolloutGroups []string `json:"rollout_groups,omitempty"`
|
|
WaiverID string `json:"waiver_id,omitempty"`
|
|
WaiverKeys []string `json:"waiver_keys,omitempty"`
|
|
}
|
|
|
|
type modelEvidence []string
|
|
|
|
func (e *modelEvidence) UnmarshalJSON(data []byte) error {
|
|
var list []string
|
|
if err := json.Unmarshal(data, &list); err == nil {
|
|
*e = list
|
|
return nil
|
|
}
|
|
var one string
|
|
if err := json.Unmarshal(data, &one); err == nil {
|
|
*e = []string{one}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("evidence must be a string or string array")
|
|
}
|
|
|
|
func validateReview(review Review) error {
|
|
if len(review.Findings) > 20 {
|
|
return fmt.Errorf("too many findings: %d", len(review.Findings))
|
|
}
|
|
for _, finding := range review.Findings {
|
|
if len(finding.Message) > 500 || len(finding.SuggestedAction) > 500 {
|
|
return fmt.Errorf("finding text exceeds limit")
|
|
}
|
|
}
|
|
return nil
|
|
}
|