feat(events): add FAV subscriber shadow without live writes
ObserveShadow computes intended outcome only and refuses FAV_DEGRADED_READ; HandleLease and Register(fav) stay fail-closed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,9 +32,13 @@ func main() {
|
|||||||
"full recover-plan / recover-apply / gc-preflight / destroy",
|
"full recover-plan / recover-apply / gc-preflight / destroy",
|
||||||
"server ingest hooks (OnFinalArtifact*)",
|
"server ingest hooks (OnFinalArtifact*)",
|
||||||
"durable fav event Register after separate approval",
|
"durable fav event Register after separate approval",
|
||||||
|
"live staging → artifact → publication (each ≥7d observation)",
|
||||||
},
|
},
|
||||||
"source_branch": "feature/final-artifact-vault",
|
"shadow_observe": true,
|
||||||
"note": "do not enable FAV_DEGRADED_READ or fav consumer without approval",
|
"would_stage": false,
|
||||||
|
"would_publish": false,
|
||||||
|
"source_branch": "feature/final-artifact-vault",
|
||||||
|
"note": "do not enable FAV_DEGRADED_READ or fav consumer without approval; subscriber shadow is read-only",
|
||||||
})
|
})
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
usage()
|
usage()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ package fav
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"chat-agent-system/internal/artifactvault"
|
"chat-agent-system/internal/artifactvault"
|
||||||
"chat-agent-system/internal/events"
|
"chat-agent-system/internal/events"
|
||||||
@@ -24,6 +26,8 @@ type EventConsumer struct {
|
|||||||
// Enabled must stay false by default. Even when true, events.Registry
|
// Enabled must stay false by default. Even when true, events.Registry
|
||||||
// still rejects Register(ConsumerFAV) until the hard gate is lifted.
|
// still rejects Register(ConsumerFAV) until the hard gate is lifted.
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
// AllowDegradedRead must stay false. Shadow refuses if this is true.
|
||||||
|
AllowDegradedRead bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleLease refuses work while the subscriber is disabled (default).
|
// HandleLease refuses work while the subscriber is disabled (default).
|
||||||
@@ -44,3 +48,46 @@ func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx
|
|||||||
func DefaultEnabled() bool {
|
func DefaultEnabled() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntendedOutcome is a read-only shadow calculation. It must never be treated
|
||||||
|
// as a write receipt; WouldStage/WouldPublish stay false in this slice.
|
||||||
|
type IntendedOutcome struct {
|
||||||
|
EventName string
|
||||||
|
AggregateType string
|
||||||
|
AggregateID string
|
||||||
|
LogicalResource string
|
||||||
|
WouldStage bool
|
||||||
|
WouldPublish bool
|
||||||
|
DegradedReadUsed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObserveShadow computes the intended FAV outcome without business side effects.
|
||||||
|
// FAV_DEGRADED_READ must stay false; live staging/artifact/publication is not opened.
|
||||||
|
func (c *EventConsumer) ObserveShadow(lease events.Lease) (IntendedOutcome, error) {
|
||||||
|
out := IntendedOutcome{
|
||||||
|
EventName: lease.EventName,
|
||||||
|
AggregateType: lease.AggregateType,
|
||||||
|
AggregateID: lease.AggregateID,
|
||||||
|
LogicalResource: strings.TrimSpace(lease.AggregateType) + ":" + strings.TrimSpace(lease.AggregateID),
|
||||||
|
}
|
||||||
|
if c == nil {
|
||||||
|
return out, fmt.Errorf("%w: fav", events.ErrDisabledConsumer)
|
||||||
|
}
|
||||||
|
if c.AllowDegradedRead || envTruthy("FAV_DEGRADED_READ") {
|
||||||
|
return out, fmt.Errorf("fav shadow refuses FAV_DEGRADED_READ")
|
||||||
|
}
|
||||||
|
out.WouldStage = false
|
||||||
|
out.WouldPublish = false
|
||||||
|
out.DegradedReadUsed = false
|
||||||
|
_ = c.Publisher // never Drain / Stage / Publish here
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envTruthy(key string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,3 +33,36 @@ func TestFavSubscriberStillDisabledWhenFlagOnWithoutRegistryApproval(t *testing.
|
|||||||
t.Fatal("Registry.Register(fav) must remain fail-closed")
|
t.Fatal("Registry.Register(fav) must remain fail-closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFavSubscriberShadowComputesIntendedOutcomeWithoutWrites(t *testing.T) {
|
||||||
|
t.Setenv("FAV_DEGRADED_READ", "false")
|
||||||
|
t.Setenv("FAV_PUBLISHER_ENABLED", "false")
|
||||||
|
c := &fav.EventConsumer{Enabled: false, Env: "dev"}
|
||||||
|
out, err := c.ObserveShadow(events.Lease{
|
||||||
|
EventName: "document.finalized", AggregateType: "document", AggregateID: "doc_1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out.WouldStage || out.WouldPublish || out.DegradedReadUsed {
|
||||||
|
t.Fatalf("shadow must not claim writes: %+v", out)
|
||||||
|
}
|
||||||
|
if out.LogicalResource != "document:doc_1" {
|
||||||
|
t.Fatalf("logical=%s", out.LogicalResource)
|
||||||
|
}
|
||||||
|
err = c.HandleLease(context.Background(), events.Lease{}, nil)
|
||||||
|
if !errors.Is(err, events.ErrDisabledConsumer) {
|
||||||
|
t.Fatalf("HandleLease still disabled: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AllowDegradedRead = true
|
||||||
|
if _, err := c.ObserveShadow(events.Lease{}); err == nil {
|
||||||
|
t.Fatal("shadow must refuse degraded read")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AllowDegradedRead = false
|
||||||
|
t.Setenv("FAV_DEGRADED_READ", "true")
|
||||||
|
if _, err := c.ObserveShadow(events.Lease{AggregateType: "document", AggregateID: "doc_2"}); err == nil {
|
||||||
|
t.Fatal("shadow must refuse process env FAV_DEGRADED_READ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ func (s *Server) handleVaultStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
"degraded_read": degraded,
|
"degraded_read": degraded,
|
||||||
"store_configured": storeReady,
|
"store_configured": storeReady,
|
||||||
"subscriber_default": false,
|
"subscriber_default": false,
|
||||||
"note": "fav consumer remains OFF until separate approval; full FAV branch not merged",
|
"shadow_observe": true,
|
||||||
|
"would_stage": false,
|
||||||
|
"would_publish": false,
|
||||||
|
"live_not_opened": true,
|
||||||
|
"note": "fav consumer remains OFF until separate approval; shadow computes intended outcome only; full FAV branch not merged",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ func TestVaultStatusReportsOff(t *testing.T) {
|
|||||||
if body["degraded_read"] != false {
|
if body["degraded_read"] != false {
|
||||||
t.Fatalf("degraded_read=%v", body["degraded_read"])
|
t.Fatalf("degraded_read=%v", body["degraded_read"])
|
||||||
}
|
}
|
||||||
|
if body["shadow_observe"] != true {
|
||||||
|
t.Fatalf("shadow_observe=%v", body["shadow_observe"])
|
||||||
|
}
|
||||||
|
if body["would_stage"] != false || body["would_publish"] != false {
|
||||||
|
t.Fatalf("shadow must not claim writes: %+v", body)
|
||||||
|
}
|
||||||
|
if body["live_not_opened"] != true {
|
||||||
|
t.Fatalf("live_not_opened=%v", body["live_not_opened"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateIndependentVaultRejectsDataRoot(t *testing.T) {
|
func TestValidateIndependentVaultRejectsDataRoot(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user