Severity: Medium. Loader strictness (the security-relevant half) is item 14; this item is the UX/fidelity half.
Lint gives false confidence
pkg/policyyaml/lint.go:22-30 claims to "reimplement the server-side validate_sandbox_policy" then admits a subset. Verified: host: "*" + access: full + enforcement: audit + allow_uninspected_credentials: true + read_write: ["/"] yields only the / finding; version: 1 alone is "no problems found" and TestPolicyLint_Clean enshrines that. No checks on protocol/tls/enforcement/access/compatibility enum values, empty host, empty allow rule, allowed_ips syntax. Two bugs: lint.go:82 uses strings.Contains(path, "..") (flags /opt/a..b while the message says "component"); lint.go:195 hardcodes endpoint index 0 for every endpoint, and the message mentions rules/deny_rules but only Access is checked (:194). Fix: either port the documented rule set from crates/openshell-policy (lib.rs, l7_validate.rs at v0.0.116, plan Appendix B.2B) or make the "subset" caveat loud in policy lint --help; fix both bugs regardless.
No client-side pattern validation at all
pkg/policyyaml/matchers.go is purely the untagged QueryMatcher/ParamMatcher shape decoder; there is no glob/path/regex matching or syntax validation in the client. That means no divergent-semantics risk versus the server, but also that lint cannot catch a malformed glob. State this in the policy lint help and README so nobody assumes patterns are validated locally.
Lossy round-trip
pkg/policyyaml/serialize.go:69-117 never emits json_rpc.max_body_bytes/mcp.max_body_bytes (only GraphqlMaxBodyBytes at :83; JSONRPCMaxBodyBytes: 7000 → 0 after reparse); :157-162 turns {any: []} into glob "" (semantics change); insertNested (:177-195) collides on flat keys a and a.b (the leaf a is overwritten); the tool restoration promised at :11-14 is not implemented. TestSerialize_StructuralYAML checks four substrings; TestFromSDK_RoundTripStructure checks ~6 fields; nothing asserts DeepEqual on a full fixture. Byte-parity with upstream --policy-only (indentless sequences, quoting) is a separate acknowledged gap (serialize.go:213-217, item 18).
Loading
pkg/policyyaml/load.go:58 sigs.k8s.io/yaml.UnmarshalStrict reads only the first YAML document; version: 1\n---\nversion: 2\nbogus: 1 parses as version 1 with no error. No size limit on ReadFile (load.go:23, internal/cli/policy.go:27). policy lint has no stdin - support, unlike -f.
Acceptance
- load→serialize→load DeepEqual on the content-guard fixture (plan Appendix B) including
max_body_bytes and empty any.
- Multi-document policy → error naming the second document; > N MiB file → error.
- Lint table test: each dangerous shape above produces a finding with the correct endpoint index;
/opt/a..b produces none.
Generated by Claude Code
Severity: Medium. Loader strictness (the security-relevant half) is item 14; this item is the UX/fidelity half.
Lint gives false confidence
pkg/policyyaml/lint.go:22-30claims to "reimplement the server-side validate_sandbox_policy" then admits a subset. Verified:host: "*"+access: full+enforcement: audit+allow_uninspected_credentials: true+read_write: ["/"]yields only the/finding;version: 1alone is "no problems found" andTestPolicyLint_Cleanenshrines that. No checks onprotocol/tls/enforcement/access/compatibilityenum values, empty host, empty allow rule,allowed_ipssyntax. Two bugs:lint.go:82usesstrings.Contains(path, "..")(flags/opt/a..bwhile the message says "component");lint.go:195hardcodes endpoint index0for every endpoint, and the message mentions rules/deny_rules but onlyAccessis checked (:194). Fix: either port the documented rule set fromcrates/openshell-policy(lib.rs,l7_validate.rsat v0.0.116, plan Appendix B.2B) or make the "subset" caveat loud inpolicy lint --help; fix both bugs regardless.No client-side pattern validation at all
pkg/policyyaml/matchers.gois purely the untaggedQueryMatcher/ParamMatchershape decoder; there is no glob/path/regex matching or syntax validation in the client. That means no divergent-semantics risk versus the server, but also thatlintcannot catch a malformed glob. State this in thepolicy linthelp and README so nobody assumes patterns are validated locally.Lossy round-trip
pkg/policyyaml/serialize.go:69-117never emitsjson_rpc.max_body_bytes/mcp.max_body_bytes(onlyGraphqlMaxBodyBytesat:83;JSONRPCMaxBodyBytes: 7000→ 0 after reparse);:157-162turns{any: []}into glob""(semantics change);insertNested(:177-195) collides on flat keysaanda.b(the leafais overwritten); thetoolrestoration promised at:11-14is not implemented.TestSerialize_StructuralYAMLchecks four substrings;TestFromSDK_RoundTripStructurechecks ~6 fields; nothing asserts DeepEqual on a full fixture. Byte-parity with upstream--policy-only(indentless sequences, quoting) is a separate acknowledged gap (serialize.go:213-217, item 18).Loading
pkg/policyyaml/load.go:58sigs.k8s.io/yaml.UnmarshalStrictreads only the first YAML document;version: 1\n---\nversion: 2\nbogus: 1parses as version 1 with no error. No size limit onReadFile(load.go:23,internal/cli/policy.go:27).policy linthas no stdin-support, unlike-f.Acceptance
max_body_bytesand emptyany./opt/a..bproduces none.Generated by Claude Code