From aa61668d4a04bd03faabfb412ece52f2f86b8a2b Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 18 Feb 2026 19:59:06 -0500 Subject: [PATCH] Add filesystem framework loading and clarify starter framework docs --- IMPLEMENTATION_CHECK.md | 2 +- README.md | 16 +++++++-------- cmd/proof/frameworks.go | 13 +++++++----- cmd/proof/root_cmd_test.go | 21 ++++++++++++++++++++ core/framework/framework.go | 34 ++++++++++++++++++++++++++++++++ core/framework/framework_test.go | 27 +++++++++++++++++++++++++ proof_test.go | 17 ++++++++++++++++ 7 files changed, 116 insertions(+), 14 deletions(-) diff --git a/IMPLEMENTATION_CHECK.md b/IMPLEMENTATION_CHECK.md index f63ba23..b5acfdf 100644 --- a/IMPLEMENTATION_CHECK.md +++ b/IMPLEMENTATION_CHECK.md @@ -36,7 +36,7 @@ Status key: | AC5 Offline guarantee | PASS | Core verification is offline-first; cosign path is explicitly local-binary based, no mandatory network dependency in CLI flow. | | AC6 Schema validation | PASS | Invalid/missing fields rejected by schema and validation layers. | | AC7 Custom type | PASS | Runtime custom type registration is supported through CLI/API (`--custom-type-schema`, `RegisterCustomTypeSchema`), and verification validates base + custom schema. | -| AC8 Framework PR only | PASS | Frameworks are YAML-only; no code change required to add files. | +| AC8 Framework PR only | PASS | Built-ins are YAML starter definitions and `LoadFramework(path)` supports runtime loading of custom YAML without code changes. | | AC9 Sigstore parity | PASS | cosign key/cert verification paths cover records/chains and Gait `proof_records.jsonl` verification with Sigstore options. | | AC10 Determinism proof | PASS | Deterministic vector assertions are enforced in tests with a dedicated cross-platform determinism workflow. | | AC11 Gait backward compatibility | PASS | Native Gait pack + signed-JSON verification with key-id compatibility is covered, including committed compatibility fixtures in `testdata/gait_compat/`. | diff --git a/README.md b/README.md index 4c712ec..3fee48b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagNam go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}" proof types list # 15 built-in record types -proof frameworks list # 8 compliance framework definitions +proof frameworks list # 8 built-in starter frameworks (11 controls) proof verify ./artifact # Verify any proof artifact offline ``` @@ -192,20 +192,20 @@ controls: minimum_frequency: continuous ``` -8 frameworks ship with v1: +8 built-in starter frameworks ship with v1 (11 controls total). Add custom frameworks via YAML. | Framework | Scope | |---|---| -| EU AI Act | Articles 9, 12, 13, 14, 15, 26 | -| SOC 2 | CC6, CC7, CC8 (AI-specific sub-controls) | -| SOX | Change management, SoD, access controls | +| EU AI Act | Articles 9, 12, 14 (starter mapping) | +| SOC 2 | CC6, CC7 (starter mapping) | +| SOX | Change management (starter mapping) | | PCI-DSS | Requirement 10 (logging and monitoring) | | Texas TRAIGA | State AI regulation | | Colorado AI Act | State AI regulation | | ISO 42001 | AI Management System | | NIST AI 600-1 | Agent security guidance | -Adding a new framework is a YAML file and a PR — no code changes required. +Built-ins are starter definitions; teams can add custom frameworks via YAML files. ## CLI Reference @@ -232,8 +232,8 @@ proof chain verify Verify chain integrity proof types list List all registered record types proof types validate Validate a custom record type schema -proof frameworks list List available compliance frameworks -proof frameworks show Display framework controls +proof frameworks list List built-in starter framework definitions +proof frameworks show Display framework controls proof completion Shell completion generation ``` diff --git a/cmd/proof/frameworks.go b/cmd/proof/frameworks.go index 6c452e7..4f7b938 100644 --- a/cmd/proof/frameworks.go +++ b/cmd/proof/frameworks.go @@ -9,10 +9,10 @@ import ( ) func newFrameworksCmd(opts *globalOpts) *cobra.Command { - cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework definitions"} + cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework starter definitions"} listCmd := &cobra.Command{ Use: "list", - Short: "List available frameworks", + Short: "List available built-in starter frameworks", RunE: func(cmd *cobra.Command, args []string) error { explainf(opts, "frameworks list") list, err := framework.List() @@ -23,18 +23,21 @@ func newFrameworksCmd(opts *globalOpts) *cobra.Command { printResult(opts, list, "") return nil } + totalControls := 0 for _, f := range list { fmt.Printf("%s\t%s\tcontrols=%d\n", f.ID, f.Version, f.ControlCount) + totalControls += f.ControlCount } + fmt.Printf("%d built-in starter frameworks (%d controls). Add custom frameworks via YAML.\n", len(list), totalControls) return nil }, } showCmd := &cobra.Command{ - Use: "show ", - Short: "Show a framework definition", + Use: "show ", + Short: "Show a framework definition by built-in id or YAML path", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - explainf(opts, "frameworks show id=%s", args[0]) + explainf(opts, "frameworks show target=%s", args[0]) f, err := framework.Load(args[0]) if err != nil { return newCLIError(exitcode.InvalidInput, err.Error()) diff --git a/cmd/proof/root_cmd_test.go b/cmd/proof/root_cmd_test.go index 711dfbf..bba4e4b 100644 --- a/cmd/proof/root_cmd_test.go +++ b/cmd/proof/root_cmd_test.go @@ -23,6 +23,27 @@ func TestFrameworksListCommand(t *testing.T) { out, err := runCLIForTest(t, []string{"frameworks", "list"}) require.NoError(t, err) require.Contains(t, out, "eu-ai-act") + require.Contains(t, out, "built-in starter frameworks") +} + +func TestFrameworksShowByPathCommand(t *testing.T) { + path := filepath.Join(t.TempDir(), "custom-framework.yaml") + require.NoError(t, os.WriteFile(path, []byte(` +framework: + id: custom-framework + version: "1" + title: Custom Framework +controls: + - id: custom-control + title: Custom Control + required_record_types: [decision] + required_fields: [record_id] + minimum_frequency: continuous +`), 0o644)) + + out, err := runCLIForTest(t, []string{"frameworks", "show", path, "--json"}) + require.NoError(t, err) + require.Contains(t, out, `"id": "custom-framework"`) } func TestInspectRecordCommand(t *testing.T) { diff --git a/core/framework/framework.go b/core/framework/framework.go index e664237..8444320 100644 --- a/core/framework/framework.go +++ b/core/framework/framework.go @@ -3,6 +3,7 @@ package framework import ( "embed" "fmt" + "os" "path/filepath" "sort" "strings" @@ -66,6 +67,18 @@ func List() ([]Info, error) { } func Load(idOrFile string) (*Framework, error) { + if info, err := os.Stat(idOrFile); err == nil { + if info.IsDir() { + if isLikelyPath(idOrFile) { + return nil, fmt.Errorf("load framework file %s: path is a directory", idOrFile) + } + } else { + return LoadFile(idOrFile) + } + } else if isLikelyPath(idOrFile) { + return nil, fmt.Errorf("load framework file %s: %w", idOrFile, err) + } + name := idOrFile if !strings.HasSuffix(name, ".yaml") { name = name + ".yaml" @@ -77,6 +90,27 @@ func Load(idOrFile string) (*Framework, error) { return parseFramework(idOrFile, raw) } +func LoadFile(path string) (*Framework, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("load framework file %s: %w", path, err) + } + return parseFramework(path, raw) +} + +func isLikelyPath(value string) bool { + if value == "" { + return false + } + if filepath.IsAbs(value) { + return true + } + if strings.HasPrefix(value, ".") { + return true + } + return strings.ContainsAny(value, `/\`) +} + func parseFramework(idOrFile string, raw []byte) (*Framework, error) { var f Framework if err := yaml.Unmarshal(raw, &f); err != nil { diff --git a/core/framework/framework_test.go b/core/framework/framework_test.go index ad43bb6..04e755a 100644 --- a/core/framework/framework_test.go +++ b/core/framework/framework_test.go @@ -30,6 +30,33 @@ func TestLoadMissingAndCountControls(t *testing.T) { require.Equal(t, 4, total) } +func TestLoadFromFilesystemPath(t *testing.T) { + path := filepath.Join(t.TempDir(), "custom-framework.yaml") + require.NoError(t, os.WriteFile(path, []byte(` +framework: + id: custom-framework + version: "1" + title: Custom Framework +controls: + - id: custom-control + title: Custom Control + required_record_types: [decision] + required_fields: [record_id] + minimum_frequency: continuous +`), 0o644)) + + f, err := Load(path) + require.NoError(t, err) + require.Equal(t, "custom-framework", f.Framework.ID) + require.Len(t, f.Controls, 1) +} + +func TestLoadMissingFilesystemPath(t *testing.T) { + _, err := Load(filepath.Join(t.TempDir(), "missing.yaml")) + require.Error(t, err) + require.ErrorContains(t, err, "load framework file") +} + func TestValidateControls(t *testing.T) { valid := []Control{ { diff --git a/proof_test.go b/proof_test.go index 6b731e4..21764e7 100644 --- a/proof_test.go +++ b/proof_test.go @@ -99,6 +99,23 @@ func TestAPIHelpers(t *testing.T) { f, err := LoadFramework("eu-ai-act") require.NoError(t, err) require.Equal(t, "eu-ai-act", f.Framework.ID) + + path := filepath.Join(t.TempDir(), "custom-framework.yaml") + require.NoError(t, os.WriteFile(path, []byte(` +framework: + id: custom-framework + version: "1" + title: Custom Framework +controls: + - id: custom-control + title: Custom Control + required_record_types: [decision] + required_fields: [record_id] + minimum_frequency: continuous +`), 0o644)) + custom, err := LoadFramework(path) + require.NoError(t, err) + require.Equal(t, "custom-framework", custom.Framework.ID) } func TestWriteReadAndCustomSchemaValidation(t *testing.T) {