fix(sdk): canonicalize projected policy lists

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
Piotr Mlocek
2026-09-21 10:26:27 -07:00
parent e51844aee2
commit 751aedade4
4 changed files with 109 additions and 4 deletions
@@ -245,7 +245,7 @@ func publicNetworkEndpointFromInternalProto(endpoint *sbv1.NetworkEndpoint) *pol
SigningRegion: endpoint.GetSigningRegion(),
}
if ports := endpoint.GetPorts(); len(ports) > 0 {
result.Ports = append([]uint32(nil), ports...)
result.Ports = stableUnique(ports)
} else if port := endpoint.GetPort(); port != 0 {
// Older internal records may still use the legacy scalar port. The
// authored contract has only the canonical list representation.
@@ -435,7 +435,7 @@ func publicMatcherMapFromInternalProto(matchers map[string]*sbv1.L7QueryMatcher)
func publicMatcherFromInternalProto(matcher *sbv1.L7QueryMatcher) *policyv1.Matcher {
if matcher != nil && len(matcher.GetAny()) > 0 {
return &policyv1.Matcher{Kind: &policyv1.Matcher_Any{Any: &policyv1.AnyMatcher{
Values: CopyStringSlice(matcher.GetAny()),
Values: stableUnique(matcher.GetAny()),
}}}
}
glob := ""
@@ -445,6 +445,22 @@ func publicMatcherFromInternalProto(matcher *sbv1.L7QueryMatcher) *policyv1.Matc
return &policyv1.Matcher{Kind: &policyv1.Matcher_Glob{Glob: glob}}
}
// stableUnique canonicalizes legacy runtime lists for public fields whose
// protobuf contract requires unique values. Retaining the first occurrence
// keeps the projection deterministic without changing caller-visible order.
func stableUnique[T comparable](values []T) []T {
result := make([]T, 0, len(values))
seen := make(map[T]struct{}, len(values))
for _, value := range values {
if _, found := seen[value]; found {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func publicTLSMode(mode sbv1.NetworkTlsMode) string {
// Numeric cases keep the explicit projection compatible with deprecated
// internal enum values without making new SDK code depend on their names.
@@ -393,6 +393,39 @@ func TestPolicyDocumentFromInternalProtoExplicitProjection(t *testing.T) {
assert.Equal(t, []string{"2025-11-25"}, policy.NetworkPolicies["api"].Endpoints[0].Mcp.Versions)
}
func TestPolicyDocumentFromInternalProtoDeduplicatesLegacyValues(t *testing.T) {
internal := &sbv1.SandboxPolicy{
Version: 1,
NetworkPolicies: map[string]*sbv1.NetworkPolicyRule{
"api": {
Endpoints: []*sbv1.NetworkEndpoint{{
Host: "api.example.com",
Ports: []uint32{443, 8443, 443},
Protocol: "http",
Rules: []*sbv1.L7Rule{{Allow: &sbv1.L7Allow{
Method: "GET",
Query: map[string]*sbv1.L7QueryMatcher{
"state": {Any: []string{"open", "closed", "open"}},
},
}}},
}},
},
},
}
policy := PolicyDocumentFromInternalProto(internal)
require.NotNil(t, policy)
endpoint := policy.NetworkPolicies["api"].Endpoints[0]
assert.Equal(t, []uint32{443, 8443}, endpoint.Ports)
require.NotNil(t, endpoint.Rules[0].Allow)
assert.Equal(t, []string{"open", "closed"}, endpoint.Rules[0].Allow.Query["state"].Any)
// Policy updates use the checked conversion path, so a projected policy
// must satisfy the public protobuf uniqueness constraints before submission.
_, err := PolicyDocumentToProtoChecked(policy)
require.NoError(t, err)
}
func TestSandboxPolicyRoundTrip(t *testing.T) {
original := &v1.PolicyDocument{
Version: 5,
+50
View File
@@ -1317,6 +1317,56 @@ describe('config / policy', () => {
expect(Object.hasOwn(Object, 'children')).toBe(false);
});
it('getConfig canonicalizes legacy lists before a checked policy update', async () => {
let submittedPolicy: unknown;
const sandbox = client({
getSandbox: () => readySandbox('sb', 'sb-id'),
getSandboxConfig: () => ({
policy: {
version: 1,
networkPolicies: {
api: {
endpoints: [
{
host: 'api.example.com',
ports: [443, 8443, 443],
protocol: 'http',
rules: [
{
allow: {
method: 'GET',
query: { state: { any: ['open', 'closed', 'open'] } },
},
},
],
},
],
},
},
},
settings: {},
}),
updateConfig: (req) => {
submittedPolicy = req.policy;
return { version: 2, policyHash: 'canonical', settingsRevision: 0n, deleted: false };
},
});
const config = await sandbox.getConfig('sb');
const projectedPolicy = config.policy;
expect(projectedPolicy).toBeDefined();
if (!projectedPolicy) throw new Error('expected projected policy');
const endpoint = projectedPolicy.networkPolicies.api?.endpoints[0];
expect(endpoint?.ports).toEqual([443, 8443]);
expect(endpoint?.rules[0]?.allow?.query.state?.kind).toEqual({
case: 'any',
value: expect.objectContaining({ values: ['open', 'closed'] }),
});
await expect(sandbox.setPolicy('sb', projectedPolicy)).resolves.toMatchObject({ policyHash: 'canonical' });
expect(submittedPolicy).toBeDefined();
});
it('setPolicy sends global=false + version pin and (wait) polls until the hash matches', async () => {
let updateReq: {
sandbox?: string;
+8 -2
View File
@@ -606,7 +606,7 @@ function policyEndpointFromRuntime(
const mcpProtocol = endpoint.protocol.toLowerCase() === 'mcp';
return {
host: endpoint.host,
ports: endpoint.ports.length > 0 ? [...endpoint.ports] : endpoint.port === 0 ? [] : [endpoint.port],
ports: endpoint.ports.length > 0 ? stableUnique(endpoint.ports) : endpoint.port === 0 ? [] : [endpoint.port],
protocol: endpoint.protocol,
tls: runtimeTLSMode(endpoint.tls),
enforcement: runtimeEnforcementMode(endpoint.enforcement),
@@ -703,11 +703,17 @@ function policyMCPMethod(
function policyMatcherFromRuntime(matcher: RuntimeMatcher | undefined): MatcherInit {
if (matcher && matcher.any.length > 0) {
return { kind: { case: 'any', value: { values: [...matcher.any] } } };
return { kind: { case: 'any', value: { values: stableUnique(matcher.any) } } };
}
return { kind: { case: 'glob', value: matcher?.glob ?? '' } };
}
// The public protobuf contract requires these projected lists to be unique.
// Set iteration retains the first-seen order from legacy runtime records.
function stableUnique<T>(values: readonly T[]): T[] {
return [...new Set(values)];
}
function policyMatcherMapFromRuntime(matchers: Record<string, RuntimeMatcher>): Record<string, MatcherInit> {
return Object.fromEntries(
Object.entries(matchers).map(([name, matcher]) => [name, policyMatcherFromRuntime(matcher)]),