diff --git a/cmd/artifact-vault/main.go b/cmd/artifact-vault/main.go new file mode 100644 index 00000000..771a992d --- /dev/null +++ b/cmd/artifact-vault/main.go @@ -0,0 +1,57 @@ +// Command artifact-vault is the FAV operator CLI stub for U7 incremental. +// +// Full backfill / inventory / recover-apply / destroy live on +// feature/final-artifact-vault and were NOT whole-branch merged. This binary +// compiles on current dev and stays non-destructive: it only prints status / +// remaining-port inventory. +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + switch os.Args[1] { + case "status", "version": + writeJSON(map[string]any{ + "binary": "artifact-vault", + "slice": "u7-incremental-stub", + "publisher_default": false, + "degraded_read": false, + "consumer": "fav", + "consumer_mode": "off", + "remaining_to_port": []string{ + "enumerate.go (EnumerateTerminals / LoadExclusions)", + "TerminalRow + Backfill(rows) API (diverged from dataset Candidate path)", + "full recover-plan / recover-apply / gc-preflight / destroy", + "server ingest hooks (OnFinalArtifact*)", + "durable fav event Register after separate approval", + }, + "source_branch": "feature/final-artifact-vault", + "note": "do not enable FAV_DEGRADED_READ or fav consumer without approval", + }) + case "help", "-h", "--help": + usage() + default: + fmt.Fprintf(os.Stderr, "artifact-vault: unknown command %q (full CLI remains on feature/final-artifact-vault)\n", os.Args[1]) + usage() + os.Exit(2) + } +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage: artifact-vault ") + fmt.Fprintln(os.Stderr, "U7 stub only — backfill/recover/destroy not ported; fav stays off") +} + +func writeJSON(v any) { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + _ = enc.Encode(v) +} diff --git a/cmd/events/main.go b/cmd/events/main.go index f9d8ebfe..a14fef81 100644 --- a/cmd/events/main.go +++ b/cmd/events/main.go @@ -83,6 +83,8 @@ func main() { (&docproject.EventConsumer{Docs: gov, Env: cfg.Env}).HandleLease, (&intentflow.ProjectionConsumer{Env: cfg.Env}).HandleLease, )) + // Intentionally NOT registering events.ConsumerFAV / internal/app/fav. + // Registry.Register("fav") fails closed; mode stays off until separate approval. mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "env": cfg.Env, "owner": owner}) diff --git a/internal/app/fav/subscriber.go b/internal/app/fav/subscriber.go new file mode 100644 index 00000000..84c38715 --- /dev/null +++ b/internal/app/fav/subscriber.go @@ -0,0 +1,46 @@ +// Package fav holds the durable Final Artifact Vault event subscriber stub. +// +// U7 incremental rule: the fav consumer stays mode=off and unregistered until +// a separate approval. Registry.Register("fav") and SetMode(fav, live) already +// fail closed in internal/events; this package must not bypass those gates. +package fav + +import ( + "context" + "fmt" + + "chat-agent-system/internal/artifactvault" + "chat-agent-system/internal/events" + + "github.com/jackc/pgx/v5" +) + +// EventConsumer is the durable FAV publisher subscriber. +// Enabled defaults to false (zero value). Do not register into cmd/events +// until separate cutover approval. +type EventConsumer struct { + Publisher *artifactvault.Publisher + Env string + // Enabled must stay false by default. Even when true, events.Registry + // still rejects Register(ConsumerFAV) until the hard gate is lifted. + Enabled bool +} + +// HandleLease refuses work while the subscriber is disabled (default). +// Even when Enabled is flipped locally, the stub still refuses until the +// events registry hard-gate and a separate cutover approval are both lifted. +func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx pgx.Tx) error { + _ = ctx + _ = lease + _ = tx + if c == nil || !c.Enabled { + return fmt.Errorf("%w: fav", events.ErrDisabledConsumer) + } + _ = c.Publisher + return fmt.Errorf("%w: fav publisher cutover not approved", events.ErrDisabledConsumer) +} + +// DefaultEnabled reports the compile-time / zero-value default for the subscriber. +func DefaultEnabled() bool { + return false +} diff --git a/internal/app/fav/subscriber_test.go b/internal/app/fav/subscriber_test.go new file mode 100644 index 00000000..a08485ca --- /dev/null +++ b/internal/app/fav/subscriber_test.go @@ -0,0 +1,35 @@ +package fav_test + +import ( + "context" + "errors" + "testing" + + "chat-agent-system/internal/app/fav" + "chat-agent-system/internal/events" +) + +func TestFavSubscriberDefaultsDisabled(t *testing.T) { + if fav.DefaultEnabled() { + t.Fatal("fav subscriber DefaultEnabled must be false") + } + c := &fav.EventConsumer{} + if c.Enabled { + t.Fatal("zero-value EventConsumer.Enabled must be false") + } + err := c.HandleLease(context.Background(), events.Lease{}, nil) + if !errors.Is(err, events.ErrDisabledConsumer) { + t.Fatalf("disabled HandleLease: %v", err) + } +} + +func TestFavSubscriberStillDisabledWhenFlagOnWithoutRegistryApproval(t *testing.T) { + c := &fav.EventConsumer{Enabled: true, Env: "dev"} + err := c.HandleLease(context.Background(), events.Lease{Environment: "dev"}, nil) + if !errors.Is(err, events.ErrDisabledConsumer) { + t.Fatalf("enabled stub must still refuse until cutover approved: %v", err) + } + if err := events.NewRegistry().Register(events.ConsumerFAV); err == nil { + t.Fatal("Registry.Register(fav) must remain fail-closed") + } +}