reeve 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -1
- data/README.md +143 -2
- data/lib/reeve/authorization/guard.rb +53 -0
- data/lib/reeve/authorization/registry.rb +43 -0
- data/lib/reeve/authorization.rb +1 -11
- data/lib/reeve/configuration.rb +47 -1
- data/lib/reeve/decision.rb +6 -1
- data/lib/reeve/errors.rb +13 -0
- data/lib/reeve/integrations/fast_mcp/tool_extension.rb +5 -1
- data/lib/reeve/inventory/report.rb +146 -0
- data/lib/reeve/inventory.rb +212 -0
- data/lib/reeve/invocation.rb +49 -1
- data/lib/reeve/testing/assertions.rb +2 -2
- data/lib/reeve/testing/checks/endpoint_coverage.rb +133 -0
- data/lib/reeve/testing/checks.rb +18 -7
- data/lib/reeve/testing/compliance_assertions.rb +3 -1
- data/lib/reeve/testing/compliance_suite.rb +4 -1
- data/lib/reeve/testing/matchers/pass_reeve_check.rb +1 -1
- data/lib/reeve/testing/report.rb +22 -6
- data/lib/reeve/testing/result.rb +28 -8
- data/lib/reeve/testing.rb +29 -0
- data/lib/reeve/version.rb +1 -1
- data/lib/reeve.rb +1 -0
- metadata +5 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Reeve
|
|
4
4
|
module Testing
|
|
5
5
|
module Matchers
|
|
6
|
-
# The escape hatch that keeps SC-009 honest: every
|
|
6
|
+
# The escape hatch that keeps SC-009 honest: every check is
|
|
7
7
|
# assertable from RSpec, not only the two with names of their own.
|
|
8
8
|
#
|
|
9
9
|
# expect(Reeve::Checks::RedactionHolds.new(tool: T, principal: alice))
|
data/lib/reeve/testing/report.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Reeve
|
|
|
15
15
|
freeze
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Nothing failed. A skip is not a failure — and it is printed, so it is not
|
|
19
|
+
# mistaken for a pass either.
|
|
18
20
|
def passed?
|
|
19
21
|
failures.empty?
|
|
20
22
|
end
|
|
@@ -23,20 +25,31 @@ module Reeve
|
|
|
23
25
|
!passed?
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
# Every result was a skip: the run established nothing either way. The front-ends
|
|
29
|
+
# report this as a skipped test rather than a green one.
|
|
30
|
+
def skipped?
|
|
31
|
+
!results.empty? && results.all?(&:skipped?)
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
def failures
|
|
27
|
-
results.
|
|
35
|
+
results.select(&:failed?)
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
def passes
|
|
31
39
|
results.select(&:passed?)
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
def skips
|
|
43
|
+
results.select(&:skipped?)
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
def size
|
|
35
47
|
results.size
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def to_s
|
|
39
|
-
[summary, *failures.map { |result| detail(result) }
|
|
51
|
+
[summary, *failures.map { |result| detail("FAIL", result) },
|
|
52
|
+
*skips.map { |result| detail("SKIP", result) }].join("\n")
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def inspect
|
|
@@ -46,12 +59,15 @@ module Reeve
|
|
|
46
59
|
private
|
|
47
60
|
|
|
48
61
|
def summary
|
|
49
|
-
"reeve compliance: #{size} #{size == 1 ? 'check' : 'checks'}, " \
|
|
50
|
-
|
|
62
|
+
line = "reeve compliance: #{size} #{size == 1 ? 'check' : 'checks'}, " \
|
|
63
|
+
"#{passes.size} passed, #{failures.size} failed"
|
|
64
|
+
skips.empty? ? line : "#{line}, #{skips.size} skipped"
|
|
51
65
|
end
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
# Every line of a multi-line message indented, so it reads as one block under its
|
|
68
|
+
# heading rather than as stray lines of the report.
|
|
69
|
+
def detail(heading, result)
|
|
70
|
+
"\n#{heading} #{result.check}\n #{result.message.gsub("\n", "\n ")}"
|
|
55
71
|
end
|
|
56
72
|
end
|
|
57
73
|
end
|
data/lib/reeve/testing/result.rb
CHANGED
|
@@ -9,31 +9,51 @@ module Reeve
|
|
|
9
9
|
# Minitest assertion, a rake task — reads +message+ and prints it; none of them
|
|
10
10
|
# composes their own (FR-019). That is what makes the same violation read identically
|
|
11
11
|
# from all three.
|
|
12
|
+
#
|
|
13
|
+
# A result is passed, failed, or skipped. Skipped is for a guarantee the run could not
|
|
14
|
+
# establish either way — endpoint coverage with no inventory declared. Reporting that
|
|
15
|
+
# as a pass would be the green that means "we did not look"; reporting it as a failure
|
|
16
|
+
# would turn every build red over a question the host never asked. A skip is printed,
|
|
17
|
+
# and shows up yellow in both test frameworks, so it is neither.
|
|
12
18
|
class Result
|
|
13
|
-
|
|
19
|
+
STATUSES = %i[passed failed skipped].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :check, :message, :details, :status
|
|
14
22
|
|
|
15
23
|
def self.passed(check:, message:, details: {})
|
|
16
|
-
new(check: check,
|
|
24
|
+
new(check: check, status: :passed, message: message, details: details)
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
def self.failed(check:, message:, details: {})
|
|
20
|
-
new(check: check,
|
|
28
|
+
new(check: check, status: :failed, message: message, details: details)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.skipped(check:, message:, details: {})
|
|
32
|
+
new(check: check, status: :skipped, message: message, details: details)
|
|
21
33
|
end
|
|
22
34
|
|
|
23
|
-
def initialize(check:,
|
|
35
|
+
def initialize(check:, status:, message:, details: {})
|
|
36
|
+
unless STATUSES.include?(status)
|
|
37
|
+
raise ArgumentError, "status must be one of #{STATUSES.inspect}, got #{status.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
@check = check.to_s
|
|
25
|
-
@
|
|
41
|
+
@status = status
|
|
26
42
|
@message = message.to_s
|
|
27
43
|
@details = details.freeze
|
|
28
44
|
freeze
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
def passed?
|
|
32
|
-
|
|
48
|
+
status == :passed
|
|
33
49
|
end
|
|
34
50
|
|
|
35
51
|
def failed?
|
|
36
|
-
|
|
52
|
+
status == :failed
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def skipped?
|
|
56
|
+
status == :skipped
|
|
37
57
|
end
|
|
38
58
|
|
|
39
59
|
def to_s
|
|
@@ -41,7 +61,7 @@ module Reeve
|
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
def inspect
|
|
44
|
-
"#<Reeve::Testing::Result #{check} #{
|
|
64
|
+
"#<Reeve::Testing::Result #{check} #{status} " \
|
|
45
65
|
"#{message.inspect}>"
|
|
46
66
|
end
|
|
47
67
|
end
|
data/lib/reeve/testing.rb
CHANGED
|
@@ -40,16 +40,45 @@ module Reeve
|
|
|
40
40
|
principals
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# The tools the suite walks. Unset means every tool reeve knows about — the classes
|
|
44
|
+
# that included the DSL, guarded or not, plus every class the endpoint inventory
|
|
45
|
+
# routes to, which need not have included anything. That default is what makes an
|
|
46
|
+
# unguarded tool visible rather than merely absent.
|
|
47
|
+
attr_writer :compliance_tools
|
|
48
|
+
|
|
49
|
+
def compliance_tools
|
|
50
|
+
source = @compliance_tools || Reeve.config.compliance_tools
|
|
51
|
+
return Reeve.registry.tool_classes | inventory_tools if source.nil?
|
|
52
|
+
|
|
53
|
+
Array(source.respond_to?(:call) ? source.call : source)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The host said which tools it is certifying, so a run is a subset by its own account.
|
|
57
|
+
def compliance_tools_narrowed?
|
|
58
|
+
!(@compliance_tools || Reeve.config.compliance_tools).nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
43
61
|
def compliance_principals?
|
|
44
62
|
!(@compliance_principals || Reeve.config.compliance_principals).nil?
|
|
45
63
|
end
|
|
46
64
|
|
|
47
65
|
def reset!
|
|
48
66
|
@compliance_principals = nil
|
|
67
|
+
@compliance_tools = nil
|
|
49
68
|
end
|
|
50
69
|
|
|
51
70
|
private
|
|
52
71
|
|
|
72
|
+
# An inventory that cannot be read adds nothing here; EndpointCoverage fails on it
|
|
73
|
+
# by name, which is a better place to learn about it than an exception in every
|
|
74
|
+
# per-tool check.
|
|
75
|
+
def inventory_tools
|
|
76
|
+
inventory = Reeve.config.inventory
|
|
77
|
+
inventory ? inventory.routed_tools : []
|
|
78
|
+
rescue StandardError
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
|
|
53
82
|
def missing_principals_message
|
|
54
83
|
"the reeve compliance suite needs two fixture principals with disjoint records. " \
|
|
55
84
|
"Set them in your test helper:\n\n " \
|
data/lib/reeve/version.rb
CHANGED
data/lib/reeve.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "reeve/context"
|
|
|
8
8
|
require_relative "reeve/scope_result"
|
|
9
9
|
require_relative "reeve/invocation"
|
|
10
10
|
require_relative "reeve/authorization"
|
|
11
|
+
require_relative "reeve/inventory"
|
|
11
12
|
|
|
12
13
|
# Reeve — per-record authorization and an append-only audit ledger for the MCP tools
|
|
13
14
|
# a Rails application exposes to AI agents.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reeve
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Victor Velazquez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Reeve makes it safe for a Rails application to expose MCP (Model Context Protocol)
|
|
@@ -56,6 +56,8 @@ files:
|
|
|
56
56
|
- lib/reeve/fast_mcp.rb
|
|
57
57
|
- lib/reeve/integrations/fast_mcp/context_builder.rb
|
|
58
58
|
- lib/reeve/integrations/fast_mcp/tool_extension.rb
|
|
59
|
+
- lib/reeve/inventory.rb
|
|
60
|
+
- lib/reeve/inventory/report.rb
|
|
59
61
|
- lib/reeve/invocation.rb
|
|
60
62
|
- lib/reeve/minitest.rb
|
|
61
63
|
- lib/reeve/rspec.rb
|
|
@@ -67,6 +69,7 @@ files:
|
|
|
67
69
|
- lib/reeve/testing/checks/base.rb
|
|
68
70
|
- lib/reeve/testing/checks/contract_version.rb
|
|
69
71
|
- lib/reeve/testing/checks/cross_principal_leak.rb
|
|
72
|
+
- lib/reeve/testing/checks/endpoint_coverage.rb
|
|
70
73
|
- lib/reeve/testing/checks/guard_declared.rb
|
|
71
74
|
- lib/reeve/testing/checks/principal_required.rb
|
|
72
75
|
- lib/reeve/testing/checks/redaction_holds.rb
|