Skip to content

feat: coverage says how much was hidden, never what (ADR 0054) - #89

Merged
kfeofantov merged 1 commit into
mainfrom
feat/collection-coverage
Aug 30, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
feat/collection-coverage

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

"Nothing found" is a result, and today it is indistinguishable from a broken
agent.
The backend receives some payloads and has no way to say "we looked at
412 pods, 37 were excluded by your own namespace filter, and one cache we hold a
grant for never synced".

Everything needed was already computed and thrown away: twelve filter counters,
two placement drops, five node-scan counters, nine inventory counters, and the
watch health of every non-gating source — all of it into a log line the backend
never sees. security.md §11 promised filtering is "verifiable from output", and
the output was the agent's own log. The node scanner's counters were worse off
still: they crossed the channel inside nodescan.Report and the join read only
the binaries beside them.

New kind collection_coverage, plus a fifth provenance class agent — the other
four answer "where in the cluster did this come from", and this payload's subject
is the agent itself. registry_test.go records the widening, which is the
mechanism working rather than being worked around.

The privacy question, which changed the design

The plan originally called for a hash of the effective configuration. That is
wrong here for a reason specific to what the file holds: it is a handful of short
namespace names, a digest of a low-entropy value is reversible by trying the
plausible ones, and the list it would expose is the deny list — precisely the
set the customer chose to hide from us. The allow list is no secret; those
namespaces are named in every other payload.

So there is no hash. The payload carries the configuration's shape — how many
entries each list holds, which switches are on — and config.since, the file's
mtime. Nothing to reverse, because nothing is concealed, and a time answers "have
the filters changed" better than an opaque token does.

TestCoverageNamesNothingItExcluded builds the payload from a deny list holding
real namespace names and asserts not one reaches the bytes, while still asserting
the counts survive — privacy by saying nothing would be the other failure.

Effective access is measured, not declared

Per source: did the cache fill, is it failing. A grant the ClusterRole holds but
an admission webhook or NetworkPolicy defeats reads as granted in any review of
the rules and as failing here. That is the difference that matters to a report
resting on it — and it removes the reason to call SelfSubjectRulesReview, which
is a create from an agent whose customer document says it only ever gets, lists
and watches. That call is not made.

What is not closed, and is stated rather than hidden

A workload that disappears from the collected set as excluded_workload_annotation
moves 0 → 1 is identifiable by subtraction. The counter does not create that
channel — the disappearance is observable without it — but it does explain it,
and the alternative is the backend concluding the workload was deleted and
telling the customer something false. In the ADR and in security.md, per
ADR 0039.

No RBAC, no new read, no new API call. Two [planned] markers leave
security.md; backend-requirements.md stops disagreeing with the code.

Ceilings: security.md 960 → 975, backend-requirements.md 530 → 555.

Evidence:

go test -race ./... && make lint && make chart-lint   # green
make inventory-e2e
# coverage: 12 pods observed, 41 processes scanned on 1 node(s)
# (2 Go, 26 out of scope), signals [cpu memory network psi throttling]

That "26 out of scope" is the node scanner's own filter counter reaching a
payload for the first time.

🤖 Generated with Claude Code

https://claude.ai/code/session_011YRKuL97KRndaopfpixrhp

An empty report and a broken agent produce identical bytes today. Everything
needed to tell them apart was already computed — twelve filter counters, two
placement drops, five node-scan counters, nine inventory counters, the watch
health of every non-gating source — and all of it went to a log line the backend
never sees. The node scanner's counters were worse off: they crossed the channel
and the join dropped them on the floor.

New kind `collection_coverage`, and a fifth provenance class `agent`: the other
four answer "where in the cluster did this come from", and this payload's subject
is the agent itself.

No hash of the configuration. A digest of a handful of short namespace names is
reversible by trying the plausible ones, and what it would expose is the deny
list — precisely the set the customer chose to hide. The payload carries the
configuration's shape (entry counts, switches) and the file's mtime instead:
nothing to reverse, and a time answers "have the filters changed" better than an
opaque token.

Effective access is measured, not declared: per source, did the cache fill and is
it failing. A grant the ClusterRole holds but a webhook defeats reads as granted
in any review of the rules and as failing here. That also removes the reason to
call SelfSubjectRulesReview, which is a `create` from an agent whose customer
document says it only gets, lists and watches.

The one inference the counters do not close — a workload disappearing as an
opt-out counter moves — is stated in the ADR and in security.md rather than left
for a reviewer, per ADR 0039. The disappearance is observable without the
counter; the counter only explains it.

    go test -race ./... && make lint && make chart-lint   # green
    make inventory-e2e
    # coverage: 12 pods observed, 41 processes scanned on 1 node(s)
    # (2 Go, 26 out of scope), signals [cpu memory network psi throttling]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YRKuL97KRndaopfpixrhp
@kfeofantov
kfeofantov merged commit 1837ae3 into main Aug 30, 2026
3 checks passed
@kfeofantov
kfeofantov deleted the feat/collection-coverage branch August 30, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant