Skip to content

labels: a caller's requirement is a conjunction, as the egress floor is - #539

Merged
aojea merged 1 commit into
google:mainfrom
aojea:required-labels-conjunction
Sep 28, 2026
Merged

aojea merged 1 commit into
google:mainfrom
aojea:required-labels-conjunction

Conversation

@aojea

@aojea aojea commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

X-Sam-Required-Labels: k=v,k2=v2 and the SDKs' requiredLabels / required_labels now refuse a provider unless its credential attests every listed pair. Until now they accepted any one pair, while egress.require_labels required all of them: one syntax, two meanings. This settles the caller's requirement on the same rule as the floor before the release freezes it.

Where the disjunction came from

bda7e72 introduced X-Sam-Required-Region with a list of regions: check if region("EU") or region("EU-DE"). "Any of these regions" is the natural reading of a list of values for one key. The header was then generalised to labels, a map with one value per key (parseRequiredLabels rejects a duplicate key), and the or came along. With a map, the disjunction can only express cross-key alternatives ("in eu, or on team platform"), and neither of the two things people write two pairs for is expressible:

Intent With the disjunction With the conjunction
region=eu yes yes
region=eu,compliance=gdpr meaning both no: a provider attesting only region=eu passes, silently yes
gpu=true,family=llama no yes
region=eu-west-1 or region=eu-central-1 no: 400 duplicate label key no today; reserved, see below
team=platform or region=eu yes no

Prior art

Every label selector a reader is likely to know reads a map or comma-list of pairs as a conjunction and spells alternatives for one key explicitly:

System Several pairs Same-key alternatives
Kubernetes selectors, matchLabels, -l a=x,b=y AND ("the comma separator acts as a logical AND") key in (a, b)
Prometheus {a="x",b="y"} AND =~"a|b"
Istio DestinationRule subset labels, Envoy subset metadata AND separate subset
SPIRE registration entry selectors AND separate entry
Docker Swarm --constraint, Nomad constraint AND separate constraint
AWS IAM Condition AND across keys ForAnyValue on list values
Kubernetes NetworkPolicy AND within a from entry, OR across entries (a list) list entries

No system was found where a map of pairs means "any of them". SAM's own served_by already follows the convention: it is a repeated string list, documented as any-of, and it can hold site=eu and site=us. Provider-side attenuation.checks are ANDed. The caller's map was the one outlier.

Why the conjunction

  • Direction of failure. A caller who assumes AND under OR sends data to a provider that lacks a property they required, with no error. A caller who assumes OR under AND gets a 403 on the first request. Under AND, adding a pair only narrows; under OR, adding a pair only widens. A fail-closed gate should be monotone in the safe direction.
  • One syntax, one meaning. About fourteen places in code, comments and docs existed only to explain the AND/OR split, and two docs already contradicted node-api.md. They collapse to one sentence.
  • Evolution. From AND, same-key alternatives are an additive per-key list. From OR, conjunction needs a second header or option plus a precedence rule, and alternatives still need the list.
  • A client can emulate OR under AND (retry with another header). It cannot emulate AND under OR at all.

What changes

  • api.LabelCheck compiles check if label(k1, v1), label(k2, v2). LabelFloorCheck is gone; the floor uses the same function. LabelsSatisfyFloor / LabelsContradictFloor become LabelsSatisfy / LabelsContradict.
  • checkPeerLabels keeps two checks so no caller input reaches the floor; both are conjunctions and the authorizer ANDs them.
  • rankProviders treats the requirement as it treated the floor: a local must declare every pair; a remote is dropped early only on a claim that conflicts, and the gate decides on attested facts.
  • SDKs: requireLabels / require_labels (from sdk: an egress floor, stated at join and held on every call #538: requireEgressLabels / require_egress_labels) share one predicate, every pair must be attested; they differ only in the message a refusal carries ("does not attest every required label" / "does not attest the egress floor"). LabelsNotSatisfiedError takes that message as a required argument.
  • Docs: authorization.md, boundaries.md, networking.md, exposing-services.md, native-sdks.md, node-api.md, node-config.md, sdk/README.md, both skills.
  • Tests: the "any-of requirement matches one key" cases in api, internal/node, both SDKs and the TestNativeSDKsMesh matrix now expect a refusal, with new cases for every pair attested and for one pair of two missing. rankProviders gets a two-pair case (remote silent on a pair survives to the gate; a conflicting one and a local short of a pair do not). The e2e bats tests use single pairs and are unaffected.

Reserved for later

A repeated key, region=eu-west-1,region=eu-central-1, stays a 400 and is reserved for per-key alternatives: AND across keys, OR within a key, as Kubernetes in and Prometheus =~. Adding it later breaks no client, and RFC 9110 folding of repeated header lines produces exactly this form. The SDK shape would be Record<string, string | string[]>. | is not an option: it is a legal value character in api/labels.go.

Verified

Rebased on #538. go build ./..., go vet, go test ./api/ ./internal/node/, JS typecheck + mcp.test + session.test, Python test_mcp.py + test_session.py + test_libp2p_http.py, and the whole go test ./tests/integration -run 'TestNativeSDKsMesh$' (the label matrix in both directions and #538's egress-floor subtest) pass locally.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request changes the matching logic for required labels (such as X-Sam-Required-Labels and SDK requiredLabels) from a disjunction (any-of) to a conjunction (all-of), aligning it with the operator's egress floor matching rule. The separate LabelFloorCheck, LabelsSatisfyFloor, and LabelsContradictFloor functions are consolidated into unified LabelCheck, LabelsSatisfy, and LabelsContradict functions in the api package. Documentation, command-line interfaces, Go/JS/Python SDKs, and their respective unit/integration tests have been updated to reflect and verify this conjunction behavior. I have no feedback to provide as there are no review comments to assess.

`X-Sam-Required-Labels: k=v,k2=v2` and the SDKs' `requiredLabels` /
`required_labels` now refuse a provider unless its credential attests
every listed pair. Until now they accepted any one pair, while
`egress.require_labels` required all of them: one syntax, two meanings.

The disjunction dates from `X-Sam-Required-Region`, which took a list of
regions (`region("EU") or region("EU-DE")`). Generalised to a map with one
value per key it can no longer spell the alternative it was made for
(`region=eu-west-1` or `region=eu-central-1` is a `400 duplicate label
key`), and the intent people do write with two pairs, `region=eu,
compliance=gdpr`, was silently widened to "either": a provider attesting
only `region=eu` passed, with no error. Every label selector a reader is
likely to know (Kubernetes, Prometheus, Istio, SPIRE, Docker, Nomad, AWS
IAM) reads a map of pairs as a conjunction and spells alternatives for one
key explicitly.

`api.LabelCheck` compiles `check if label(k1, v1), label(k2, v2)`;
`LabelFloorCheck` is gone, the floor uses the same function.
`LabelsSatisfyFloor` / `LabelsContradictFloor` become `LabelsSatisfy` /
`LabelsContradict` and serve the requirement in `rankProviders` the way
they served the floor: a local must declare every pair, a remote is only
dropped on a conflicting claim and the gate decides on attested facts.
`checkPeerLabels` keeps two checks so no caller input reaches the floor.

A repeated key (`region=a,region=b`) stays rejected and is reserved for
per-key alternatives later; adding it then breaks no client.
@aojea
aojea force-pushed the required-labels-conjunction branch from 5a21468 to 6443c66 Compare September 28, 2026 17:25
@aojea
aojea merged commit b2803d0 into google:main Sep 28, 2026
21 checks passed
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