ValidatedAgainst named the proposed v0.8, which is held under GH-DEC-2026-019, so every run claimed a check against text that does not govern. It now names security-layer-model_v0.7.md (net-kingdom@66dc491) as amended by GH-DEC-2026-017, -020 and -021 (gate-house@39d9287), per 021 §2. The pin detector converges on ops-warden's reference (021 §3): keys naming a standard or companion version, versions in path or file-name tokens, and the 021 addition of any version token in a standard:/companion: value. A revision cited in prose is provenance and is no longer reached (021 §1), including under standard_*-prefixed keys that name no version. One kept difference: an empty version key carries no version and is not flagged. Survey receipt refreshed; no declaration in the estate carries a pin. 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
378 lines
15 KiB
Go
378 lines
15 KiB
Go
// Package layer asserts the NetKingdom security-layer-model §11 declaration.
|
|
package layer
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Layer vocabulary from the Security Layer Model §3, as stated once and closed
|
|
// by amendment A9 (GH-DEC-2026-017 §2 and §3).
|
|
//
|
|
// It has FOUR tokens, not three. The earlier set here admitted
|
|
// {Staff, Engine, Tooling} and rejected Taxonomy — which §3.1 defines, §4
|
|
// catalogues twice (info-tech-canon, net-kingdom), and the standard itself is
|
|
// an instance of. That set was built from §4's role-typed catalog rows rather
|
|
// than from §3's layer table, so it inherited §4's shape and lost §3's fourth
|
|
// row. railiance-master's `Taxonomy` was conforming the whole time and this
|
|
// validator was the divergent artifact; see docs/conformance/boundaries-review.md.
|
|
//
|
|
// The spelling below is §4's Layer column form, which GH-DEC-2026-017 §2 makes
|
|
// canonical. §3's table heading reads `Engines`, plural; the token is `Engine`.
|
|
// Comparison is ASCII case-insensitive and a run MUST fold case before
|
|
// comparing (A9): two spellings of Engine do not describe two boundaries, and a
|
|
// check that reports nine findings about capital letters has buried the one
|
|
// real disagreement it exists to find.
|
|
var canonicalLayers = []string{"Taxonomy", "Tooling", "Engine", "Staff"}
|
|
|
|
// foldedLayers maps the ASCII-case-folded form of each token to its canonical
|
|
// §4 spelling.
|
|
var foldedLayers = func() map[string]string {
|
|
m := make(map[string]string, len(canonicalLayers))
|
|
for _, l := range canonicalLayers {
|
|
m[strings.ToLower(l)] = l
|
|
}
|
|
return m
|
|
}()
|
|
|
|
// CanonicalLayer folds ASCII case and returns the §4 column spelling of a
|
|
// declared layer value, plus whether the value is in the §3 vocabulary at all.
|
|
// A lowercase declaration is conforming, not tolerated (GH-DEC-2026-017 §2).
|
|
func CanonicalLayer(declared string) (string, bool) {
|
|
canon, ok := foldedLayers[strings.ToLower(strings.TrimSpace(declared))]
|
|
return canon, ok
|
|
}
|
|
|
|
// 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).
|
|
//
|
|
// It names the ACCEPTED v0.7 text plus the gate-house decisions this checker
|
|
// enforces beyond it, pinned by commit (GH-DEC-2026-021 §2). v0.8 is held under
|
|
// GH-DEC-2026-019, so a run must not claim a check against it; the re-point to
|
|
// v0.8 belongs to the post-flip commit (GH-WP-0004-T11).
|
|
const ValidatedAgainst = "net-kingdom/canon/standards/security-layer-model_v0.7.md (net-kingdom@66dc491) as amended by GH-DEC-2026-017, GH-DEC-2026-020, GH-DEC-2026-021 (gate-house@39d9287)"
|
|
|
|
// Engine roles from §3.3. An Engine declaration must state one.
|
|
var validEngineRoles = map[string]bool{
|
|
"PDP": true,
|
|
"PIP": true,
|
|
}
|
|
|
|
// Tooling clients are invocations, not mentions. These match import paths and
|
|
// argv construction that would actually contact OpenBao/Vault.
|
|
var toolingPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`github\.com/hashicorp/vault`),
|
|
regexp.MustCompile(`github\.com/openbao/`),
|
|
regexp.MustCompile(`exec\.Command\([^)]*["'](?:bao|vault)["']`),
|
|
}
|
|
|
|
// Declaration is the machine-readable §11 form carried in INTENT.md frontmatter.
|
|
type Declaration struct {
|
|
Layer string `yaml:"layer"`
|
|
Role string `yaml:"role"`
|
|
Framework string `yaml:"framework"`
|
|
DeclaredBy string `yaml:"declared_by"`
|
|
DeclaredAt string `yaml:"declared_at"`
|
|
// ConformanceRecord points at the derived, version-stamped conformance state.
|
|
// The declaration itself is a boundary and carries no standard version.
|
|
ConformanceRecord string `yaml:"conformance_record"`
|
|
// StandardVersion must stay empty: a layer is a boundary, and the standard
|
|
// says assent is "to a BOUNDARY, given at the version named. It is not
|
|
// assent to the current text."
|
|
StandardVersion string `yaml:"standard_version"`
|
|
PepStance any `yaml:"pep_stance"`
|
|
ToolingContacts []any `yaml:"tooling_contacts"`
|
|
// SourceOfEvidence records whether §4 marks this repository as a source of
|
|
// evidence (A10). GH-DEC-2026-018 §2 rules that flex-auth is one: custody is
|
|
// never source, and the decision record is emitted by the repository that
|
|
// renders the decision.
|
|
SourceOfEvidence *bool `yaml:"source_of_evidence"`
|
|
// EmissionGuarantee names the per-event-class emission declaration §11
|
|
// requires of a marked source. GH-DEC-2026-018 §3 rules that the declaration
|
|
// is PER EVENT CLASS, not per repository: one guarantee averaged over a
|
|
// stream holding both a high-volume allow and a rare deny is not a
|
|
// declaration, and would be satisfied by rate monitoring that cannot see the
|
|
// deny go missing.
|
|
EmissionGuarantee string `yaml:"emission_guarantee"`
|
|
}
|
|
|
|
// Check parses INTENT.md, asserts the Engine/PDP declaration, and scans
|
|
// production Go sources for undeclared Tooling clients.
|
|
func Check(root string) error {
|
|
decl, err := LoadDeclaration(filepath.Join(root, "INTENT.md"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ValidateDeclaration(decl); err != nil {
|
|
return err
|
|
}
|
|
for _, named := range []struct{ field, path string }{
|
|
{"conformance_record", decl.ConformanceRecord},
|
|
{"emission_guarantee", decl.EmissionGuarantee},
|
|
} {
|
|
if strings.TrimSpace(named.path) == "" {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, named.path)); err != nil {
|
|
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
|
|
}
|
|
if len(hits) > 0 {
|
|
return fmt.Errorf("undeclared Tooling client(s) under §11: %s", strings.Join(hits, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadDeclaration reads YAML frontmatter from INTENT.md.
|
|
func LoadDeclaration(path string) (Declaration, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Declaration{}, fmt.Errorf("read layer declaration: %w", err)
|
|
}
|
|
frontmatter, err := splitFrontmatter(string(data))
|
|
if err != nil {
|
|
return Declaration{}, err
|
|
}
|
|
return parseDeclarationYAML(frontmatter)
|
|
}
|
|
|
|
// parseDeclarationYAML unmarshals a bare declaration document. Shared with the
|
|
// estate survey, which reads layer.yaml files that carry no frontmatter fence.
|
|
func parseDeclarationYAML(doc string) (Declaration, error) {
|
|
var decl Declaration
|
|
if err := yaml.Unmarshal([]byte(doc), &decl); err != nil {
|
|
return Declaration{}, fmt.Errorf("parse layer declaration: %w", err)
|
|
}
|
|
return decl, nil
|
|
}
|
|
|
|
// ValidateDeclaration asserts §3 vocabulary and Engine-role presence.
|
|
func ValidateDeclaration(decl Declaration) error {
|
|
canon, ok := CanonicalLayer(decl.Layer)
|
|
if !ok {
|
|
return fmt.Errorf("layer %q is not in the §3 vocabulary (%s); the vocabulary is closed and a fifth layer arrives by amending §3, not by declaring one", decl.Layer, strings.Join(canonicalLayers, ", "))
|
|
}
|
|
if canon == "Engine" && !validEngineRoles[decl.Role] {
|
|
return fmt.Errorf("Engine declaration must state role PDP or PIP; got %q", decl.Role)
|
|
}
|
|
if canon != "Engine" && strings.TrimSpace(decl.Role) != "" {
|
|
return fmt.Errorf("layer %q must not state an Engine role", decl.Layer)
|
|
}
|
|
if len(decl.ToolingContacts) > 0 {
|
|
return fmt.Errorf("Engine/PDP holds no Tooling client; tooling_contacts must be empty")
|
|
}
|
|
if decl.PepStance != nil {
|
|
return fmt.Errorf("flex-auth is not PEP-shaped; pep_stance must be null")
|
|
}
|
|
if strings.TrimSpace(decl.StandardVersion) != "" {
|
|
return fmt.Errorf("layer declaration must carry no standard_version: a layer is a boundary, not a version-scoped claim; version-stamped state belongs in conformance_record (got %q)", decl.StandardVersion)
|
|
}
|
|
if strings.TrimSpace(decl.ConformanceRecord) == "" {
|
|
return fmt.Errorf("layer declaration must name a conformance_record: §11 requires a derived artifact to carry the version it was derived at")
|
|
}
|
|
if decl.SourceOfEvidence == nil {
|
|
return fmt.Errorf("layer declaration must state source_of_evidence: §4 marks it (A10) and §11's emission check may not infer it — GH-DEC-2026-018 §5")
|
|
}
|
|
if *decl.SourceOfEvidence && strings.TrimSpace(decl.EmissionGuarantee) == "" {
|
|
return fmt.Errorf("a §4 evidence source must name an emission_guarantee declaration, per event class: §11, GH-DEC-2026-018 §3")
|
|
}
|
|
if !*decl.SourceOfEvidence && strings.TrimSpace(decl.EmissionGuarantee) != "" {
|
|
return fmt.Errorf("emission_guarantee is declared but source_of_evidence is false")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A12 reaches a PIN, not a citation (GH-DEC-2026-020 §1, narrowed by
|
|
// GH-DEC-2026-021 §1). This detector is converged on the estate reference in
|
|
// ops-warden's playbook (wiki/playbooks/netkingdom-layer-declaration.md;
|
|
// GH-DEC-2026-021 §3), with the one addition that ruling makes. A pin is:
|
|
//
|
|
// - a key naming a standard or companion version (standard_version,
|
|
// companion_version, standard_version_reviewed), with a value;
|
|
// - a version in a path or file-name token of any value (`_v0.7`,
|
|
// `-v0.8.md`, `@0.7`) — the reference errs wide here, deliberately;
|
|
// - any version token (v?N.N) in the value of a `standard:` or `companion:`
|
|
// key (the kings-guard addition: `standard: security-layer-model v0.7`).
|
|
//
|
|
// Not reached: a revision cited in prose ("the v0.5 scope rule"), which is
|
|
// provenance; `schema_version`; a key such as `intent_version` that names no
|
|
// standard or companion; and comments, which a YAML parse never surfaces.
|
|
var (
|
|
// versionKey is the reference VERSION_KEY.
|
|
versionKey = regexp.MustCompile(`(?i)(standard|companion).*version|version.*(standard|companion)`)
|
|
// versionInValue is the reference VERSION_IN_VALUE. Go's RE2 has no \b, so
|
|
// the trailing word boundary is spelled out.
|
|
versionInValue = regexp.MustCompile(`(?i)(?:[_\-.]v\d+(?:\.\d+)*(?:\.md)?(?:[^a-z0-9_]|$))|@v?\d+\.\d+`)
|
|
// versionToken is the GH-DEC-2026-021 §3 addition, applied only to the
|
|
// value of a `standard:` or `companion:` key.
|
|
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 versionKey.MatchString(name) {
|
|
// An empty key carries no version, so it is not a pin; the
|
|
// reference flags the key alone. flex-auth's own Declaration
|
|
// keeps an always-empty standard_version field.
|
|
if !isEmptyNode(v) {
|
|
*out = append(*out, fmt.Sprintf("%s: %s", k.Value, nodeText(v)))
|
|
}
|
|
continue
|
|
}
|
|
walkPins(v, name, out)
|
|
}
|
|
case yaml.ScalarNode:
|
|
identity := key == "standard" || key == "companion"
|
|
if versionInValue.MatchString(n.Value) || (identity && 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
|
|
for _, dir := range []string{"cmd", "internal", "pkg"} {
|
|
err := filepath.WalkDir(filepath.Join(root, dir), func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pattern := range toolingPatterns {
|
|
if pattern.Find(body) != nil {
|
|
rel, _ := filepath.Rel(root, path)
|
|
hits = append(hits, rel)
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
return hits, nil
|
|
}
|
|
|
|
func splitFrontmatter(document string) (string, error) {
|
|
document = strings.TrimPrefix(document, "\ufeff")
|
|
lines := strings.SplitAfter(document, "\n")
|
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
return "", fmt.Errorf("INTENT.md must start with YAML frontmatter")
|
|
}
|
|
for i := 1; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
return strings.Join(lines[1:i], ""), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("INTENT.md frontmatter is not closed")
|
|
}
|