Files
Agent-System-Backend/pkg/extension/capability_explain.go
T
agent-exec-transport e942a35b73
governance / governance (pull_request) Successful in 14s
ci / vet-and-test (pull_request) Successful in 1m18s
agent-exec transport 8315c523cef26780543b9b2130f4f4a4/5c8333ec4e92
2026-10-19 12:21:32 +00:00

159 lines
5.7 KiB
Go

package extension
import (
"context"
"errors"
"net/http"
)
const CapabilityExplainSchemaVersion = 1
// Capability sources name where an allowed action comes from.
const (
CapabilitySourceRole = "role"
CapabilitySourceSystemGrant = "system-grant"
CapabilitySourceBootstrapAdmin = "bootstrap-admin"
)
// Capability scopes name where an allowed action applies.
const (
CapabilityScopeSystem = "system"
CapabilityScopeOwnProjects = "own-projects"
CapabilityScopeMemberProjects = "member-projects"
)
// CapabilitySubject identifies the Host-bound actor and the authority snapshot
// an explanation was computed from. It never names another account.
type CapabilitySubject struct {
SchemaVersion int `json:"schemaVersion"`
UserID string `json:"userId"`
Owner string `json:"owner"`
AuthorityEpoch int64 `json:"authorityEpoch"`
RoleID string `json:"roleId,omitempty"`
RoleName string `json:"roleName,omitempty"`
RoleEnabled bool `json:"roleEnabled"`
}
// CapabilityActionExplain reports one action's effective decision. Source is
// set only when Allowed; LinkedVia names the action that links it; Conflict
// marks a linked action that the enabled role caps off.
type CapabilityActionExplain struct {
ActionID string `json:"actionId"`
Scope string `json:"scope"`
Allowed bool `json:"allowed"`
CapAllowed bool `json:"capAllowed"`
Source string `json:"source,omitempty"`
LinkedVia string `json:"linkedVia,omitempty"`
Conflict bool `json:"conflict,omitempty"`
Reason string `json:"reason,omitempty"`
}
// CapabilityExplain is the creator explanation: project.create, the linked
// own-project management actions, their conflicts and the migration note.
type CapabilityExplain struct {
CapabilitySubject
ProjectCreate CapabilityActionExplain `json:"projectCreate"`
Linked []CapabilityActionExplain `json:"linked"`
Conflicts []string `json:"conflicts"`
MigrationNote string `json:"migrationNote,omitempty"`
}
// MemberBasicCollaborate explains the role defaults a member brings to the
// projects they are a member of. Bootstrap admin is not a content source.
type MemberBasicCollaborate struct {
CapabilitySubject
Actions []CapabilityActionExplain `json:"actions"`
}
// DocumentDutyQualify reports whether the role cap lets a document duty be
// assigned to the actor and whether the role grants it by default.
type DocumentDutyQualify struct {
ActionID string `json:"actionId"`
Qualified bool `json:"qualified"`
DefaultAllowed bool `json:"defaultAllowed"`
Source string `json:"source,omitempty"`
Reason string `json:"reason,omitempty"`
}
type DocumentQualify struct {
CapabilitySubject
Duties []DocumentDutyQualify `json:"duties"`
}
// CapabilityExplainProvider is optional on a trusted dispatch Host. Every
// method explains only the Host-bound actor; callers cannot name another one.
type CapabilityExplainProvider interface {
CurrentCapabilityExplain(context.Context) (CapabilityExplain, error)
CurrentMemberBasicCollaborate(context.Context) (MemberBasicCollaborate, error)
CurrentDocumentQualify(context.Context) (DocumentQualify, error)
}
// Work operations whose failures are categorized for clients.
const (
WorkOperationProjectCreate = "project.create"
WorkOperationMemberAdd = "project.member.add"
WorkOperationConversation = "conversation.work"
)
// Work failure reasons. Each maps to exactly one HTTP status.
const (
WorkFailureDuplicateName = "duplicate-name"
WorkFailurePolicyRejected = "policy-rejected"
WorkFailureMappingNotReady = "mapping-not-ready"
WorkFailureServiceUnavailable = "service-unavailable"
)
// WorkFailure is a categorized refusal a provider may return from project
// create, member add or conversation work. Its text carries only the operation
// and reason, never an account, project or name.
type WorkFailure struct {
Operation string `json:"operation"`
Reason string `json:"reason"`
}
func (f WorkFailure) Error() string { return f.Operation + ": " + f.Reason }
// StatusCode returns 0 for an unknown reason so it is never trusted as a status.
func (f WorkFailure) StatusCode() int {
switch f.Reason {
case WorkFailureDuplicateName:
return http.StatusConflict
case WorkFailurePolicyRejected:
return http.StatusForbidden
case WorkFailureMappingNotReady, WorkFailureServiceUnavailable:
return http.StatusServiceUnavailable
}
return 0
}
// CategorizeWorkFailure classifies a provider error for operation. A known
// WorkFailure keeps its reason; a coded 403/503 (and 409 outside conversation
// work) maps to its category; a deadline is service-unavailable. Anything else
// is left uncategorized so the caller keeps its existing not-found handling.
func CategorizeWorkFailure(operation string, err error) (WorkFailure, bool) {
if err == nil {
return WorkFailure{}, false
}
var failure WorkFailure
if errors.As(err, &failure) && failure.StatusCode() != 0 {
return WorkFailure{Operation: operation, Reason: failure.Reason}, true
}
var coded interface{ StatusCode() int }
if errors.As(err, &coded) {
switch coded.StatusCode() {
case http.StatusForbidden:
return WorkFailure{Operation: operation, Reason: WorkFailurePolicyRejected}, true
case http.StatusConflict:
if operation != WorkOperationConversation {
return WorkFailure{Operation: operation, Reason: WorkFailureDuplicateName}, true
}
case http.StatusServiceUnavailable:
return WorkFailure{Operation: operation, Reason: WorkFailureServiceUnavailable}, true
}
}
if errors.Is(err, context.DeadlineExceeded) {
return WorkFailure{Operation: operation, Reason: WorkFailureServiceUnavailable}, true
}
return WorkFailure{}, false
}