Skip to content

sdk: an egress floor, stated at join and held on every call - #538

Merged
aojea merged 3 commits into
google:mainfrom
kaisoz:kaisoz/sdk-egress-floor-3
Sep 28, 2026
Merged

aojea merged 3 commits into
google:mainfrom
kaisoz:kaisoz/sdk-egress-floor-3

Conversation

@kaisoz

@kaisoz kaisoz commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

join({ egressRequireLabels }) (join(egress_require_labels=)) is sam-node's egress.require_labels for an SDK member: labels every provider the session calls must attest, all of them, on top of a call's required labels, on every outbound call, MCP and HTTP alike, however the peer was named. It is stated once at join and held for the session; a call cannot waive or widen it. An unmet floor is a LabelsNotSatisfiedError, in both SDKs.

The HTTP path (request, fetch, Python MeshTransport) now verifies the provider through the mutual /sam/auth/1.0.0 handshake before sending anything, floor or not, as the MCP path and sam-node's egress proxy (VerifyPeerLabels) already did. A positive verdict is kept per peer for five minutes, sam-node's labelGateTTL; a miss is never kept. A banned peer is still refused at connect().

Tests: unit tests in both SDKs (floor met and missed on the MCP and the HTTP path, an unenrolled provider refused on the HTTP path, the cached verdict); the integration runners take SAM_SDK_EGRESS_REQUIRE_LABELS, every member holds a floor the mesh satisfies, and a new egress-floor subtest runs one extra member per SDK whose floor nobody attests, refused by the node and by the other members.

Docs: sdk/README.md, the native SDKs guide and both SDK READMEs.

join({ egressRequireLabels }) / join(egress_require_labels=) is
sam-node's egress.require_labels for an SDK member: every provider the
session calls must attest all of its pairs, on top of a call's required
labels, MCP and HTTP alike, however the peer was named. A call cannot
waive or widen it; an unmet floor is a LabelsNotSatisfiedError.

The HTTP path (request, fetch, MeshTransport) now verifies the provider
through the mutual /sam/auth handshake before sending anything, floor or
not, as the MCP path and sam-node's egress proxy already did; a positive
verdict is kept per peer for five minutes, sam-node's labelGateTTL.

The integration runners hold a floor the mesh satisfies, and one extra
runner per SDK, whose floor nobody attests, is refused by the node and by
the other members before anything is sent.

@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 implements support for egress floors in both the JavaScript and Python SDKs, aligning their behavior with sam-node's egress.require_labels. When joining a mesh session, an egress floor can be specified (via egressRequireLabels or egress_require_labels), requiring all called providers to attest to all specified label pairs for both MCP and HTTP outbound calls. The implementations verify providers, enforce the conjunction of these labels, and cache positive verdicts for five minutes. Conformance runners, documentation, unit tests, and integration tests have been updated accordingly. As there are no review comments to evaluate, I have no additional feedback to provide.

@aojea aojea left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked the semantics against sam-node (api.LabelCheck, api.LabelFloorCheck, VerifyPeerLabels/checkPeerLabels). They agree on the points that matter:

  • Caller requirement is a disjunction: or / some / any. Empty or absent is no requirement in all three (len(required) > 0, !required || pairs.length === 0, not required).
  • Floor is a conjunction: , / every / all. Empty or absent is no floor in all three (len(floor) > 0, !required with every([]) === true, not required).
  • Both apply independently when both are set; the tests in both SDKs cover the cross cases where one's pairs would otherwise stand in for the other's.
  • The gate runs with no requirement and no floor and still verifies an enrolled, bound credential; the impostor case covers it.
  • Positive verdicts are kept five minutes, misses never. Keying the cache on the peer alone is enough here because the floor is fixed for the session and the HTTP path carries no per-call requirement, which is why sam-node keys on (peer, required, floor).
  • The runners read a blank SAM_SDK_EGRESS_REQUIRE_LABELS as no floor, as parseRequiredLabels("") does.

Two divergences inline: the provider role that checkPeerLabels requires and the SDK gates do not, and the runners' env parser turning a malformed floor into no floor. Two style nits on the docs. discover() does not apply the floor to gossip labels the way rankProviders does; that is ranking only, enforcement is unchanged, so no action needed.

