carlos / platform Public

release: system/<name>.json, a typed deployment settings record

Deployment-wide policy the platform reads and acts on, as against
features.json, which is per-app and holds switches the platform
deliberately never interprets. Different thing, different object.

A typed record per feature rather than one generic settings map. A map
would be features.json by another name — uninterpreted, and impossible
to schema-pin meaningfully. system/pdf.json later is a new type and a
new door, not a new magic string.

The schema is pinned and an unknown one refuses, the rule
objstore.Record and ScanSidecar already follow. It matters more here:
this record decides whether the deployment reads tenant object bytes, so
a shape we cannot read is not one to guess at.

Two things the tests pin because getting them wrong fails quietly.
Absent means NOT ENABLED — a deployment that never ran enable does not
scan — which is why the upgrade needs a real seeding step rather than a
default. And AntivirusEnabled does not turn an error into "disabled": a
caller acting on false would silently stop scanning on a transient
bucket failure or a schema it could not read.

Create-only conflicts against an existing key, which is what will make
the boot seed idempotent across two consoles and a restart loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 2af9045e8cc78e170727dfef3d2ef57f0e072c40 parent 1287c30
2 files changed, +195 −0
  • internal/release/system.go +95 −0
  • internal/release/system_test.go +100 −0
diff --git a/internal/release/system.go b/internal/release/system.go
new file mode 100644
index 000000000..8491a3a2a
--- /dev/null
+++ b/internal/release/system.go
@@ -0,0 +1,95 @@
+// Deployment-wide settings records: system/<name>.json.
+//
+// These are NOT features.json. That one is per-app and holds switches the
+// platform "stores and serves but never interprets" (apifeatures.go) — an
+// app's own vocabulary, passed through. These the platform reads and acts on,
+// so each gets a typed shape and a pinned schema.
+//
+// A typed record per feature rather than one generic settings map, for the
+// same reason: a map would be features.json by another name, uninterpreted and
+// impossible to schema-pin meaningfully. system/pdf.json later is a new type
+// and a new door, not a new magic string.
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+)
+
+// AntivirusSchema is pinned. An unknown schema refuses rather than guessing —
+// the rule objstore.Record and ScanSidecar already follow, and it matters more
+// here: this record decides whether the deployment reads tenant object bytes.
+const AntivirusSchema = 1
+
+// SystemKey is where a deployment-wide settings record lives.
+func SystemKey(name string) string { return "system/" + name + ".json" }
+
+// AntivirusKey is the object this package's typed helpers read and write.
+const AntivirusName = "antivirus"
+
+// AntivirusSettings is whether object scanning is on for this deployment.
+//
+// Policy, not wiring. Where clamd lives stays in the unit file, because those
+// are coordinates a console must not guess; whether scanning is ON lives here,
+// so turning a compliance control on does not mean editing a systemd unit and
+// restarting the control plane.
+type AntivirusSettings struct {
+ Schema int `json:"schema"`
+ Enabled bool `json:"enabled"`
+ Engine string `json:"engine"`
+ At time.Time `json:"at"`
+ By string `json:"by"`
+}
+
+// GetAntivirus reads the record. Absent is (nil, "", nil) — no record is a
+// state, not an error, the same idiom objstore.Get uses.
+//
+// Absent means NOT ENABLED. A deployment that has never run `carlos system
+// antivirus enable` does not scan, which is what makes the migration in
+// cmd/console worth its own seeding step rather than a default.
+func GetAntivirus(ctx context.Context, s Store) (*AntivirusSettings, string, error) {
+ raw, etag, err := s.Get(ctx, SystemKey(AntivirusName))
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, "", nil
+ }
+ return nil, "", err
+ }
+ var av AntivirusSettings
+ if err := json.Unmarshal(raw, &av); err != nil {
+ return nil, "", fmt.Errorf("release: %s: %w", SystemKey(AntivirusName), err)
+ }
+ if av.Schema != AntivirusSchema {
+ return nil, "", fmt.Errorf("release: %s: schema %d is not %d",
+ SystemKey(AntivirusName), av.Schema, AntivirusSchema)
+ }
+ return &av, etag, nil
+}
+
+// PutAntivirus writes with the caller's CAS position; etag "" is create-only.
+// It stamps the schema so a caller cannot write a body it would then refuse to
+// read back.
+func PutAntivirus(ctx context.Context, s Store, av *AntivirusSettings, etag string) (string, error) {
+ av.Schema = AntivirusSchema
+ body, err := json.MarshalIndent(av, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return s.Put(ctx, SystemKey(AntivirusName), append(body, '\n'), etag)
+}
+
+// AntivirusEnabled folds the read into the one question every caller actually
+// asks. A read error is NOT "disabled": it is an error, and a caller that
+// treated it as disabled would silently stop scanning on a transient bucket
+// failure.
+func AntivirusEnabled(ctx context.Context, s Store) (bool, error) {
+ av, _, err := GetAntivirus(ctx, s)
+ if err != nil {
+ return false, err
+ }
+ return av != nil && av.Enabled, nil
+}
diff --git a/internal/release/system_test.go b/internal/release/system_test.go
new file mode 100644
index 000000000..9d29e1a48
--- /dev/null
+++ b/internal/release/system_test.go
@@ -0,0 +1,100 @@
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "testing"
+ "time"
+)
+
+func TestSystemKeyShape(t *testing.T) {
+ if got := SystemKey("antivirus"); got != "system/antivirus.json" {
+ t.Fatalf("SystemKey = %q", got)
+ }
+}
+
+// Absent is a state, not an error — and it means NOT ENABLED. A deployment
+// that has never enabled scanning does not scan.
+func TestGetAntivirusAbsentIsNotEnabled(t *testing.T) {
+ ctx := context.Background()
+ s := NewDirStore(t.TempDir())
+
+ av, etag, err := GetAntivirus(ctx, s)
+ if err != nil || av != nil || etag != "" {
+ t.Fatalf("absent record = %+v, %q, %v", av, etag, err)
+ }
+ on, err := AntivirusEnabled(ctx, s)
+ if err != nil || on {
+ t.Fatalf("AntivirusEnabled with no record = %v, %v", on, err)
+ }
+}
+
+// An unknown schema refuses rather than guessing. A future schema 2 could mean
+// anything, and the thing this record drives is whether the deployment reads
+// tenant object bytes.
+func TestAntivirusSchemaRefusesUnknown(t *testing.T) {
+ ctx := context.Background()
+ s := NewDirStore(t.TempDir())
+
+ body, _ := json.Marshal(map[string]any{"schema": 2, "enabled": true})
+ if _, err := s.Put(ctx, SystemKey(AntivirusName), body, ""); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, _, err := GetAntivirus(ctx, s); err == nil {
+ t.Fatal("an unknown schema was read as if it were understood")
+ }
+ // And the folded helper must not turn that refusal into "disabled" — a
+ // caller acting on false would silently stop scanning.
+ if _, err := AntivirusEnabled(ctx, s); err == nil {
+ t.Fatal("AntivirusEnabled swallowed a schema refusal as disabled")
+ }
+}
+
+// A read error is not "disabled" either, for the same reason.
+func TestAntivirusEnabledDoesNotSwallowErrors(t *testing.T) {
+ ctx := context.Background()
+ s := NewDirStore(t.TempDir())
+
+ body, _ := json.Marshal(map[string]any{"schema": 1, "enabled": true})
+ if _, err := s.Put(ctx, SystemKey(AntivirusName), []byte(string(body)[:len(body)-3]), ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := AntivirusEnabled(ctx, s); err == nil {
+ t.Fatal("malformed JSON was read as disabled rather than as an error")
+ }
+}
+
+func TestPutAntivirusCreateOnlyThenReplace(t *testing.T) {
+ ctx := context.Background()
+ s := NewDirStore(t.TempDir())
+ now := time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC)
+
+ etag, err := PutAntivirus(ctx, s, &AntivirusSettings{
+ Enabled: true, Engine: "clamav", At: now, By: "op@example.com",
+ }, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create-only against an existing key conflicts — this is what makes the
+ // boot seed idempotent across two consoles and a restart loop.
+ if _, err := PutAntivirus(ctx, s, &AntivirusSettings{Enabled: false}, ""); !errors.Is(err, ErrCASConflict) {
+ t.Fatalf("second create-only write = %v, want ErrCASConflict", err)
+ }
+
+ if _, err := PutAntivirus(ctx, s, &AntivirusSettings{
+ Enabled: false, Engine: "clamav", At: now, By: "op@example.com",
+ }, etag); err != nil {
+ t.Fatalf("replace with the current etag: %v", err)
+ }
+
+ av, _, err := GetAntivirus(ctx, s)
+ if err != nil || av == nil || av.Enabled {
+ t.Fatalf("after disable: %+v, %v", av, err)
+ }
+ if av.Schema != AntivirusSchema {
+ t.Fatalf("schema not stamped on write: %+v", av)
+ }
+}