Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
661 changes: 661 additions & 0 deletions docs/superpowers/plans/2026-08-08-proto-gap-drop-d.md

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions docs/superpowers/specs/2026-08-08-proto-gap-drop-d-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Proto Gap Fixes: Drop D Network Policy Types

**Date**: 2026-08-08
**Issues**: #35, #36, #37
**JIRA**: RHAIENG-6595
**Branch**: `fix/proto-gap-drop-d`

## Problem

Three sets of upstream proto fields have no corresponding SDK domain types or converter code. Some are incorrectly listed as "handled" in the coverage test despite having no implementation. This blocks Drop D upstream contribution.

## Scope

Single branch and PR covering all three issues. All changes follow the established 3-layer pattern: domain types, converters, coverage test, unit tests.

## Issue #37: SigV4 + JSON-RPC Fields on NetworkEndpoint

Four scalar fields added to `PolicyNetworkEndpoint`:

| Field | Go Type | Proto Source |
|-------|---------|-------------|
| `CredentialSigning` | `string` | `credential_signing` (line 166) |
| `SigningService` | `string` | `signing_service` (line 169) |
| `SigningRegion` | `string` | `signing_region` (line 172) |
| `JsonRpcMaxBodyBytes` | `uint32` | `json_rpc_max_body_bytes` (line 175) |

Converter: direct field assignment, no nil checks (scalars).

## Issue #35: MCP Policy Types

### New type: `McpOptions`

```go
type McpOptions struct {
StrictToolNames *bool
AllowAllKnownMcpMethods *bool
}
```

Uses `*bool` because proto fields are `optional bool`. Converter uses `CopyBoolPtr()`.

### Modified types

- `PolicyNetworkEndpoint`: add `Mcp *McpOptions` field
- `L7Allow`: add `Params map[string]L7QueryMatcher` field
- `L7DenyRule`: add `Params map[string]L7QueryMatcher` field

Note: `L7QueryMatcher` needs to be defined if not already present. Check proto for the exact message shape during implementation.

## Issue #36: Network Middleware Types

### New type: `NetworkMiddlewareConfig`

```go
type NetworkMiddlewareConfig struct {
Name string
Middleware string
Config map[string]any
OnError string
Endpoints *MiddlewareEndpointSelector
Order int32
}
```

`Config` maps from `google.protobuf.Struct` (same pattern used elsewhere in the SDK).

### New type: `MiddlewareEndpointSelector`

```go
type MiddlewareEndpointSelector struct {
Include []string
Exclude []string
}
```

### New type: `SupervisorMiddlewareService`

```go
type SupervisorMiddlewareService struct {
Name string
GrpcEndpoint string
MaxBodyBytes uint64
Timeout string
}
```

### Modified type: `SandboxPolicy`

Add `NetworkMiddlewares map[string]NetworkMiddlewareConfig`. Move `network_middlewares` from `skipped` to `handled` in coverage test.

## Files Changed

| File | Changes |
|------|---------|
| `openshell/v1/types/network_policy.go` | `PolicyNetworkEndpoint` fields, `McpOptions` type |
| `openshell/v1/types/policy.go` | Middleware types, `SandboxPolicy.NetworkMiddlewares`, `Params` on L7 types |
| `openshell/v1/internal/converter/network_policy.go` | Converter for SigV4, MCP, Params fields |
| `openshell/v1/internal/converter/policy.go` | Converter for middleware types |
| `openshell/v1/internal/converter/coverage_test.go` | Move fields to handled, remove from skipped |
| `openshell/v1/internal/converter/network_policy_test.go` | Tests for endpoint/MCP conversions |
| `openshell/v1/internal/converter/policy_test.go` | Tests for middleware conversions |

## Testing Strategy

For each new type/field:
- `TestXxxFromProto`: build proto message, convert, assert each field
- `TestXxxRoundTrip`: SDK -> proto -> SDK, assert equality
- `TestXxxDeepCopy`: convert, mutate source, assert target isolation

Coverage test must pass with all new fields in `handled` set.

`mise run ci` must pass (lint, build, test, proto:check).

## Out of Scope

