Bind trusted operation IDs before domain handlers
Why: A domain outbox could not correlate its committed event with the host receipt because the bus generated the operation ID after the handler returned. Evidence: RED-to-GREEN bus and compiled-handler tests; local go test ./... -count=1 and isolated PostgreSQL operation/link checks passed as SELF. Impact: Expose a host-generated operation ID to handlers and resolve a unique committed audit offset after dispatch; missing receipts remain retryable and ambiguous rows fail closed. Refs: Stage 5 W9; approved Stage 6 audit-link design adjustment r1
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- Store write snapshots the project from the owner-matched conversation; query requires the authenticated actor's owner and that immutable project. This deliberately excludes other members' private conversations and unbound historical rows, and does not move old receipts when a conversation changes projects.
|
||||
- Command audit stores the current verified account display name at write time; old rows expose `not-recorded`. Public receipts omit raw payload and parameter digest. Cursor uses project-and-owner scoped source cutoff.
|
||||
- Initial commit `c8dd169a1fdc1451fab3b475c14f9636a7f5bf6c` exposed only the actor-owned project slice. The next local increment fixes button actor propagation and adds a trusted domain-commit link publisher, verifier callback, exact linked metadata read, current `audit.read` and per-object metadata checks, and durable idempotency. Unlinked personal conversations remain private.
|
||||
- Trusted command dispatch now creates an operation ID before calling the compiled handler and passes that ID through `extension.Request`. The handler can commit the same ID with its domain event/outbox; the final Backend audit/receipt uses it. Client `params.operationId` is ignored. An outbox link request with `HostAuditOffset=0` resolves a unique committed host row by owner, fixed project, and operation ID; a missing row returns retryable `ErrProjectAuditPending`, while duplicate rows fail closed. The separate idempotency key still controls retry equivalence.
|
||||
- Exact-source linked reads do not enumerate hidden links or reveal raw payload, private conversation ID, or content. Body disclosure and a broad project link list are not implemented; the public coverage reports the narrower capability.
|
||||
|
||||
## Verification and remaining work
|
||||
@@ -21,5 +22,6 @@
|
||||
- SELF receipt `/tmp/backend-w9-build-20261014-01.json`: `CGO_ENABLED=0 go build ./cmd/agent-system`, exit 0, run on that pre-commit tree. Generated local binary removed after the build.
|
||||
- SELF PostgreSQL receipt `/tmp/backend-w9-pg-cutoff-20261014-01.json`: isolated PostgreSQL 18.6 instance on port 55442 reproduced a low-ID transaction committing after a higher visible ID. `/tmp/backend-w9-pg-link-20261014-02.json` verifies the new link table, retry/conflict behavior, and the cutoff counterexample against isolated schemas. Both exited 0. The current PG cutoff means only `offset-upper-bound-only`; it is **not** a fixed historical visibility snapshot or safe incremental cursor.
|
||||
- SELF PostgreSQL receipt `/tmp/backend-w9-pg-migrate-20261014-01.json`: isolated SQLite→PostgreSQL migration preserved one linked receipt and advanced its target sequence; the same migration ID replayed idempotently. The temporary PG instance was stopped and removed after verification.
|
||||
- SELF PostgreSQL receipt `/tmp/backend-w9-pg-operation-20261014-01.json`: a fresh isolated PG instance reran the link lookup, idempotency/conflict, and low-ID late-commit checks after trusted operation-offset resolution was added. The instance was stopped and removed.
|
||||
- Integration constraint: this branch uses schema v31 (audit attribution) and v32 (links), while the independent W6 Backend branch also has a local v31. Neither branch has consumed the other's unmerged work. After the first branch lands, the later branch owner must sync the new main, renumber its unpublished migrations without rewriting an already released version, and rerun fresh/old SQLite and PG migration plus combined regression on the actual merged source. These isolated tests do not prove W6+W9 compatibility.
|
||||
- Required next W9 work: add a committed-order waterline or materialize the authorized ID set at the first read, then prove with PG low-ID late commits that history and cross-page incremental reads neither omit nor duplicate operations. The Backend project-event source, actual RDM committed-event verifier/outbox consumer, run ID propagation from command dispatch, model/tool/background receipts, body/detail disclosure, broad authorized list pagination, history/export, and combined product verification remain unfinished. The exact-link slice is not Stage 7 audit acceptance.
|
||||
- Required next W9 work: add a committed-order waterline or materialize the authorized ID set at the first read, then prove with PG low-ID late commits that history and cross-page incremental reads neither omit nor duplicate operations. The Backend project-event source, actual RDM committed-event verifier/outbox consumer, run ID propagation from command dispatch, model/tool/background receipts, body/detail disclosure, broad authorized list pagination, history/export, and combined product verification remain unfinished. A domain commit followed by a lost Backend audit append leaves the link pending; recovery must retain and reconcile that unknown state without claiming a host receipt. The exact-link slice is not Stage 7 audit acceptance.
|
||||
|
||||
@@ -67,7 +67,7 @@ func (r pluginRegistry) AddCommandHandler(spec json.RawMessage, h extension.Hand
|
||||
host := inprocHost{caps: r.caps, pluginID: r.pluginID, owner: req.Owner, actor: req.Actor}
|
||||
return h(ctx, host, extension.Request{
|
||||
ID: req.ID, Params: req.Params, Entrance: req.Entrance,
|
||||
Actor: req.Actor, Owner: req.Owner, ConversationID: req.ConversationID,
|
||||
Actor: req.Actor, Owner: req.Owner, ConversationID: req.ConversationID, OperationID: req.OperationID,
|
||||
HostProjectID: req.HostProjectID,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -128,9 +128,22 @@ func (h inprocHost) PublishProjectAuditLink(ctx context.Context, req extension.P
|
||||
if holder != h.pluginID || verify == nil {
|
||||
return extension.ProjectAuditLinkReceipt{}, extension.ErrProjectAuditUnsupported
|
||||
}
|
||||
if h.owner == "" || (h.actor != "" && h.actor != h.owner) || req.HostAuditOffset <= 0 || req.ProjectID == "" || req.SourceKind != "domain-commit" || req.SourceID == "" || req.OperationID == "" || req.ResourceKind == "" || req.ResourceID == "" || req.DisclosureClass != "metadata" || req.IdempotencyKey == "" {
|
||||
if h.owner == "" || (h.actor != "" && h.actor != h.owner) || req.HostAuditOffset < 0 || req.ProjectID == "" || req.SourceKind != "domain-commit" || req.SourceID == "" || req.OperationID == "" || req.ResourceKind == "" || req.ResourceID == "" || req.DisclosureClass != "metadata" || req.IdempotencyKey == "" {
|
||||
return extension.ProjectAuditLinkReceipt{}, extension.ErrProjectAuditDenied
|
||||
}
|
||||
if req.HostAuditOffset == 0 {
|
||||
resolved, err := h.caps.st.CommittedAuditOffset(h.owner, string(req.ProjectID), req.OperationID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return extension.ProjectAuditLinkReceipt{}, extension.ErrProjectAuditPending
|
||||
}
|
||||
if errors.Is(err, store.ErrConflict) {
|
||||
return extension.ProjectAuditLinkReceipt{}, extension.ErrProjectAuditLinkConflict
|
||||
}
|
||||
if err != nil {
|
||||
return extension.ProjectAuditLinkReceipt{}, err
|
||||
}
|
||||
req.HostAuditOffset = resolved
|
||||
}
|
||||
row, projectID, err := h.caps.st.CommandAuditForLink(h.owner, req.HostAuditOffset)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return extension.ProjectAuditLinkReceipt{}, extension.ErrProjectAuditDenied
|
||||
|
||||
@@ -73,12 +73,13 @@ func TestProjectAuditLinkSharesOnlyVerifiedCommittedMetadata(t *testing.T) {
|
||||
t.Fatalf("unlinked private audit: %v", err)
|
||||
}
|
||||
alicePublisher := inprocHost{caps: caps, pluginID: "rdm", owner: "alice"} // trusted outbox-scoped host
|
||||
linkReq := extension.ProjectAuditLinkRequest{ProjectID: p.ID, HostAuditOffset: offset, SourceKind: "domain-commit", SourceID: "commit-1", OperationID: "op-1", ResourceKind: "document", ResourceID: "doc-1", DisclosureClass: "metadata", IdempotencyKey: "outbox-1"}
|
||||
linkReq := extension.ProjectAuditLinkRequest{ProjectID: p.ID, SourceKind: "domain-commit", SourceID: "commit-1", OperationID: "op-1", ResourceKind: "document", ResourceID: "doc-1", DisclosureClass: "metadata", IdempotencyKey: "outbox-1"}
|
||||
if rows, err := bob.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: p.ID}); err != nil || len(rows.Receipts) != 0 {
|
||||
t.Fatalf("private owner row visible without exact link: %+v %v", rows, err)
|
||||
}
|
||||
wrongProject := linkReq
|
||||
wrongProject.ProjectID = otherProject.ID
|
||||
wrongProject.HostAuditOffset = offset
|
||||
if _, err := alicePublisher.PublishProjectAuditLink(context.Background(), wrongProject); !errors.Is(err, extension.ErrProjectAuditDenied) {
|
||||
t.Fatalf("wrong host project: %v", err)
|
||||
}
|
||||
@@ -90,10 +91,18 @@ func TestProjectAuditLinkSharesOnlyVerifiedCommittedMetadata(t *testing.T) {
|
||||
if _, err := (inprocHost{caps: caps, pluginID: "other", owner: "alice"}).PublishProjectAuditLink(context.Background(), linkReq); !errors.Is(err, extension.ErrProjectAuditUnsupported) {
|
||||
t.Fatalf("other plugin: %v", err)
|
||||
}
|
||||
pending := linkReq
|
||||
pending.OperationID, pending.IdempotencyKey = "op-not-yet-recorded", "outbox-pending"
|
||||
if _, err := alicePublisher.PublishProjectAuditLink(context.Background(), pending); !errors.Is(err, extension.ErrProjectAuditPending) {
|
||||
t.Fatalf("host receipt race must be retryable: %v", err)
|
||||
}
|
||||
first, err := alicePublisher.PublishProjectAuditLink(context.Background(), linkReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.HostAuditOffset != offset {
|
||||
t.Fatalf("trusted resolved offset: got %d want %d", first.HostAuditOffset, offset)
|
||||
}
|
||||
retry, err := alicePublisher.PublishProjectAuditLink(context.Background(), linkReq)
|
||||
if err != nil || retry.LinkID != first.LinkID {
|
||||
t.Fatalf("idempotent retry: first=%+v retry=%+v err=%v", first, retry, err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"agent-system-backend/internal/cmdbus"
|
||||
"agent-system-backend/internal/store"
|
||||
"agent-system-backend/internal/surfacesvc"
|
||||
"agent-system-backend/pkg/extension"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
func TestSurfaceButtonDispatchUsesAuthenticatedActor(t *testing.T) {
|
||||
@@ -48,3 +51,61 @@ func TestSurfaceButtonDispatchUsesAuthenticatedActor(t *testing.T) {
|
||||
t.Fatalf("button audit actor: %+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompiledHandlerReceivesHostOperationID(t *testing.T) {
|
||||
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "audit.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer st.Close()
|
||||
p, err := st.CreateProject("alice", "A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := st.CreateConversation("alice", string(p.ID), "work", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bus := cmdbus.New(st, nil, func() time.Time { return time.Now().UTC() })
|
||||
slot := &projectAuditSlot{}
|
||||
caps := hostcaps{st: st, projectAudit: slot}
|
||||
reg := pluginRegistry{compiledRegistry: compiledRegistry{bus: bus}, pluginID: "rdm", caps: caps}
|
||||
var seen string
|
||||
if err := reg.AddCommandHandler(json.RawMessage(`{"id":"rdm.confirm","title":"Confirm"}`), func(_ context.Context, _ extension.Host, req extension.Request) (any, error) {
|
||||
seen = req.OperationID
|
||||
return "domain-committed", nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp := bus.Dispatch(context.Background(), cmdbus.Request{ID: "rdm.confirm", Actor: "alice", Owner: "alice", ConversationID: string(c.ID)})
|
||||
rows, _, _, err := st.ListProjectCommandAudits("alice", string(p.ID), 0, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if seen == "" || resp.Receipt.OperationID != seen || len(rows) != 1 || rows[0].Envelope.OperationID != seen {
|
||||
t.Fatalf("handler/domain correlation: seen=%q receipt=%+v rows=%+v", seen, resp.Receipt, rows)
|
||||
}
|
||||
if err := reg.RegisterProjectAuditAuthorizer(func(actor string, project core.ID) (bool, error) { return actor == "bob" && project == p.ID, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reg.RegisterProjectAuditLinkVerifier(func(_ context.Context, proof extension.ProjectAuditLinkProof) (bool, error) {
|
||||
return proof.Owner == "alice" && proof.ProjectID == p.ID && proof.OperationID == seen && proof.SourceID == "domain-commit-1" && proof.ResourceID == "doc-1", nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reg.RegisterProjectAuditDisclosureAuthorizer(func(_ context.Context, actor string, r extension.ProjectAuditLinkedResource) (extension.ProjectAuditDisclosure, error) {
|
||||
return extension.ProjectAuditDisclosure{Metadata: actor == "bob" && r.ResourceID == "doc-1"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publisher := inprocHost{caps: caps, owner: "alice", pluginID: "rdm"}
|
||||
linked, err := publisher.PublishProjectAuditLink(context.Background(), extension.ProjectAuditLinkRequest{ProjectID: p.ID, OperationID: seen, SourceKind: "domain-commit", SourceID: "domain-commit-1", ResourceKind: "document", ResourceID: "doc-1", DisclosureClass: "metadata", IdempotencyKey: "outbox-1"})
|
||||
if err != nil || linked.HostAuditOffset != rows[0].Offset {
|
||||
t.Fatalf("publish by trusted operation: %+v %v", linked, err)
|
||||
}
|
||||
bob := inprocHost{caps: caps, owner: "bob", actor: "bob", pluginID: "rdm"}
|
||||
page, err := bob.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: p.ID, LinkedSourceKind: "domain-commit", LinkedSourceID: "domain-commit-1", LinkedHostOffset: linked.HostAuditOffset})
|
||||
if err != nil || len(page.Receipts) != 1 || page.Receipts[0].OperationID != seen {
|
||||
t.Fatalf("linked read: %+v %v", page, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ type Request struct {
|
||||
Actor string
|
||||
Owner string
|
||||
ConversationID string
|
||||
OperationID string // overwritten by the trusted bus before handler execution
|
||||
AnchorMessageID string // 确认卡锚定的发起消息(内联渲染)
|
||||
Confirmed bool // 危险命令经确认卡放行后的执行
|
||||
HostProjectID string // EXT-18:分发所属基座项目(空=无项目上下文)
|
||||
@@ -279,12 +280,15 @@ func (b *Bus) Dispatch(ctx context.Context, req Request) Response {
|
||||
if !ok {
|
||||
return receipt(agentcommand.StateRejected, "", "no host handler bound for "+req.ID)
|
||||
}
|
||||
// The domain handler can persist this trusted ID with its transaction/outbox.
|
||||
// Never accept an ID from Params or the incoming Request. A handler failure
|
||||
// still writes only a rejected audit receipt with no committed operation ID.
|
||||
req.OperationID = fmt.Sprintf("op_%s_%d", strings.ReplaceAll(req.ID, ".", "_"), b.now().UnixNano())
|
||||
payload, err := h(ctx, req)
|
||||
if err != nil {
|
||||
return receipt(agentcommand.StateRejected, "", "rejected: "+err.Error())
|
||||
}
|
||||
op := fmt.Sprintf("op_%s_%d", strings.ReplaceAll(strings.TrimPrefix(req.ID, ""), ".", "_"), b.now().UnixNano())
|
||||
return Response{Receipt: b.auditReceipt(req, agentcommand.StateCommitted, op, "", start), Payload: payload}
|
||||
return Response{Receipt: b.auditReceipt(req, agentcommand.StateCommitted, req.OperationID, "", start), Payload: payload}
|
||||
}
|
||||
|
||||
// Confirm 确认卡回道:actionID=confirm 执行挂起请求(entrance 归一 card),其余视为取消。
|
||||
|
||||
@@ -87,6 +87,34 @@ func TestDispatchFourEntrancesSameAudit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerReceivesCommittedOperationIDBeforeDomainWrite(t *testing.T) {
|
||||
audit := &memAudit{}
|
||||
bus := New(audit, nil, func() time.Time { return time.Unix(1700000000, 123).UTC() })
|
||||
var seen string
|
||||
if err := bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "rdm.confirm", Title: "Confirm"}, "", func(_ context.Context, req Request) (any, error) {
|
||||
seen = req.OperationID
|
||||
return "committed-domain-event", nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp := bus.Dispatch(context.Background(), Request{ID: "rdm.confirm", Actor: "alice", Owner: "alice", Params: map[string]any{"operationId": "forged-client-id"}})
|
||||
if seen == "" || seen == "forged-client-id" || resp.Receipt.OperationID != seen || len(audit.logs) != 1 || audit.logs[0].OperationID != seen {
|
||||
t.Fatalf("operation correlation missing: handler=%q receipt=%+v audit=%+v", seen, resp.Receipt, audit.logs)
|
||||
}
|
||||
if err := bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "rdm.fail", Title: "Fail"}, "", func(_ context.Context, req Request) (any, error) {
|
||||
if req.OperationID == "" {
|
||||
t.Fatal("handler must receive a tentative host operation ID")
|
||||
}
|
||||
return nil, errors.New("domain transaction rejected")
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
failed := bus.Dispatch(context.Background(), Request{ID: "rdm.fail", Actor: "alice", Owner: "alice"})
|
||||
if failed.Receipt.State != agentcommand.StateRejected || failed.Receipt.OperationID != "" || len(audit.logs) != 2 || audit.logs[1].OperationID != "" {
|
||||
t.Fatalf("failed handler was marked committed: %+v audits=%+v", failed.Receipt, audit.logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchUnknownAndBadParams(t *testing.T) {
|
||||
a := &memAudit{}
|
||||
b := newTestBus(a, &memCards{})
|
||||
|
||||
@@ -23,6 +23,38 @@ type ProjectAuditLink struct {
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
// CommittedAuditOffset resolves exactly one persisted host receipt. A domain
|
||||
// outbox can have the operation ID before the bus appends its audit row; no row
|
||||
// yet is a retryable pending state, while duplicates are ambiguous and refused.
|
||||
func (s *Store) CommittedAuditOffset(owner, projectID, operationID string) (int64, error) {
|
||||
if owner == "" || projectID == "" || operationID == "" {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
rows, err := s.db.Query(s.q(`SELECT id FROM command_audit WHERE owner=? AND project_id=? AND operation_id=? AND state='committed' ORDER BY id LIMIT 2`), owner, projectID, operationID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ids []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
if len(ids) > 1 {
|
||||
return 0, ErrConflict
|
||||
}
|
||||
return ids[0], nil
|
||||
}
|
||||
|
||||
func (s *Store) CommandAuditForLink(owner string, offset int64) (ProjectCommandAuditRow, string, error) {
|
||||
var row ProjectCommandAuditRow
|
||||
var projectID, raw string
|
||||
|
||||
@@ -32,6 +32,13 @@ func TestProjectAuditLinkStoreIdempotenceAndExactScope(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolved, err := st.CommittedAuditOffset("alice", string(a.ID), "op-1")
|
||||
if err != nil || resolved != rows[0].Offset {
|
||||
t.Fatalf("trusted operation lookup: %d %v", resolved, err)
|
||||
}
|
||||
if _, err := st.CommittedAuditOffset("alice", string(a.ID), "op-pending"); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("unrecorded operation should wait: %v", err)
|
||||
}
|
||||
link := ProjectAuditLink{HostAuditOffset: rows[0].Offset, ProjectID: string(a.ID), Owner: "alice", SourceKind: "domain-commit", SourceID: "commit-1", OperationID: "op-1", ResourceKind: "document", ResourceID: "doc-1", DisclosureClass: "metadata", IdempotencyKey: "outbox-1"}
|
||||
first, err := st.SaveProjectAuditLink(link)
|
||||
if err != nil {
|
||||
@@ -81,4 +88,10 @@ func TestProjectAuditLinkStoreIdempotenceAndExactScope(t *testing.T) {
|
||||
if _, err := st.SaveProjectAuditLink(bobLink); err != nil {
|
||||
t.Fatalf("another owner may use the same outbox key: %v", err)
|
||||
}
|
||||
if err := st.AppendAudit("alice", agentcommand.AuditEnvelope{Time: time.Now(), Actor: "alice", CommandID: "rdm.confirm", OperationID: "op-1", ConversationID: string(c.ID), State: agentcommand.StateCommitted}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.CommittedAuditOffset("alice", string(a.ID), "op-1"); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("ambiguous host operation must not choose one: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ import (
|
||||
// project-bound command-audit read slice and domain audit.read callback).
|
||||
// 2.14.0-experimental: X2 optional committed project-audit link publisher,
|
||||
// verifier, disclosure callback, and exact linked receipt read.
|
||||
const SurfaceVersion = "2.14.0-experimental"
|
||||
// 2.15.0-experimental: trusted Request.OperationID before handler execution and
|
||||
// retryable ProjectAuditLinkRequest.HostAuditOffset=0 resolution.
|
||||
const SurfaceVersion = "2.15.0-experimental"
|
||||
|
||||
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
|
||||
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
|
||||
|
||||
@@ -14,6 +14,9 @@ type Request struct {
|
||||
Actor string
|
||||
Owner string
|
||||
ConversationID string
|
||||
// OperationID is generated by the trusted host bus before this handler runs.
|
||||
// Persist it with an actual domain commit/outbox; never treat it as a retry key.
|
||||
OperationID string
|
||||
// HostProjectID 本次分发所属基座项目(EXT-18 可选上下文;空=无项目
|
||||
// 上下文——API 入口无对话或对话未关联项目时为零值)。下游按显式映射
|
||||
// 解析(如 RDM rdm_host_project_map),不得据此替代自身授权判定。
|
||||
|
||||
@@ -23,11 +23,15 @@ var (
|
||||
ErrProjectAuditDenied = errors.New("extension: project audit denied")
|
||||
ErrProjectAuditUnsupported = errors.New("extension: project audit unsupported")
|
||||
ErrProjectAuditLinkConflict = errors.New("extension: project audit link conflict")
|
||||
ErrProjectAuditPending = errors.New("extension: committed host audit receipt pending")
|
||||
)
|
||||
|
||||
// ProjectAuditLinkRequest binds one host command receipt to an already
|
||||
// committed domain fact. Owner is never caller supplied; the host handle and
|
||||
// stored audit row establish it. This first slice supports domain-commit only.
|
||||
// stored audit row establish it. HostAuditOffset=0 asks the host to resolve a
|
||||
// unique committed row by its trusted owner/project/OperationID, which lets an
|
||||
// outbox wait for the bus audit commit after the domain handler returns. This
|
||||
// first slice supports domain-commit only.
|
||||
type ProjectAuditLinkRequest struct {
|
||||
ProjectID core.ID `json:"projectId"`
|
||||
HostAuditOffset int64 `json:"hostAuditOffset"`
|
||||
|
||||
@@ -10,6 +10,7 @@ Compiled
|
||||
ErrNotLinked
|
||||
ErrProjectAuditDenied
|
||||
ErrProjectAuditLinkConflict
|
||||
ErrProjectAuditPending
|
||||
ErrProjectAuditUnsupported
|
||||
Handler
|
||||
Host
|
||||
|
||||
Reference in New Issue
Block a user