Skip to content

feat(policy): distinguish single- and multi-segment path wildcards #2586

Description

@johntmyers

Problem Statement

OpenShell currently evaluates path globs with separator-insensitive matching. Both * and ** may consume /, so patterns such as /v1/* and /v1/** overlap more than operators commonly expect. A trailing /** has a special subtree behavior—it also matches the named path itself—but otherwise the two wildcard forms are largely redundant.

This is surprising for profile and policy authors. Common routing conventions treat * as a wildcard within one path component and ** as a recursive wildcard across components. The current behavior makes it easy for /v1/* to authorize /v1/admin/secrets unintentionally.

This came up while reviewing static credential endpoint binding in PR #2510, particularly this discussion. It should be addressed as a deliberate policy-language change rather than changing credential bindings alone.

Proposed Design

Define one shared path-glob contract across all network-policy consumers:

  • * matches zero or more characters within one path component and never consumes /.
  • ** matches zero or more characters across path components.
  • ? and bracket classes remain component-local and retain their documented character-matching behavior.
  • A trailing /prefix/** continues to match /prefix itself as well as all descendants.
  • Request paths remain canonicalized before any policy or credential-binding match.

Apply the contract consistently to:

  • L7 endpoint selection
  • REST and WebSocket allow/deny rules
  • Static credential endpoint bindings
  • Ambiguity and overlap validation
  • Any policy prover representation of path globs

The runtime matcher, ambiguity validator, documentation, examples, and tests must all use the same semantics. Because OpenShell is currently alpha, treat this as an intentional breaking policy-language correction and call it out in release notes. Before implementation, inventory repository policies and tests that rely on * crossing / and rewrite them to use ** where recursive matching is intended.

Representative acceptance cases:

Pattern Path Result
/v1/* /v1/chat match
/v1/* /v1/chat/messages no match
/v1/** /v1 match
/v1/** /v1/chat/messages match
/repos/*/issues /repos/acme/issues match
/repos/*/issues /repos/acme/private/issues no match
/**/info/refs* /org/repo/info/refs match

Alternatives Considered

  • Keep the current semantics. This preserves compatibility but leaves * and ** mostly redundant and retains a surprising authorization boundary.
  • Change only static credential bindings. This would make credential paths safer in isolation, but operators would have to understand two incompatible path languages in the same policy/profile workflow.
  • Introduce a new wildcard token. This avoids changing existing patterns but adds more syntax when the conventional */** distinction already expresses the intended model.

Agent Investigation

  • crates/openshell-core/src/endpoint_path.rs uses glob::Pattern::matches, where separators are not treated specially, and explicitly tests that /v1/* matches /v1/chat/messages.
  • The same matcher special-cases trailing /** so /v1/** matches both /v1 and its descendants.
  • crates/openshell-policy/src/ambiguity.rs models both * and ** as delimiter-crossing wildcard tokens to mirror runtime behavior.
  • docs/sandboxes/policies.mdx and docs/reference/policy-schema.mdx explicitly document that both wildcard forms may cross /.
  • No open duplicate issue was found. Closed issue feat(policy): spike policy envelope and narrowness checks #1840 concerns broader policy-envelope and narrowness checks rather than wildcard semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions