Widen A12 enforcement from the key name to the declaration's content (GH-DEC-2026-020).
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:
parent
2be655703d
commit
e0c6c4389d
7 changed files with 334 additions and 17 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue