Background
Spun off from PR #469 review (round 2). The Guard's Check method evaluates published policy documents as Layer 1 (caller-side, pre-connection). At that layer, PolicyContext can only be populated with CallerID, CallerDomain, and Protocol. Connection-state fields — AuthType, HasMutualTLS, TLSVersion, DNSSECValidated, CallerTrustScore, Method, Intent, GeoCountry, ConsentToken — cannot be filled in until the connection actually exists.
In ModePermissive (the default) this is fine: rules dependent on those fields produce warnings only.
In ModeStrict, however, any policy that publishes required_auth_types, require_dnssec, require_mutual_tls, min_tls_version, required_caller_trust_score, allowed_methods, allowed_intents, geo_restrictions, or consent_required will unconditionally deny because the zero value never satisfies the rule.
PR #469 documents this prominently in the Guard doc comment but stops short of the code fix.
Proposal
In pkg/openshell/evaluator.go::Evaluate, when a rule applies at the current layer but the corresponding PolicyContext field is at its zero value (i.e., we genuinely don't know the answer yet), emit a warning instead of a violation. Concretely:
required_auth_types with pctx.AuthType == \"\" → warning (not violation)
require_dnssec with !pctx.DNSSECValidated AND layer == LayerCaller → warning
require_mutual_tls with !pctx.HasMutualTLS AND layer == LayerCaller → warning
min_tls_version with empty TLSVersion → warning
required_caller_trust_score with nil score → warning
allowed_methods / allowed_intents with empty value → warning
geo_restrictions with empty GeoCountry → warning
consent_required with empty ConsentToken AND layer == LayerCaller → warning
The signal flips back to violation when (a) we're at Layer 2 with the value populated, or (b) the field is set at Layer 1 and the comparison fails for real.
Acceptance criteria
- ModeStrict at Layer 1 against a policy that requires DNSSEC + mutual TLS no longer denies a connection that hasn't been initiated yet
- ModeStrict at Layer 2 against the same policy still denies if the connection comes back without DNSSEC / mTLS
- Doc-comment limitation note on
Guard can be downgraded or removed
- Table-driven test covering each rule's "unavailable field at Layer 1" path
Out of scope
- Cap-doc fetch / DNSSEC/DANE validation / Phase 6 policy enforcement (separate roadmap items in the integration README)
Context
PR #469 review thread (round 2 follow-up by @mchmarny): #469 (comment)
Background
Spun off from PR #469 review (round 2). The Guard's
Checkmethod evaluates published policy documents as Layer 1 (caller-side, pre-connection). At that layer,PolicyContextcan only be populated withCallerID,CallerDomain, andProtocol. Connection-state fields —AuthType,HasMutualTLS,TLSVersion,DNSSECValidated,CallerTrustScore,Method,Intent,GeoCountry,ConsentToken— cannot be filled in until the connection actually exists.In
ModePermissive(the default) this is fine: rules dependent on those fields produce warnings only.In
ModeStrict, however, any policy that publishesrequired_auth_types,require_dnssec,require_mutual_tls,min_tls_version,required_caller_trust_score,allowed_methods,allowed_intents,geo_restrictions, orconsent_requiredwill unconditionally deny because the zero value never satisfies the rule.PR #469 documents this prominently in the
Guarddoc comment but stops short of the code fix.Proposal
In
pkg/openshell/evaluator.go::Evaluate, when a rule applies at the current layer but the correspondingPolicyContextfield is at its zero value (i.e., we genuinely don't know the answer yet), emit a warning instead of a violation. Concretely:required_auth_typeswithpctx.AuthType == \"\"→ warning (not violation)require_dnssecwith!pctx.DNSSECValidatedANDlayer == LayerCaller→ warningrequire_mutual_tlswith!pctx.HasMutualTLSANDlayer == LayerCaller→ warningmin_tls_versionwith emptyTLSVersion→ warningrequired_caller_trust_scorewithnilscore → warningallowed_methods/allowed_intentswith empty value → warninggeo_restrictionswith emptyGeoCountry→ warningconsent_requiredwith emptyConsentTokenANDlayer == LayerCaller→ warningThe signal flips back to violation when (a) we're at Layer 2 with the value populated, or (b) the field is set at Layer 1 and the comparison fails for real.
Acceptance criteria
Guardcan be downgraded or removedOut of scope
Context
PR #469 review thread (round 2 follow-up by @mchmarny): #469 (comment)