Widen A12 enforcement from the key name to the declaration's content (GH-DEC-2026-020).
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m11s

internal/layer/conformance.go enforced A12 as "no key named standard_version",
and so could not see the same pin as a versioned standard: path or as
companion_version. It now detects a version of the standard or its companion
in any key or value of the declaration (INTENT.md frontmatter, layer.yaml),
including a version in a path, and excludes comments and schema_version. It
refuses to be applied to pep-stance.yaml, pip-claims.yaml or
evidence-classification.yaml, which A12 r2 does not reach (§3).

Every run of check_layer_conformance and of the estate survey now prints the
standard version it checks against (layer.ValidatedAgainst, kings-guard's
pattern) and its scope (§4). The survey applies the same detection to peers'
declarations; the receipt is refreshed because the survey's output changed
(no peer declaration currently carries a version).

Tests fail if a versioned standard: path or a companion_version comes back.
flex-auth's own INTENT.md needed no change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
This commit is contained in:
tegwick 2026-09-21 09:39:46 +02:00
parent 2be655703d
commit e0c6c4389d
7 changed files with 334 additions and 17 deletions

View file

@ -51,6 +51,15 @@ func CanonicalLayer(declared string) (string, bool) {
// Vocabulary returns the four §3 tokens in their canonical spelling.
func Vocabulary() []string { return append([]string(nil), canonicalLayers...) }
// ValidatedAgainst names the text this package checks against. A12 r2
// (GH-DEC-2026-020 §4) moves version-scoped state out of the declaration and
// into the run: every run MUST print the version or commit of the standard it
// checked against, and its scope. This constant lives in the checker, which is
// not a declaration, so A12 does not reach it. When the canon moves it goes
// visibly stale in every run's output — which is the point (kings-guard's
// VALIDATED_AGAINST pattern, adopted as the reference by GH-DEC-2026-020).
const ValidatedAgainst = "net-kingdom security-layer-model v0.8 with amendments A9-A13 (A12 r2), gate-house@104f3fc (GH-DEC-2026-017, GH-DEC-2026-020)"
// Engine roles from §3.3. An Engine declaration must state one.
var validEngineRoles = map[string]bool{
"PDP": true,
@ -116,6 +125,11 @@ func Check(root string) error {
return fmt.Errorf("%s names %q, which is not on disk: §11's derived-artifact rule needs the artifact, not the pointer", named.field, named.path)
}
}
if pins, err := DeclarationVersionPins(filepath.Join(root, "INTENT.md")); err != nil {
return err
} else if len(pins) > 0 {
return fmt.Errorf("layer declaration carries a standard or companion version (A12 r2, GH-DEC-2026-020 §1-§2): %s", strings.Join(pins, "; "))
}
hits, err := ScanToolingClients(root)
if err != nil {
return err
@ -185,6 +199,123 @@ func ValidateDeclaration(decl Declaration) error {
return nil
}
// A12 r2 reaches CONTENT, not a key name (GH-DEC-2026-020 §1). Every checker in
// the estate, this one included, enforced A12 as "no key named standard_version"
// and so could not see the same pin as `standard: …security-layer-model_v0.7.md`
// or as `companion_version: "0.2"`. These detect a version of the standard or of
// its companion anywhere in the declaration.
var (
// pinKeys are keys whose presence with a value IS the pin (§1, §2).
pinKeys = map[string]bool{"standard_version": true, "companion_version": true}
// versionedRef matches a reference to the standard or its companion that
// carries a version in its path or name, whatever key holds it.
versionedRef = regexp.MustCompile(`(?i)(security-layer-model|security-companion|standard-companion)[^\s]*?[_@/-]v?\d+(\.\d+)+`)
// versionToken matches a bare version, e.g. 0.8 or v0.7, bounded so a date
// or a decision id does not count. Applied only under standard*/companion* keys.
versionToken = regexp.MustCompile(`(?i)(?:^|[^a-z0-9.])v?\d+\.\d+`)
)
// notReached are keys A12 r2 states it does not reach: the version of a
// declaration file's own schema is not a version of the standard. Comments are
// not reached either, and a YAML parse never surfaces them.
var notReached = map[string]bool{"schema_version": true}
// DeclarationVersionPins reads a declaration — INTENT.md frontmatter, or a bare
// derived declaration file such as layer.yaml — and returns every key or value
// carrying a version of the standard or its companion.
//
// It MUST be pointed at a declaration only. A stance map, claims map or
// evidence classification (pep-stance.yaml, pip-claims.yaml,
// evidence-classification.yaml) SHOULD carry the version of the clause text it
// answers, and A12 r2 forbids a run applying this rule there (GH-DEC-2026-020
// §3). Nothing in this package calls it on such a file.
func DeclarationVersionPins(path string) ([]string, error) {
if !isDeclarationPath(path) {
return nil, fmt.Errorf("%s is not a §11 declaration (INTENT.md or layer.yaml); A12 r2 does not reach it and a run MUST NOT apply it there", filepath.Base(path))
}
body, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read layer declaration: %w", err)
}
doc := string(body)
if strings.HasSuffix(path, ".md") {
if doc, err = splitFrontmatter(doc); err != nil {
return nil, err
}
}
return VersionPins(doc)
}
func isDeclarationPath(path string) bool {
switch filepath.Base(path) {
case "INTENT.md", "layer.yaml", "layer.yml":
return true
}
return false
}
// VersionPins scans a bare declaration YAML document. Findings are stable,
// human-readable "key: value" strings in document order.
func VersionPins(doc string) ([]string, error) {
var root yaml.Node
if err := yaml.Unmarshal([]byte(doc), &root); err != nil {
return nil, fmt.Errorf("parse layer declaration: %w", err)
}
var out []string
walkPins(&root, "", &out)
return out, nil
}
func walkPins(n *yaml.Node, key string, out *[]string) {
switch n.Kind {
case yaml.DocumentNode, yaml.SequenceNode:
for _, c := range n.Content {
walkPins(c, key, out)
}
case yaml.MappingNode:
for i := 0; i+1 < len(n.Content); i += 2 {
k, v := n.Content[i], n.Content[i+1]
name := strings.ToLower(strings.TrimSpace(k.Value))
if notReached[name] {
continue
}
if versionedRef.MatchString(k.Value) {
*out = append(*out, fmt.Sprintf("key %q names a versioned standard or companion", k.Value))
}
if pinKeys[name] && !isEmptyNode(v) {
*out = append(*out, fmt.Sprintf("%s: %s", k.Value, nodeText(v)))
continue
}
walkPins(v, name, out)
}
case yaml.ScalarNode:
standardish := key == "standard" || key == "companion" ||
strings.HasPrefix(key, "standard_") || strings.HasPrefix(key, "companion_")
if versionedRef.MatchString(n.Value) || (standardish && versionToken.MatchString(n.Value)) {
*out = append(*out, fmt.Sprintf("%s: %s", dash(key), n.Value))
}
case yaml.AliasNode:
if n.Alias != nil {
walkPins(n.Alias, key, out)
}
}
}
func isEmptyNode(n *yaml.Node) bool {
if n.Kind == yaml.ScalarNode {
return n.Tag == "!!null" || strings.TrimSpace(n.Value) == ""
}
return len(n.Content) == 0
}
func nodeText(n *yaml.Node) string {
if n.Kind == yaml.ScalarNode {
return n.Value
}
b, _ := yaml.Marshal(n)
return strings.TrimSpace(string(b))
}
// ScanToolingClients returns production Go files that invoke OpenBao/Vault.
func ScanToolingClients(root string) ([]string, error) {
var hits []string