agent-exec transport 8315c523cef26780543b9b2130f4f4a4/5c8333ec4e92
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type codedTestError int
|
||||
|
||||
func (e codedTestError) Error() string { return "provider detail about other-owner" }
|
||||
func (e codedTestError) StatusCode() int { return int(e) }
|
||||
|
||||
func TestWorkFailureStatusAndTextCarryNoIdentifiers(t *testing.T) {
|
||||
for reason, code := range map[string]int{
|
||||
WorkFailureDuplicateName: http.StatusConflict,
|
||||
WorkFailurePolicyRejected: http.StatusForbidden,
|
||||
WorkFailureMappingNotReady: http.StatusServiceUnavailable,
|
||||
WorkFailureServiceUnavailable: http.StatusServiceUnavailable,
|
||||
"invented": 0,
|
||||
} {
|
||||
failure := WorkFailure{Operation: WorkOperationMemberAdd, Reason: reason}
|
||||
if failure.StatusCode() != code || failure.Error() != WorkOperationMemberAdd+": "+reason {
|
||||
t.Fatalf("%s: status=%d text=%q", reason, failure.StatusCode(), failure.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeWorkFailure(t *testing.T) {
|
||||
cases := []struct {
|
||||
op string
|
||||
err error
|
||||
reason string
|
||||
ok bool
|
||||
}{
|
||||
{WorkOperationProjectCreate, fmt.Errorf("wrap: %w", WorkFailure{Reason: WorkFailureMappingNotReady}), WorkFailureMappingNotReady, true},
|
||||
{WorkOperationProjectCreate, codedTestError(409), WorkFailureDuplicateName, true},
|
||||
{WorkOperationMemberAdd, codedTestError(403), WorkFailurePolicyRejected, true},
|
||||
{WorkOperationConversation, codedTestError(503), WorkFailureServiceUnavailable, true},
|
||||
{WorkOperationConversation, codedTestError(409), "", false},
|
||||
{WorkOperationConversation, context.DeadlineExceeded, WorkFailureServiceUnavailable, true},
|
||||
{WorkOperationMemberAdd, codedTestError(404), "", false},
|
||||
{WorkOperationMemberAdd, WorkFailure{Reason: "invented"}, "", false},
|
||||
{WorkOperationMemberAdd, errors.New("plain"), "", false},
|
||||
{WorkOperationMemberAdd, nil, "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
failure, ok := CategorizeWorkFailure(tc.op, tc.err)
|
||||
if ok != tc.ok || failure.Reason != tc.reason || (ok && failure.Operation != tc.op) || strings.Contains(failure.Error(), "other-owner") {
|
||||
t.Fatalf("%s %v: failure=%+v ok=%v", tc.op, tc.err, failure, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,11 @@ import (
|
||||
// reject stale role snapshots.
|
||||
// 2.26.0-experimental: IAM link U1 RolePolicySnapshot.Linked carries the
|
||||
// Backend-computed project.create own-project management link and its source.
|
||||
const SurfaceVersion = "2.26.0-experimental"
|
||||
// 2.27.0-experimental: IAM link U3 optional CapabilityExplainProvider
|
||||
// (CapabilityExplain / MemberBasicCollaborate / DocumentQualify for the
|
||||
// Host-bound actor) and categorized WorkFailure for create, member add and
|
||||
// conversation work.
|
||||
const SurfaceVersion = "2.27.0-experimental"
|
||||
|
||||
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
|
||||
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
|
||||
|
||||
@@ -49,6 +49,7 @@ type Handler func(ctx context.Context, h Host, req Request) (any, error)
|
||||
// - WorkTaskHost — X1:分发期建立可恢复的受信项目任务绑定
|
||||
// - HumanDecisionVerifier — 本次分发是否为认证人对确切待确认卡的响应
|
||||
// - RolePolicyProvider — Backend 权威职位与动作默认/上限
|
||||
// - CapabilityExplainProvider — 当前分发主体的创建者/成员/文档职责能力解释
|
||||
type Host interface {
|
||||
// PluginID 返回注册处理器的插件标识(形如 rdm)。
|
||||
PluginID() string
|
||||
|
||||
@@ -26,9 +26,26 @@ AdminProvider
|
||||
AdminProvider.IsAdmin
|
||||
AdminZoneSpec
|
||||
CLIHook
|
||||
CapabilityActionExplain
|
||||
CapabilityExplain
|
||||
CapabilityExplainProvider
|
||||
CapabilityExplainProvider.CurrentCapabilityExplain
|
||||
CapabilityExplainProvider.CurrentDocumentQualify
|
||||
CapabilityExplainProvider.CurrentMemberBasicCollaborate
|
||||
CapabilityExplainSchemaVersion
|
||||
CapabilityScopeMemberProjects
|
||||
CapabilityScopeOwnProjects
|
||||
CapabilityScopeSystem
|
||||
CapabilitySourceBootstrapAdmin
|
||||
CapabilitySourceRole
|
||||
CapabilitySourceSystemGrant
|
||||
CapabilitySubject
|
||||
CategorizeWorkFailure
|
||||
Compiled
|
||||
DelegatedProjectProjectionHost
|
||||
DelegatedProjectProjectionHost.ProjectProjectionForCreator
|
||||
DocumentDutyQualify
|
||||
DocumentQualify
|
||||
ErrNotLinked
|
||||
ErrProjectAuditDenied
|
||||
ErrProjectAuditLinkConflict
|
||||
@@ -66,6 +83,7 @@ InProc
|
||||
IntentSpec
|
||||
LinkedCapabilities
|
||||
Main
|
||||
MemberBasicCollaborate
|
||||
MergeHostOptions
|
||||
NewHostClient
|
||||
NewScopedHost
|
||||
@@ -240,6 +258,11 @@ WorkCompletionHost
|
||||
WorkCompletionHost.CompleteForWork
|
||||
WorkCompletionRequest
|
||||
WorkCompletionResult
|
||||
WorkFailure
|
||||
WorkFailureDuplicateName
|
||||
WorkFailureMappingNotReady
|
||||
WorkFailurePolicyRejected
|
||||
WorkFailureServiceUnavailable
|
||||
WorkObservation
|
||||
WorkObservationAccepted
|
||||
WorkObservationDenied
|
||||
@@ -247,5 +270,8 @@ WorkObservationPartial
|
||||
WorkObservationPending
|
||||
WorkObservationResult
|
||||
WorkObservationState
|
||||
WorkOperationConversation
|
||||
WorkOperationMemberAdd
|
||||
WorkOperationProjectCreate
|
||||
WorkTaskHost
|
||||
WorkTaskHost.TaskCreateForWork
|
||||
|
||||
Reference in New Issue
Block a user