Define trusted project work observation contract
Why: Real project messages and retrieved material need a scoped observer and preflight decision contract before they can become governed project candidates.
Evidence: Registration denial test was RED before the implementation; go test ./... passed on Backend origin/main 2716b9e.
Impact: Add public X3 types and trusted single-provider registration; the production observer slot is deliberately not wired yet, so no project observation behavior is claimed.
Refs: TT-N03, TT-R01; approved work observation stage-five plan r1
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
// inprocHost(EXT-1 批注 1:复用现有 owner 作用域函数,不新造句柄)。
|
||||
type hostcaps struct {
|
||||
projects *projectLifecycleSlot
|
||||
workObserver *projectWorkObserverSlot
|
||||
directory *accountDirectorySlot
|
||||
accountGuard func(string) error
|
||||
allowImplicitLocal bool
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"agent-system-backend/pkg/extension"
|
||||
)
|
||||
|
||||
type projectWorkObserverSlot struct {
|
||||
mu sync.RWMutex
|
||||
provider *extension.ProjectWorkObserver
|
||||
}
|
||||
|
||||
func (s *projectWorkObserverSlot) get() *extension.ProjectWorkObserver {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.provider == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *s.provider
|
||||
return ©
|
||||
}
|
||||
|
||||
func (r compiledRegistry) RegisterProjectWorkObserver(extension.ProjectWorkObserver) error {
|
||||
return errors.New("extension: declarative plugins cannot register project work observers")
|
||||
}
|
||||
|
||||
func (r pluginRegistry) RegisterProjectWorkObserver(p extension.ProjectWorkObserver) error {
|
||||
if r.pluginID != "rdm" || r.caps.workObserver == nil {
|
||||
return errors.New("extension: project work observer requires trusted RDM registration")
|
||||
}
|
||||
if p.Observe == nil || p.Preflight == nil || p.Respond == nil {
|
||||
return errors.New("extension: project work observer requires all callbacks")
|
||||
}
|
||||
s := r.caps.workObserver
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.provider != nil {
|
||||
return errors.New("extension: project work observer already registered")
|
||||
}
|
||||
copy := p
|
||||
s.provider = ©
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r recordRegistry) RegisterProjectWorkObserver(p extension.ProjectWorkObserver) error {
|
||||
reg, ok := r.Registry.(extension.ProjectWorkObservationRegistry)
|
||||
if !ok {
|
||||
return errors.New("extension: project work observation registration unavailable")
|
||||
}
|
||||
return reg.RegisterProjectWorkObserver(p)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/pkg/extension"
|
||||
)
|
||||
|
||||
func workObserverForTest() extension.ProjectWorkObserver {
|
||||
return extension.ProjectWorkObserver{
|
||||
Observe: func(context.Context, extension.WorkObservation) (extension.WorkObservationResult, error) {
|
||||
return extension.WorkObservationResult{State: extension.WorkObservationPending}, nil
|
||||
},
|
||||
Preflight: func(context.Context, extension.ProjectWorkAction) (extension.ProjectWorkDecision, error) {
|
||||
return extension.ProjectWorkDecision{State: extension.ProjectWorkPending}, nil
|
||||
},
|
||||
Respond: func(context.Context, extension.ProjectRiskAnswer) (extension.ProjectWorkDecision, error) {
|
||||
return extension.ProjectWorkDecision{State: extension.ProjectWorkPending}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectWorkObservationRegistrationIsTrustedAndUnique(t *testing.T) {
|
||||
slot := &projectWorkObserverSlot{}
|
||||
trusted := pluginRegistry{pluginID: "rdm", caps: hostcaps{workObserver: slot}}
|
||||
other := pluginRegistry{pluginID: "other", caps: hostcaps{workObserver: slot}}
|
||||
if err := other.RegisterProjectWorkObserver(workObserverForTest()); err == nil {
|
||||
t.Fatal("untrusted plugin registered project observer")
|
||||
}
|
||||
if err := (compiledRegistry{}).RegisterProjectWorkObserver(workObserverForTest()); err == nil {
|
||||
t.Fatal("declarative plugin registered live observer callback")
|
||||
}
|
||||
if err := trusted.RegisterProjectWorkObserver(extension.ProjectWorkObserver{}); err == nil {
|
||||
t.Fatal("partial observer registered")
|
||||
}
|
||||
if err := trusted.RegisterProjectWorkObserver(workObserverForTest()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := trusted.RegisterProjectWorkObserver(workObserverForTest()); err == nil {
|
||||
t.Fatal("second observer replaced the trusted provider")
|
||||
}
|
||||
if got := slot.get(); got == nil || got.Observe == nil || got.Preflight == nil || got.Respond == nil {
|
||||
t.Fatal("registered observer not available to host")
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ import (
|
||||
// ResourceAuthzQuery.Source for per-document publication authorization.
|
||||
// 2.13.0-experimental: optional AccountDirectoryProvider and trusted in-process
|
||||
// AccountDirectoryRegistrar for scoped, current account candidate decisions.
|
||||
const SurfaceVersion = "2.13.0-experimental"
|
||||
// 2.14.0-experimental: optional ProjectWorkObservationRegistry; host-origin
|
||||
// work-source observations and project action decisions (X3 contract).
|
||||
const SurfaceVersion = "2.14.0-experimental"
|
||||
|
||||
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
|
||||
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// WorkObservation is built by the host from a committed message or an actual
|
||||
// retrieval, tool, or visible analysis result. Callers and models cannot supply
|
||||
// Actor, ProjectID, source identity, or the source authorization epoch.
|
||||
// RunID may be empty for a newly committed user message or chat command.
|
||||
type WorkObservation struct {
|
||||
EventID string `json:"eventId"`
|
||||
Phase string `json:"phase"`
|
||||
Actor string `json:"actor"`
|
||||
Owner string `json:"owner"`
|
||||
ProjectID core.ID `json:"projectId"`
|
||||
ConversationID core.ID `json:"conversationId,omitempty"`
|
||||
RunID core.ID `json:"runId,omitempty"`
|
||||
SourceKind string `json:"sourceKind"`
|
||||
SourceID string `json:"sourceId"`
|
||||
SourceVersion string `json:"sourceVersion"`
|
||||
SourceSHA256 string `json:"sourceSha256"`
|
||||
PartIndex int `json:"partIndex"`
|
||||
SpanStart int `json:"spanStart"` // UTF-8 byte offset in the exact source
|
||||
SpanEnd int `json:"spanEnd"`
|
||||
Text string `json:"text,omitempty"` // only the actual authorized fragment
|
||||
Scope string `json:"scope"` // private|project|controlled
|
||||
Delivery string `json:"delivery"` // complete|summary-only|partial
|
||||
AuthorizationEpoch string `json:"authorizationEpoch"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
}
|
||||
|
||||
type WorkObservationState string
|
||||
|
||||
const (
|
||||
WorkObservationAccepted WorkObservationState = "accepted" // durable intake, analysis may be pending
|
||||
WorkObservationPending WorkObservationState = "pending"
|
||||
WorkObservationDenied WorkObservationState = "denied"
|
||||
WorkObservationPartial WorkObservationState = "partial"
|
||||
)
|
||||
|
||||
type WorkObservationResult struct {
|
||||
ReceiptID string `json:"receiptId"`
|
||||
State WorkObservationState `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectWorkSource is one exact part consumed by the pending action. A hash
|
||||
// mismatch or missing part must block Preflight rather than use a stale receipt.
|
||||
type ProjectWorkSource struct {
|
||||
EventID string `json:"eventId"`
|
||||
SourceKind string `json:"sourceKind"`
|
||||
SourceID string `json:"sourceId"`
|
||||
SourceVersion string `json:"sourceVersion"`
|
||||
SourceSHA256 string `json:"sourceSha256"`
|
||||
PartIndex int `json:"partIndex"`
|
||||
}
|
||||
|
||||
// ProjectWorkAction is constructed by the host immediately before model use,
|
||||
// project command dispatch, or another project-affecting action. ProjectID is
|
||||
// checked against the stored conversation or run, never trusted from Params.
|
||||
type ProjectWorkAction struct {
|
||||
Actor string `json:"actor"`
|
||||
Owner string `json:"owner"`
|
||||
ProjectID core.ID `json:"projectId"`
|
||||
ConversationID core.ID `json:"conversationId,omitempty"`
|
||||
RunID core.ID `json:"runId,omitempty"`
|
||||
Action string `json:"action"`
|
||||
TargetID string `json:"targetId,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Manifest []ProjectWorkSource `json:"manifest"`
|
||||
SourceWatermark string `json:"sourceWatermark"`
|
||||
}
|
||||
|
||||
type ProjectWorkDecisionState string
|
||||
|
||||
const (
|
||||
ProjectWorkProceed ProjectWorkDecisionState = "proceed"
|
||||
ProjectWorkClarify ProjectWorkDecisionState = "clarify"
|
||||
ProjectWorkPending ProjectWorkDecisionState = "pending"
|
||||
ProjectWorkDenied ProjectWorkDecisionState = "denied"
|
||||
)
|
||||
|
||||
// ProjectWorkDecision is a domain decision, not a replacement for a controlled
|
||||
// document's separate one-time human approval. A successful Observe ACK cannot
|
||||
// be interpreted as ProjectWorkProceed.
|
||||
type ProjectWorkDecision struct {
|
||||
State ProjectWorkDecisionState `json:"state"`
|
||||
DecisionID string `json:"decisionId"`
|
||||
RiskID string `json:"riskId,omitempty"`
|
||||
RiskRevision string `json:"riskRevision,omitempty"`
|
||||
SourceWatermark string `json:"sourceWatermark,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Question string `json:"question,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectRiskAnswer comes from a current authenticated session and persisted
|
||||
// risk card. An answer is applied only after the provider commits a CAS receipt.
|
||||
type ProjectRiskAnswer struct {
|
||||
Actor string `json:"actor"`
|
||||
Owner string `json:"owner"`
|
||||
ProjectID core.ID `json:"projectId"`
|
||||
RiskID string `json:"riskId"`
|
||||
RiskRevision string `json:"riskRevision"`
|
||||
Answer string `json:"answer"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
// ProjectWorkObserver is owned by the project domain. Backend must check a
|
||||
// returned decision against the exact current source manifest and must fail
|
||||
// closed on missing provider, timeout, or partial analysis when project facts
|
||||
// are required. RDM remains the sole persistent risk-state owner.
|
||||
type ProjectWorkObserver struct {
|
||||
Observe func(context.Context, WorkObservation) (WorkObservationResult, error)
|
||||
Preflight func(context.Context, ProjectWorkAction) (ProjectWorkDecision, error)
|
||||
Respond func(context.Context, ProjectRiskAnswer) (ProjectWorkDecision, error)
|
||||
}
|
||||
|
||||
// ProjectWorkObservationRegistry is optional so older in-process extensions do
|
||||
// not acquire a mandatory Registry method. Declarative plugins cannot register
|
||||
// a callback. The host accepts one trusted RDM provider.
|
||||
type ProjectWorkObservationRegistry interface {
|
||||
RegisterProjectWorkObserver(ProjectWorkObserver) error
|
||||
}
|
||||
@@ -64,6 +64,18 @@ ProjectHost.RenameOwnedProject
|
||||
ProjectLifecycle
|
||||
ProjectLifecycleRegistry
|
||||
ProjectLifecycleRegistry.RegisterProjectLifecycle
|
||||
ProjectRiskAnswer
|
||||
ProjectWorkAction
|
||||
ProjectWorkClarify
|
||||
ProjectWorkDecision
|
||||
ProjectWorkDecisionState
|
||||
ProjectWorkDenied
|
||||
ProjectWorkObservationRegistry
|
||||
ProjectWorkObservationRegistry.RegisterProjectWorkObserver
|
||||
ProjectWorkObserver
|
||||
ProjectWorkPending
|
||||
ProjectWorkProceed
|
||||
ProjectWorkSource
|
||||
RegisterCompiled
|
||||
Registry
|
||||
Registry.AddAdminZone
|
||||
@@ -88,3 +100,10 @@ SurfaceHost.SurfaceList
|
||||
SurfaceHost.SurfaceSave
|
||||
SurfaceSaveRequest
|
||||
SurfaceVersion
|
||||
WorkObservation
|
||||
WorkObservationAccepted
|
||||
WorkObservationDenied
|
||||
WorkObservationPartial
|
||||
WorkObservationPending
|
||||
WorkObservationResult
|
||||
WorkObservationState
|
||||
|
||||
Reference in New Issue
Block a user