agent-exec transport f461d74086dd6a80db72f83748f34b04/128fee1609ad
This commit is contained in:
@@ -75,7 +75,13 @@ import (
|
||||
// directory: RoleOption roleId+name, no permissions or bindings) and the
|
||||
// AccountDirectory purpose project.member-roster, an explicit project member
|
||||
// enumeration inside the trusted RDM scope.
|
||||
const SurfaceVersion = "2.28.0-experimental"
|
||||
// 2.29.0-experimental: X2 project audit read completion. ProjectAuditQuery gains
|
||||
// optional linked resource/run locators, IncludeBody, and Export; receipts may
|
||||
// carry ProjectAuditReceiptResource only under the domain's per-object body
|
||||
// right; ProjectAuditCoverage states Export scope. Every addition is an optional
|
||||
// field or a new exported type: existing ProjectAuditHost/ProjectAuditRegistry
|
||||
// providers and queries keep compiling and behaving unchanged.
|
||||
const SurfaceVersion = "2.29.0-experimental"
|
||||
|
||||
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
|
||||
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
|
||||
|
||||
@@ -74,6 +74,19 @@ type ProjectAuditDisclosure struct {
|
||||
Body bool
|
||||
}
|
||||
|
||||
// ProjectAuditDisclosureAuthorizer answers one per-object decision for the
|
||||
// authenticated actor on a stored, immutable link. Metadata and Body are
|
||||
// separate authorities: the host never substitutes one for the other, and it
|
||||
// never answers a body request from a metadata-only right.
|
||||
//
|
||||
// TODO(RDM consumer, out of this repository): the decision itself — the current
|
||||
// project audit.read grant, the document read duty plus stage scope that carries
|
||||
// 正文权, and any export authority — belongs to the domain plugin that registers
|
||||
// this callback. The Backend only refuses, trims, and reports coverage; it must
|
||||
// not grow a local account/project policy to answer in the domain's place. A
|
||||
// later RDM unit implements the callbacks and can then widen Coverage
|
||||
// (Export, ResourceBodies, RunRelations) from this base's current honest
|
||||
// "unsupported" statements.
|
||||
type ProjectAuditDisclosureAuthorizer func(context.Context, string, ProjectAuditLinkedResource) (ProjectAuditDisclosure, error)
|
||||
|
||||
// ProjectAuditPublisherFactory is issued only by successful verifier
|
||||
@@ -107,6 +120,11 @@ type ProjectAuditPublisher interface {
|
||||
// database guards prove a
|
||||
// committed prefix; otherwise the host rejects the read instead of claiming a
|
||||
// fixed snapshot. See Coverage.CutoffConsistency.
|
||||
//
|
||||
// A read is keyed by the trusted project, the authenticated actor, the verified
|
||||
// resource and run, and the source offset plus cutoff. Every caller-supplied
|
||||
// field is a locator, never an authorization: the host re-checks it against
|
||||
// stored host facts and the domain callbacks before a receipt is projected.
|
||||
type ProjectAuditQuery struct {
|
||||
ProjectID core.ID `json:"projectId"`
|
||||
BeforeOffset int64 `json:"beforeOffset,omitempty"`
|
||||
@@ -117,20 +135,48 @@ type ProjectAuditQuery struct {
|
||||
LinkedSourceKind string `json:"linkedSourceKind,omitempty"`
|
||||
LinkedSourceID string `json:"linkedSourceId,omitempty"`
|
||||
LinkedHostOffset int64 `json:"linkedHostOffset,omitempty"`
|
||||
// LinkedResourceKind/LinkedResourceID/LinkedRunID narrow the same exact
|
||||
// locator to the verified binding. A value that disagrees with stored host
|
||||
// state denies the read; it never widens it and never stands in for the
|
||||
// domain's current decision on that object.
|
||||
LinkedResourceKind string `json:"linkedResourceKind,omitempty"`
|
||||
LinkedResourceID string `json:"linkedResourceId,omitempty"`
|
||||
LinkedRunID string `json:"linkedRunId,omitempty"`
|
||||
// IncludeBody asks for the per-object body projection of one verified link.
|
||||
// The host asks the domain disclosure authorizer again for that object: a
|
||||
// metadata-only right serves metadata only, and a request without a verified
|
||||
// object binding is reported unsupported instead of a silently trimmed page.
|
||||
IncludeBody bool `json:"includeBody,omitempty"`
|
||||
// Export asks for a whole-history export. No base capability serves one, so
|
||||
// the host refuses instead of degrading into a partial paginated read that
|
||||
// would claim a coverage it cannot establish. See Coverage.Export.
|
||||
Export bool `json:"export,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectAuditReceiptResource names the object one verified link attributes a
|
||||
// receipt to. The host copies it from the stored binding and only when the
|
||||
// domain granted the per-object body right; it is never derived from caller
|
||||
// input and never carries resource content.
|
||||
type ProjectAuditReceiptResource struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"runId,omitempty"`
|
||||
}
|
||||
|
||||
// HostAuditReceipt is a server-projected receipt, never a raw audit payload.
|
||||
// ActorName is the name recorded with this command; old rows have no snapshot.
|
||||
// Resource appears only for a body-permitted verified link.
|
||||
type HostAuditReceipt struct {
|
||||
SourceOffset int64 `json:"sourceOffset"`
|
||||
Time time.Time `json:"time"`
|
||||
ActorID string `json:"actorId,omitempty"`
|
||||
ActorName string `json:"actorName,omitempty"`
|
||||
ActorNameStatus string `json:"actorNameStatus"`
|
||||
CommandID string `json:"commandId"`
|
||||
State string `json:"state"`
|
||||
OperationID string `json:"operationId,omitempty"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
SourceOffset int64 `json:"sourceOffset"`
|
||||
Time time.Time `json:"time"`
|
||||
ActorID string `json:"actorId,omitempty"`
|
||||
ActorName string `json:"actorName,omitempty"`
|
||||
ActorNameStatus string `json:"actorNameStatus"`
|
||||
CommandID string `json:"commandId"`
|
||||
State string `json:"state"`
|
||||
OperationID string `json:"operationId,omitempty"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
Resource *ProjectAuditReceiptResource `json:"resource,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectAuditCoverage states the exact scope of this first X2 slice. Missing
|
||||
@@ -144,6 +190,9 @@ type ProjectAuditCoverage struct {
|
||||
ModelReceipts string `json:"modelReceipts"`
|
||||
BackgroundReceipts string `json:"backgroundReceipts"`
|
||||
ResourceBodies string `json:"resourceBodies"`
|
||||
// Export states whether a whole-history export is served. An audit grant is
|
||||
// not an export grant: this slice only pages, and says so.
|
||||
Export string `json:"export"`
|
||||
}
|
||||
|
||||
type ProjectAuditPage struct {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package extension
|
||||
|
||||
// W9 X2 读取面补完(2.29.0-experimental)的公开面契约:
|
||||
// 1. 新增项全部是可选字段/新导出类型:既有 ProjectAuditHost、ProjectAuditRegistry
|
||||
// 提供者与既有查询字面量照旧编译、照旧语义(不破坏旧 provider);
|
||||
// 2. 新字段序列化带 omitempty:未使用的读取请求与旧消费者的线格式不变;
|
||||
// 3. 正文投影与导出声明是显式契约字段,未开启时不下发。
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// legacyAuditHost 是 2.14.0 形态的消费者:只实现既有方法,不感知新增可选字段。
|
||||
type legacyAuditHost struct{}
|
||||
|
||||
func (legacyAuditHost) QueryProjectAudit(context.Context, ProjectAuditQuery) (ProjectAuditPage, error) {
|
||||
return ProjectAuditPage{}, nil
|
||||
}
|
||||
|
||||
// legacyAuditRegistry 是 2.14.0 形态的注册方实现。
|
||||
type legacyAuditRegistry struct{}
|
||||
|
||||
func (legacyAuditRegistry) RegisterProjectAuditAuthorizer(ProjectAuditAuthorizer) error { return nil }
|
||||
|
||||
var (
|
||||
_ ProjectAuditHost = legacyAuditHost{}
|
||||
_ ProjectAuditRegistry = legacyAuditRegistry{}
|
||||
)
|
||||
|
||||
func TestProjectAuditReadContractIsAdditiveAndOptional(t *testing.T) {
|
||||
if SurfaceVersion < "2.29.0" {
|
||||
t.Fatalf("SurfaceVersion must record the audit read completion, got %s", SurfaceVersion)
|
||||
}
|
||||
// 既有查询字面量(只填项目与游标)仍然可用,新增字段全部零值。
|
||||
legacy := ProjectAuditQuery{ProjectID: core.ID("p1"), Limit: 10, BeforeOffset: 4, CutoffOffset: 9}
|
||||
if legacy.IncludeBody || legacy.Export || legacy.LinkedResourceID != "" || legacy.LinkedRunID != "" {
|
||||
t.Fatalf("optional fields defaulted to non-zero: %+v", legacy)
|
||||
}
|
||||
raw, err := json.Marshal(legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, unwanted := range []string{"includeBody", "export", "linkedResourceKind", "linkedResourceId", "linkedRunId"} {
|
||||
if strings.Contains(string(raw), unwanted) {
|
||||
t.Fatalf("omitted optional field %s reached the wire: %s", unwanted, raw)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{`"projectId":"p1"`, `"beforeOffset":4`, `"cutoffOffset":9`, `"limit":10`} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Fatalf("existing field %s missing from the wire form: %s", want, raw)
|
||||
}
|
||||
}
|
||||
// 显式请求时字段可见,消费端能区分“正文/导出请求”与普通分页。
|
||||
full, err := json.Marshal(ProjectAuditQuery{ProjectID: core.ID("p1"), IncludeBody: true, Export: true,
|
||||
LinkedResourceKind: "document", LinkedResourceID: "doc-1", LinkedRunID: "run-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{`"includeBody":true`, `"export":true`, `"linkedResourceKind":"document"`,
|
||||
`"linkedResourceId":"doc-1"`, `"linkedRunId":"run-1"`} {
|
||||
if !strings.Contains(string(full), want) {
|
||||
t.Fatalf("requested field %s missing: %s", want, full)
|
||||
}
|
||||
}
|
||||
// 未授予正文时回执不带 resource 字段;授予后才出现对象身份(不含正文载荷)。
|
||||
metadataOnly, err := json.Marshal(ProjectAuditPage{Receipts: []HostAuditReceipt{{SourceOffset: 3, CommandID: "rdm.confirm"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(metadataOnly), `"resource"`) {
|
||||
t.Fatalf("metadata-only receipt carried a resource: %s", metadataOnly)
|
||||
}
|
||||
withBody, err := json.Marshal(ProjectAuditPage{Receipts: []HostAuditReceipt{{SourceOffset: 3, CommandID: "rdm.confirm",
|
||||
Resource: &ProjectAuditReceiptResource{Kind: "document", ID: "doc-1", RunID: "run-1"}}},
|
||||
Coverage: ProjectAuditCoverage{ResourceBodies: "verified-exact-link-body", Export: "unsupported"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{`"resource":{"kind":"document","id":"doc-1","runId":"run-1"}`, `"export":"unsupported"`} {
|
||||
if !strings.Contains(string(withBody), want) {
|
||||
t.Fatalf("body-permitted projection missing %s: %s", want, withBody)
|
||||
}
|
||||
}
|
||||
// 四个审计哨兵互不相等:unsupported(缺基座能力)与 denied(策略拒绝)
|
||||
// 不得互相冒充。
|
||||
if !errors.Is(ErrProjectAuditUnsupported, ErrProjectAuditUnsupported) {
|
||||
t.Fatal("unsupported sentinel must stay comparable with errors.Is")
|
||||
}
|
||||
for _, distinct := range []error{ErrProjectAuditDenied, ErrProjectAuditLinkConflict, ErrProjectAuditPending} {
|
||||
if errors.Is(ErrProjectAuditUnsupported, distinct) || errors.Is(distinct, ErrProjectAuditUnsupported) {
|
||||
t.Fatalf("audit sentinels must stay distinct: %v vs %v", distinct, ErrProjectAuditUnsupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,7 @@ ProjectAuditPublisher.PublishProjectAuditLink
|
||||
ProjectAuditPublisherFactory
|
||||
ProjectAuditPublisherFactory.ForOwner
|
||||
ProjectAuditQuery
|
||||
ProjectAuditReceiptResource
|
||||
ProjectAuditRegistry
|
||||
ProjectAuditRegistry.RegisterProjectAuditAuthorizer
|
||||
ProjectContextBundle
|
||||
|
||||
Reference in New Issue
Block a user