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:
weichen
2026-10-14 15:33:26 +00:00
parent 2716b9e607
commit 2c4ccc33c4
6 changed files with 253 additions and 1 deletions
+1
View File
@@ -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
+56
View File
@@ -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 &copy
}
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 = &copy
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")
}
}
+3 -1
View File
@@ -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")
+128
View File
@@ -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
}
+19
View File
@@ -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