Skip to content

Commit 7ec1233

Browse files
authored
Add filesystem framework loading and clarify starter framework docs (#9)
1 parent a31eb40 commit 7ec1233

File tree

7 files changed

+116
-14
lines changed

7 files changed

+116
-14
lines changed

IMPLEMENTATION_CHECK.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Status key:
3636
| AC5 Offline guarantee | PASS | Core verification is offline-first; cosign path is explicitly local-binary based, no mandatory network dependency in CLI flow. |
3737
| AC6 Schema validation | PASS | Invalid/missing fields rejected by schema and validation layers. |
3838
| AC7 Custom type | PASS | Runtime custom type registration is supported through CLI/API (`--custom-type-schema`, `RegisterCustomTypeSchema`), and verification validates base + custom schema. |
39-
| AC8 Framework PR only | PASS | Frameworks are YAML-only; no code change required to add files. |
39+
| AC8 Framework PR only | PASS | Built-ins are YAML starter definitions and `LoadFramework(path)` supports runtime loading of custom YAML without code changes. |
4040
| AC9 Sigstore parity | PASS | cosign key/cert verification paths cover records/chains and Gait `proof_records.jsonl` verification with Sigstore options. |
4141
| AC10 Determinism proof | PASS | Deterministic vector assertions are enforced in tests with a dedicated cross-platform determinism workflow. |
4242
| 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/`. |

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagNam
4141
go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}"
4242

4343
proof types list # 15 built-in record types
44-
proof frameworks list # 8 compliance framework definitions
44+
proof frameworks list # 8 built-in starter frameworks (11 controls)
4545
proof verify ./artifact # Verify any proof artifact offline
4646
```
4747

@@ -192,20 +192,20 @@ controls:
192192
minimum_frequency: continuous
193193
```
194194

195-
8 frameworks ship with v1:
195+
8 built-in starter frameworks ship with v1 (11 controls total). Add custom frameworks via YAML.
196196

197197
| Framework | Scope |
198198
|---|---|
199-
| EU AI Act | Articles 9, 12, 13, 14, 15, 26 |
200-
| SOC 2 | CC6, CC7, CC8 (AI-specific sub-controls) |
201-
| SOX | Change management, SoD, access controls |
199+
| EU AI Act | Articles 9, 12, 14 (starter mapping) |
200+
| SOC 2 | CC6, CC7 (starter mapping) |
201+
| SOX | Change management (starter mapping) |
202202
| PCI-DSS | Requirement 10 (logging and monitoring) |
203203
| Texas TRAIGA | State AI regulation |
204204
| Colorado AI Act | State AI regulation |
205205
| ISO 42001 | AI Management System |
206206
| NIST AI 600-1 | Agent security guidance |
207207

208-
Adding a new framework is a YAML file and a PR — no code changes required.
208+
Built-ins are starter definitions; teams can add custom frameworks via YAML files.
209209

210210
## CLI Reference
211211

@@ -232,8 +232,8 @@ proof chain verify <path> Verify chain integrity
232232
proof types list List all registered record types
233233
proof types validate <schema-path> Validate a custom record type schema
234234
235-
proof frameworks list List available compliance frameworks
236-
proof frameworks show <id> Display framework controls
235+
proof frameworks list List built-in starter framework definitions
236+
proof frameworks show <id|path> Display framework controls
237237
238238
proof completion <shell> Shell completion generation
239239
```

cmd/proof/frameworks.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
)
1010

