carlos / platform Public

cli: store status says whether scanning is on

Once scanning exists, a store status that said nothing about it is a
half-truth.

Three states, not a boolean. "Requested" is a member waiting on an
operator, "enabled" is an engine looking at objects, and "enforcing" is
infected objects actually being blocked. Collapsing those would let
someone read "scanning: on" and believe reads were denied when they
were only recorded — and the gap between those two is the entire value
of the enforcement switch.

The reply key is purely additive, which is the only safe move:
release.PurgeApp parses this same object with its own struct and
freezes the grant and provisioned names. A key it does not know is
ignored; a renamed one it does know is a purge that stops working. The
test asserts those three names survive alongside the new one.

A store that never opted in prints exactly what it printed before, and
so does one whose scanning block was emptied by a revoke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJkJkxZZLgW8X8A5tk69Sq
Paul Campbell pushed by paul@keymail.dev ef22cfe24810400e4908cba7e3bfb612e6f8b8bf parent 05c376c
5 files changed, +180 −0
  • apimode.go +4 −0
  • internal/console/apiscan_test.go +45 −0
  • internal/console/apistore.go +8 −0
  • store.go +29 −0
  • storescan_test.go +94 −0
diff --git a/apimode.go b/apimode.go
index 98e9e7f33..4d247f794 100644
--- a/apimode.go
+++ b/apimode.go
@@ -1554,6 +1554,10 @@ type apiStoreReply struct {
EnvNames []string `json:"env_names"`
PendingFinish bool `json:"pending_finish"`
MissingVars []string `json:"missing_vars"`
+ // Scanning is nil on every store that never opted in — which is all of
+ // them by default — so an older console that does not send it renders
+ // exactly as it always did.
+ Scanning *apiScanBlock `json:"scanning,omitempty"`
}
// ---- store scanning (scan.go) ----------------------------------------
diff --git a/internal/console/apiscan_test.go b/internal/console/apiscan_test.go
index a787ceede..4d5ee20f1 100644
--- a/internal/console/apiscan_test.go
+++ b/internal/console/apiscan_test.go
@@ -246,3 +246,48 @@ func TestScanStatusCarriesProvenance(t *testing.T) {
t.Fatal("status carries no signature-database version")
}
}
+
+// `carlos store status` carries the scan block once it exists. Additive only:
+// release.PurgeApp parses this same object with its own struct and freezes the
+// grant and provisioned names, so a new key is safe and a renamed one is a
+// purge that stops working.
+func TestStoreStatusCarriesScanningBlock(t *testing.T) {
+ fx, _, token := scanArmed(t, "code-store-status-scan")
+
+ status, out := getJSON(t, storeURL(fx, "titogo"), token)
+ if status != http.StatusOK {
+ t.Fatalf("store status = %d %v", status, out)
+ }
+ if _, present := out["scanning"]; !present {
+ t.Fatal("store status carries no scanning key at all")
+ }
+ if out["scanning"] != nil {
+ t.Fatalf("a store nobody requested scanning for reported %v", out["scanning"])
+ }
+
+ if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK {
+ t.Fatalf("scan request = %d %v", s, o)
+ }
+ if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
+ t.Fatalf("scan grant = %d %v", s, o)
+ }
+
+ status, out = getJSON(t, storeURL(fx, "titogo"), token)
+ if status != http.StatusOK {
+ t.Fatalf("store status = %d %v", status, out)
+ }
+ sc, ok := out["scanning"].(map[string]any)
+ if !ok {
+ t.Fatalf("scanning = %#v, want an object", out["scanning"])
+ }
+ if _, ok := sc["enabled"]; !ok {
+ t.Fatalf("scanning block does not report the enabled engine: %v", sc)
+ }
+ // The grant and provisioned names PurgeApp parses must still be there and
+ // still be spelled the same.
+ for _, frozen := range []string{"grant", "provisioned", "declared"} {
+ if _, present := out[frozen]; !present {
+ t.Fatalf("the %q key PurgeApp parses is gone from the reply", frozen)
+ }
+ }
+}
diff --git a/internal/console/apistore.go b/internal/console/apistore.go
index feae53e4f..e8e85c1de 100644
--- a/internal/console/apistore.go
+++ b/internal/console/apistore.go
@@ -139,6 +139,14 @@ func (s *Server) storeReply(ctx context.Context, acct *Account, app string, rec
// two credentials open this bucket.
"pending_finish": rec.Provisioned != nil && rec.Provisioned.PrevKeyID != "",
"missing_vars": missing,
+ // Additive, and additive is the ONLY safe move here: release.PurgeApp
+ // parses this same object with its own struct and freezes the `grant`
+ // and `provisioned` names. A new key it does not know about is
+ // ignored; a renamed one it does know about is a purge that stops
+ // working. Once scanning exists, a status that said nothing about it
+ // would be a half-truth — but it is nil on every store that never
+ // opted in, which is all of them by default.
+ "scanning": rec.Scanning,
}, nil
}
diff --git a/store.go b/store.go
index 2b94a67a2..1aa2fdea5 100644
--- a/store.go
+++ b/store.go
@@ -365,6 +365,14 @@ func printStoreStatus(w io.Writer, res apiStoreReply) {
if res.PendingFinish {
warnf(w, "a rotation is pending finish — the previous key is still live: carlos store rotate --finish --app %s", res.App)
}
+ // Scanning, once it exists. Deliberately three distinct states rather
+ // than a boolean: "requested" is a member waiting on an operator,
+ // "enabled" is an engine looking at objects, and "enforcing" is infected
+ // objects actually being blocked. Collapsing those would let someone read
+ // "scanning: on" and believe reads were denied when they were only
+ // recorded.
+ printStoreScanLine(w, res)
+
if len(res.MissingVars) > 0 {
// Named as drift, not as a fresh failure: this is the same state a
// healthy store passes through for a few seconds after every grant
@@ -376,3 +384,24 @@ func printStoreStatus(w io.Writer, res apiStoreReply) {
"repair (this rotates the credential)", strings.Join(res.MissingVars, ", "), res.App)
}
}
+
+// printStoreScanLine is store status's one-line summary of the scan family.
+// The detail lives behind `carlos store scan status`; this exists so a member
+// reading store status is never left believing nothing is looking at their
+// objects when something is, or the reverse.
+func printStoreScanLine(w io.Writer, res apiStoreReply) {
+ sc := res.Scanning
+ if sc == nil || (sc.Requested == nil && sc.Enabled == nil) {
+ return
+ }
+ switch {
+ case sc.Enabled != nil && sc.Enforce != nil:
+ fmt.Fprintf(w, " scanning: %s, enforcing — infected objects cannot be read by this app\n", sc.Enabled.Engine)
+ case sc.Enabled != nil:
+ fmt.Fprintf(w, " scanning: %s, recording only — infected objects are NOT blocked\n", sc.Enabled.Engine)
+ case sc.Requested != nil && sc.Requested.Encrypted:
+ mutedf(w, " scanning: requested, attested client-encrypted — no coverage will be claimed")
+ default:
+ mutedf(w, " scanning: requested, no engine enabled yet (carlos store scan status --app %s)", res.App)
+ }
+}
diff --git a/storescan_test.go b/storescan_test.go
new file mode 100644
index 000000000..8221b90fc
--- /dev/null
+++ b/storescan_test.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+ "time"
+)
+
+func storeReplyWithScanning(sc *apiScanBlock) apiStoreReply {
+ at := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
+ return apiStoreReply{
+ App: "titogo",
+ Backend: "aws",
+ Declared: &apiStoreDeclared{EnvPrefix: "CARLOS_STORE", By: "m@example.com", At: at},
+ Provisioned: &apiStoreProvisioned{
+ Bucket: "carlos-bab-titogo", Region: "eu-west-1",
+ IAMUser: "carlos-bab-titogo", KeyID: "AKIA1", At: at,
+ },
+ EnvNames: []string{"CARLOS_STORE_BUCKET"},
+ MissingVars: []string{},
+ Scanning: sc,
+ }
+}
+
+// store status must mention scanning once it exists, and must not imply
+// coverage — or enforcement — for a store that only has it recorded.
+func TestPrintStoreStatusShowsScanState(t *testing.T) {
+ at := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
+
+ // Enabled but NOT enforcing: the line must say infected objects are not
+ // blocked. Someone reading "scanning: clamav" and assuming otherwise is
+ // the exact misunderstanding this line exists to prevent.
+ var b bytes.Buffer
+ printStoreStatus(&b, storeReplyWithScanning(&apiScanBlock{
+ Requested: &apiScanRequested{At: at, By: "m@example.com"},
+ Enabled: &apiScanEnabled{Engine: "clamav", At: at, By: "op@example.com"},
+ }))
+ if out := b.String(); !strings.Contains(out, "recording only") || !strings.Contains(out, "NOT blocked") {
+ t.Fatalf("an enabled-but-not-enforcing store did not say so:\n%s", out)
+ }
+
+ // Enforcing.
+ var b2 bytes.Buffer
+ printStoreStatus(&b2, storeReplyWithScanning(&apiScanBlock{
+ Requested: &apiScanRequested{At: at, By: "m@example.com"},
+ Enabled: &apiScanEnabled{Engine: "clamav", At: at, By: "op@example.com"},
+ Enforce: &apiScanEnforce{Key: "carlos-scan", Value: "infected"},
+ }))
+ if out := b2.String(); !strings.Contains(out, "enforcing") {
+ t.Fatalf("an enforcing store did not say so:\n%s", out)
+ }
+
+ // Requested only: a member waiting on an operator, with no claim of
+ // anything having looked at an object.
+ var b3 bytes.Buffer
+ printStoreStatus(&b3, storeReplyWithScanning(&apiScanBlock{
+ Requested: &apiScanRequested{At: at, By: "m@example.com"},
+ }))
+ out3 := b3.String()
+ if !strings.Contains(out3, "no engine enabled yet") {
+ t.Fatalf("a requested-only store did not say it is waiting:\n%s", out3)
+ }
+ if strings.Contains(out3, "enforcing") {
+ t.Fatalf("a requested-only store implied enforcement:\n%s", out3)
+ }
+
+ // Requested and attested encrypted.
+ var b4 bytes.Buffer
+ printStoreStatus(&b4, storeReplyWithScanning(&apiScanBlock{
+ Requested: &apiScanRequested{At: at, By: "m@example.com", Encrypted: true},
+ }))
+ if out := b4.String(); !strings.Contains(out, "no coverage will be claimed") {
+ t.Fatalf("an encrypted store's status implied coverage:\n%s", out)
+ }
+}
+
+// A store with no scanning block prints exactly what it prints today — every
+// existing deployment sees no change at all.
+func TestPrintStoreStatusUnchangedWithoutScanning(t *testing.T) {
+ var b bytes.Buffer
+ printStoreStatus(&b, storeReplyWithScanning(nil))
+ if strings.Contains(b.String(), "scanning") {
+ t.Fatalf("a store that never opted in grew a scanning line:\n%s", b.String())
+ }
+
+ // An empty block is the same case: a record can carry `scanning: {}` after
+ // a revoke cleared both halves, and that is not a state worth a line.
+ var b2 bytes.Buffer
+ printStoreStatus(&b2, storeReplyWithScanning(&apiScanBlock{}))
+ if strings.Contains(b2.String(), "scanning") {
+ t.Fatalf("an emptied scanning block still printed a line:\n%s", b2.String())
+ }
+}