agent-exec transport f461d74086dd6a80db72f83748f34b04/128fee1609ad
governance / governance (pull_request) Successful in 15s
ci / vet-and-test (pull_request) Successful in 1m20s

This commit is contained in:
agent-exec-transport
2026-10-22 03:41:15 +00:00
parent 275b1e234d
commit 97f60c1df6
8 changed files with 1023 additions and 18 deletions
+7 -1
View File
@@ -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")
+58 -9
View File
@@ -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 {
+99
View File
@@ -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)
}
}
}
+1
View File
@@ -110,6 +110,7 @@ ProjectAuditPublisher.PublishProjectAuditLink
ProjectAuditPublisherFactory
ProjectAuditPublisherFactory.ForOwner
ProjectAuditQuery
ProjectAuditReceiptResource
ProjectAuditRegistry
ProjectAuditRegistry.RegisterProjectAuditAuthorizer
ProjectContextBundle