Comment thread sdk/js/src/session.ts
return conn;
}
const provider = await authenticateWithPeer(conn, this.mesh.authFrame(), this.mesh.credential.controlPlaneKeys);
requireEgressLabels(provider, this.#egressRequireLabels);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

checkPeerLabels also requires role("sam:role:node") on the provider ("only nodes host services"), so on sam-node a router's or an admin's credential never passes the gate even when it attests the floor. This gate (and openMCPSession) stops at verifyPeerBiscuit. Since the PR states parity with VerifyPeerLabels on the HTTP path, a requireRole(provider, ROLE_NODE) here (and its Python twin in _egress_peer) would close the gap; the MCP path has the same gap, pre-existing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: requireRole(provider, ROLE_NODE) runs between authenticateWithPeer and the label check, so the order is checkPeerLabels's (verify, role, labels). Both SDKs' HTTP-path tests now include a router-role credential that attests the floor and is refused. The MCP path is left as it was; it is the same one line in openMCPSession / open_mcp_session if you want it in this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: the same requireRole(provider, ROLE_NODE) / require_role(provider, ROLE_NODE) now runs in openMCPSession and open_mcp_session right after the provider's credential verifies, before the requirement and the floor, so both paths match checkPeerLabels. Each SDK has a test with a router-role provider that attests the floor and is refused on the MCP path.

if self._egress_verdicts.get(str(peer_id), 0.0) > time.monotonic():
return peer_id
provider = await authenticate_with_peer(self.host, peer_id, self.mesh.auth_frame(), self.mesh.credential.control_plane_keys)
require_egress_labels(provider, self.egress_require_labels)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as the JS side: sam-node's checkPeerLabels requires the node role on the provider before evaluating the requirement and the floor. require_role(provider, ROLE_NODE) here would match it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done: require_role(provider, ROLE_NODE) before require_egress_labels in _egress_peer, same order as checkPeerLabels.

Comment thread sdk/js/src/conformance-join.ts Outdated
return Object.fromEntries(
(process.env[name] ?? "")
.split(",")
.filter((pair) => pair.includes("="))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail-open for a floor: SAM_SDK_EGRESS_REQUIRE_LABELS=teamnobody (no =) parses to {}, which main then reads as "no floor". sam-node's parseRequiredLabels rejects a non-blank specification that yields no pair for exactly this reason (a typo must not switch the gate off), and its config validates require_labels at load. This is the test runner, and the same parser already served SAM_SDK_LABELS, but for the floor the safe direction is to fail: throw on a pair without = (or on non-blank input that produces no pair). Same for _labels_from_env in the Python runner.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in both runners: a pair without = now fails the runner (blank input is still no floor), as parseRequiredLabels does. The helper is shared with SAM_SDK_LABELS, so a malformed enrollment label fails too instead of being dropped.

Comment thread site/content/docs/guides/native-sdks.md Outdated
`egress.require_labels` does for a `sam-node`. It is stated once at `join`
and held for the session, on every call and however the peer was named;
the agent's calls cannot waive or widen it. It is the program author's
floor, not the operator's: nothing outside the process sets it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Style: "It is the program author's floor, not the operator's: nothing outside the process sets it." is an antithesis with a colon fragment. Suggest: "The floor belongs to the program that calls join; no configuration outside the process sets it."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken as suggested.

Comment thread sdk/README.md Outdated
`MeshTransport`) verifies the provider with or without a floor, through
the mutual `/sam/auth/1.0.0` handshake, as `sam-node`'s `VerifyPeerLabels`
does before its egress proxy sends anything; a positive verdict is kept
per peer for five minutes (`labelGateTTL`), a miss never. Refusals are

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Style: "a positive verdict is kept per peer for five minutes (labelGateTTL), a miss never" is a fragment. Suggest: "a positive verdict is kept per peer for five minutes (labelGateTTL); a refusal is not kept."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken, with the next sentence folded in: "; a refusal is not kept. An unmet floor is a LabelsNotSatisfiedError naming the floor."

…the runner

sam-node's checkPeerLabels requires the node role on the provider before
the requirement and the floor are evaluated; the HTTP-path gate in both
SDKs now does the same, so a router's or an admin's credential attesting
the floor is still not a provider.

The runners' label parser refused nothing: a pair without "=" was dropped
and a floor spelled wrong became no floor. It now fails on such a pair,
as sam-node's parseRequiredLabels does.

Two doc sentences reworded.
@aojea

aojea commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Done: requireRole(provider, ROLE_NODE) runs between authenticateWithPeer and the label check, so the order is checkPeerLabels's (verify, role, labels). Both SDKs' HTTP-path tests now include a router-role credential that attests the floor and is refused. The MCP path is left as it was; it is the same one line in openMCPSession / open_mcp_session if you want it in this PR.

Better to get all the things fixed in one PR

Can you fix MCP path too?

The same node-role check as on the HTTP path, in openMCPSession and
open_mcp_session right after the provider's credential verifies, as
sam-node's checkPeerLabels runs it for every service call.
@aojea
aojea merged commit 7b0debb 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.

2 participants