- Fake client changes (fakes don't need to know about these types)
- New sub-client methods (these are type/converter additions only)
- Proto file changes (we consume upstream proto as-is)
37 changes: 27 additions & 10 deletions openshell/v1/internal/converter/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,37 @@ func TestConverterCoversAllProtoFields_SandboxCondition(t *testing.T) {

func TestConverterCoversAllProtoFields_SandboxPolicy(t *testing.T) {
handled := fieldSet{
"version": true,
"filesystem": true,
"network_policies": true,
"process": true,
"landlock": true,
"version": true,
"filesystem": true,
"network_policies": true,
"process": true,
"landlock": true,
"network_middlewares": true,
}

skipped := fieldSet{
// Middleware support is not yet exposed in the SDK domain model.
// Tracked in GitHub issue #36 for Drop D.
"network_middlewares": true,
assertAllFieldsCovered(t, (&sandboxpb.SandboxPolicy{}).ProtoReflect().Descriptor(), handled, nil)
}

func TestConverterCoversAllProtoFields_NetworkMiddlewareConfig(t *testing.T) {
handled := fieldSet{
"name": true,
"middleware": true,
"config": true,
"on_error": true,
"endpoints": true,
"order": true,
}

assertAllFieldsCovered(t, (&sandboxpb.NetworkMiddlewareConfig{}).ProtoReflect().Descriptor(), handled, nil)
}

func TestConverterCoversAllProtoFields_MiddlewareEndpointSelector(t *testing.T) {
handled := fieldSet{
"include": true,
"exclude": true,
}

assertAllFieldsCovered(t, (&sandboxpb.SandboxPolicy{}).ProtoReflect().Descriptor(), handled, skipped)
assertAllFieldsCovered(t, (&sandboxpb.MiddlewareEndpointSelector{}).ProtoReflect().Descriptor(), handled, nil)
}

func TestConverterCoversAllProtoFields_NetworkEndpoint(t *testing.T) {
Expand Down
42 changes: 42 additions & 0 deletions openshell/v1/internal/converter/network_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func policyNetworkEndpointFromProto(ep *sbv1.NetworkEndpoint) types.PolicyNetwor
WebsocketCredentialRewrite: ep.GetWebsocketCredentialRewrite(),
RequestBodyCredentialRewrite: ep.GetRequestBodyCredentialRewrite(),
AdvisorProposed: ep.GetAdvisorProposed(),
CredentialSigning: ep.GetCredentialSigning(),
SigningService: ep.GetSigningService(),
SigningRegion: ep.GetSigningRegion(),
JSONRPCMaxBodyBytes: ep.GetJsonRpcMaxBodyBytes(),
}
if ports := ep.GetPorts(); len(ports) > 0 {
result.Ports = make([]uint32, len(ports))
Expand Down Expand Up @@ -109,6 +113,7 @@ func policyNetworkEndpointFromProto(ep *sbv1.NetworkEndpoint) types.PolicyNetwor
}
}
}
result.Mcp = mcpOptionsFromProto(ep.GetMcp())
return result
}

Expand All @@ -127,6 +132,10 @@ func policyNetworkEndpointToProto(ep *types.PolicyNetworkEndpoint) *sbv1.Network
WebsocketCredentialRewrite: ep.WebsocketCredentialRewrite,
RequestBodyCredentialRewrite: ep.RequestBodyCredentialRewrite,
AdvisorProposed: ep.AdvisorProposed,
CredentialSigning: ep.CredentialSigning,
SigningService: ep.SigningService,
SigningRegion: ep.SigningRegion,
JsonRpcMaxBodyBytes: ep.JSONRPCMaxBodyBytes,
}
if len(ep.Ports) > 0 {
result.Ports = make([]uint32, len(ep.Ports))
Expand All @@ -153,9 +162,32 @@ func policyNetworkEndpointToProto(ep *types.PolicyNetworkEndpoint) *sbv1.Network
result.GraphqlPersistedQueries[k] = graphqlOperationToProto(&v)
}
}
result.Mcp = mcpOptionsToProto(ep.Mcp)
return result
}

// --- McpOptions ---

func mcpOptionsFromProto(m *sbv1.McpOptions) *types.McpOptions {
if m == nil {
return nil
}
return &types.McpOptions{
StrictToolNames: CopyBoolPtr(m.StrictToolNames),
AllowAllKnownMcpMethods: CopyBoolPtr(m.AllowAllKnownMcpMethods),
}
}

func mcpOptionsToProto(m *types.McpOptions) *sbv1.McpOptions {
if m == nil {
return nil
}
return &sbv1.McpOptions{
StrictToolNames: CopyBoolPtr(m.StrictToolNames),
AllowAllKnownMcpMethods: CopyBoolPtr(m.AllowAllKnownMcpMethods),
}
}

// --- L7Rule ---

func l7RuleFromProto(r *sbv1.L7Rule) types.L7Rule {
Expand All @@ -172,6 +204,9 @@ func l7RuleFromProto(r *sbv1.L7Rule) types.L7Rule {
if q := a.GetQuery(); len(q) > 0 {
result.Allow.Query = l7QueryMapFromProto(q)
}
if p := a.GetParams(); len(p) > 0 {
result.Allow.Params = l7QueryMapFromProto(p)
}
}
return result
}
Expand All @@ -190,6 +225,9 @@ func l7RuleToProto(r *types.L7Rule) *sbv1.L7Rule {
if len(r.Allow.Query) > 0 {
result.Allow.Query = l7QueryMapToProto(r.Allow.Query)
}
if len(r.Allow.Params) > 0 {
result.Allow.Params = l7QueryMapToProto(r.Allow.Params)
}
}
return result
}
Expand All @@ -205,6 +243,7 @@ func l7DenyRuleFromProto(r *sbv1.L7DenyRule) types.L7DenyRule {
OperationName: r.GetOperationName(),
Fields: CopyStringSlice(r.GetFields()),
Query: l7QueryMapFromProtoDeny(r.GetQuery()),
Params: l7QueryMapFromProtoDeny(r.GetParams()),
}
}

Expand All @@ -220,6 +259,9 @@ func l7DenyRuleToProto(r *types.L7DenyRule) *sbv1.L7DenyRule {
if len(r.Query) > 0 {
result.Query = l7QueryMapToProtoDeny(r.Query)
}
if len(r.Params) > 0 {
result.Params = l7QueryMapToProtoDeny(r.Params)
}
return result
}

Expand Down
Loading
Loading