1111
func newFrameworksCmd(opts *globalOpts) *cobra.Command {
12-
cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework definitions"}
12+
cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework starter definitions"}
1313
listCmd := &cobra.Command{
1414
Use: "list",
15-
Short: "List available frameworks",
15+
Short: "List available built-in starter frameworks",
1616
RunE: func(cmd *cobra.Command, args []string) error {
1717
explainf(opts, "frameworks list")
1818
list, err := framework.List()
@@ -23,18 +23,21 @@ func newFrameworksCmd(opts *globalOpts) *cobra.Command {
2323
printResult(opts, list, "")
2424
return nil
2525
}
26+
totalControls := 0
2627
for _, f := range list {
2728
fmt.Printf("%s\t%s\tcontrols=%d\n", f.ID, f.Version, f.ControlCount)
29+
totalControls += f.ControlCount
2830
}
31+
fmt.Printf("%d built-in starter frameworks (%d controls). Add custom frameworks via YAML.\n", len(list), totalControls)
2932
return nil
3033
},
3134
}
3235
showCmd := &cobra.Command{
33-
Use: "show <id>",
34-
Short: "Show a framework definition",
36+
Use: "show <id-or-path>",
37+
Short: "Show a framework definition by built-in id or YAML path",
3538
Args: cobra.ExactArgs(1),
3639
RunE: func(cmd *cobra.Command, args []string) error {
37-
explainf(opts, "frameworks show id=%s", args[0])
40+
explainf(opts, "frameworks show target=%s", args[0])
3841
f, err := framework.Load(args[0])
3942
if err != nil {
4043
return newCLIError(exitcode.InvalidInput, err.Error())

cmd/proof/root_cmd_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ func TestFrameworksListCommand(t *testing.T) {
2323
out, err := runCLIForTest(t, []string{"frameworks", "list"})
2424
require.NoError(t, err)
2525
require.Contains(t, out, "eu-ai-act")
26+
require.Contains(t, out, "built-in starter frameworks")
27+
}
28+
29+
func TestFrameworksShowByPathCommand(t *testing.T) {
30+
path := filepath.Join(t.TempDir(), "custom-framework.yaml")
31+
require.NoError(t, os.WriteFile(path, []byte(`
32+
framework:
33+
id: custom-framework
34+
version: "1"
35+
title: Custom Framework
36+
controls:
37+
- id: custom-control
38+
title: Custom Control
39+
required_record_types: [decision]
40+
required_fields: [record_id]
41+
minimum_frequency: continuous
42+
`), 0o644))
43+
44+
out, err := runCLIForTest(t, []string{"frameworks", "show", path, "--json"})
45+
require.NoError(t, err)
46+
require.Contains(t, out, `"id": "custom-framework"`)
2647
}
2748

2849
func TestInspectRecordCommand(t *testing.T) {

core/framework/framework.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package framework
33
import (
44
"embed"
55
"fmt"
6+
"os"
67
"path/filepath"
78
"sort"
89
"strings"
@@ -66,6 +67,18 @@ func List() ([]Info, error) {
6667
}
6768

6869
func Load(idOrFile string) (*Framework, error) {
70+
if info, err := os.Stat(idOrFile); err == nil {
71+
if info.IsDir() {
72+
if isLikelyPath(idOrFile) {
73+
return nil, fmt.Errorf("load framework file %s: path is a directory", idOrFile)
74+
}
75+
} else {
76+
return LoadFile(idOrFile)
77+
}
78+
} else if isLikelyPath(idOrFile) {
79+
return nil, fmt.Errorf("load framework file %s: %w", idOrFile, err)
80+
}
81+
6982
name := idOrFile
7083
if !strings.HasSuffix(name, ".yaml") {
7184
name = name + ".yaml"
@@ -77,6 +90,27 @@ func Load(idOrFile string) (*Framework, error) {
7790
return parseFramework(idOrFile, raw)
7891
}
7992

93+
func LoadFile(path string) (*Framework, error) {
94+
raw, err := os.ReadFile(path)
95+
if err != nil {
96+
return nil, fmt.Errorf("load framework file %s: %w", path, err)
97+
}
98+
return parseFramework(path, raw)
99+
}
100+
101+
func isLikelyPath(value string) bool {
102+
if value == "" {
103+
return false
104+
}
105+
if filepath.IsAbs(value) {
106+
return true
107+
}
108+
if strings.HasPrefix(value, ".") {
109+
return true
110+
}
111+
return strings.ContainsAny(value, `/\`)
112+
}
113+
80114
func parseFramework(idOrFile string, raw []byte) (*Framework, error) {
81115
var f Framework
82116
if err := yaml.Unmarshal(raw, &f); err != nil {

core/framework/framework_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ func TestLoadMissingAndCountControls(t *testing.T) {
3030
require.Equal(t, 4, total)
3131
}
3232

33+
func TestLoadFromFilesystemPath(t *testing.T) {
34+
path := filepath.Join(t.TempDir(), "custom-framework.yaml")
35+
require.NoError(t, os.WriteFile(path, []byte(`
36+
framework:
37+
id: custom-framework
38+
version: "1"
39+
title: Custom Framework
40+
controls:
41+
- id: custom-control
42+
title: Custom Control
43+
required_record_types: [decision]
44+
required_fields: [record_id]
45+
minimum_frequency: continuous
46+
`), 0o644))
47+
48+
f, err := Load(path)
49+
require.NoError(t, err)
50+
require.Equal(t, "custom-framework", f.Framework.ID)
51+
require.Len(t, f.Controls, 1)
52+
}
53+
54+
func TestLoadMissingFilesystemPath(t *testing.T) {
55+
_, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
56+
require.Error(t, err)
57+
require.ErrorContains(t, err, "load framework file")
58+
}
59+
3360
func TestValidateControls(t *testing.T) {
3461
valid := []Control{
3562
{

proof_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ func TestAPIHelpers(t *testing.T) {
9999
f, err := LoadFramework("eu-ai-act")
100100
require.NoError(t, err)
101101
require.Equal(t, "eu-ai-act", f.Framework.ID)
102+
103+
path := filepath.Join(t.TempDir(), "custom-framework.yaml")
104+
require.NoError(t, os.WriteFile(path, []byte(`
105+
framework:
106+
id: custom-framework
107+
version: "1"
108+
title: Custom Framework
109+
controls:
110+
- id: custom-control
111+
title: Custom Control
112+
required_record_types: [decision]
113+
required_fields: [record_id]
114+
minimum_frequency: continuous
115+
`), 0o644))
116+
custom, err := LoadFramework(path)
117+
require.NoError(t, err)
118+
require.Equal(t, "custom-framework", custom.Framework.ID)
102119
}
103120

104121
func TestWriteReadAndCustomSchemaValidation(t *testing.T) {

0 commit comments

Comments
 (0)