reeve 0.0.1 → 0.1.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -0
  3. data/README.md +191 -17
  4. data/lib/generators/reeve/install/install_generator.rb +46 -0
  5. data/lib/generators/reeve/install/templates/create_audit_entries.rb.tt +69 -0
  6. data/lib/generators/reeve/install/templates/initializer.rb.tt +65 -0
  7. data/lib/reeve/audit/entry.rb +48 -0
  8. data/lib/reeve/audit/query.rb +84 -0
  9. data/lib/reeve/audit/recorder.rb +163 -0
  10. data/lib/reeve/audit/redactor.rb +67 -0
  11. data/lib/reeve/audit.rb +88 -0
  12. data/lib/reeve/authorization/adapter.rb +71 -0
  13. data/lib/reeve/authorization/adapters/plain.rb +65 -0
  14. data/lib/reeve/authorization/adapters/pundit.rb +84 -0
  15. data/lib/reeve/authorization/authorizer.rb +23 -0
  16. data/lib/reeve/authorization/current.rb +66 -0
  17. data/lib/reeve/authorization/declaration.rb +73 -0
  18. data/lib/reeve/authorization/guard.rb +97 -0
  19. data/lib/reeve/authorization/registry.rb +118 -0
  20. data/lib/reeve/authorization/scoper.rb +399 -0
  21. data/lib/reeve/authorization.rb +76 -0
  22. data/lib/reeve/configuration.rb +158 -0
  23. data/lib/reeve/context.rb +104 -0
  24. data/lib/reeve/decision.rb +111 -0
  25. data/lib/reeve/errors.rb +89 -0
  26. data/lib/reeve/fast_mcp.rb +24 -0
  27. data/lib/reeve/integrations/fast_mcp/context_builder.rb +48 -0
  28. data/lib/reeve/integrations/fast_mcp/tool_extension.rb +55 -0
  29. data/lib/reeve/invocation.rb +260 -0
  30. data/lib/reeve/minitest.rb +19 -0
  31. data/lib/reeve/rspec.rb +18 -0
  32. data/lib/reeve/scope_result.rb +104 -0
  33. data/lib/reeve/testing/assertions.rb +76 -0
  34. data/lib/reeve/testing/checks/audit_coverage.rb +46 -0
  35. data/lib/reeve/testing/checks/base.rb +155 -0
  36. data/lib/reeve/testing/checks/contract_version.rb +73 -0
  37. data/lib/reeve/testing/checks/cross_principal_leak.rb +132 -0
  38. data/lib/reeve/testing/checks/guard_declared.rb +33 -0
  39. data/lib/reeve/testing/checks/principal_required.rb +60 -0
  40. data/lib/reeve/testing/checks/redaction_holds.rb +142 -0
  41. data/lib/reeve/testing/checks/rule_present.rb +55 -0
  42. data/lib/reeve/testing/checks.rb +95 -0
  43. data/lib/reeve/testing/compliance_assertions.rb +34 -0
  44. data/lib/reeve/testing/compliance_suite.rb +23 -0
  45. data/lib/reeve/testing/ledger.rb +82 -0
  46. data/lib/reeve/testing/matchers/audit_every_call.rb +39 -0
  47. data/lib/reeve/testing/matchers/base.rb +84 -0
  48. data/lib/reeve/testing/matchers/deny_access_for.rb +38 -0
  49. data/lib/reeve/testing/matchers/pass_reeve_check.rb +32 -0
  50. data/lib/reeve/testing/matchers.rb +33 -0
  51. data/lib/reeve/testing/report.rb +58 -0
  52. data/lib/reeve/testing/result.rb +49 -0
  53. data/lib/reeve/testing.rb +60 -0
  54. data/lib/reeve/version.rb +1 -1
  55. data/lib/reeve.rb +9 -3
  56. metadata +52 -1
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ # The checks' read-only window onto the audit ledger.
6
+ #
7
+ # It exists so that `require "reeve/testing"` stays loadable with no ActiveRecord in
8
+ # the process (FR-020, SC-008): nothing here mentions +Reeve::Audit::Entry+ until a
9
+ # check actually asks a question, and every question has an answer for the case where
10
+ # the ledger is absent.
11
+ #
12
+ # A host with its own non-ActiveRecord recorder can pass any object with these six
13
+ # methods as `ledger:` to any ledger-reading check.
14
+ class Ledger
15
+ def self.default
16
+ new
17
+ end
18
+
19
+ def available?
20
+ return false if entry_class.nil?
21
+
22
+ entry_class.table_exists?
23
+ rescue StandardError
24
+ false
25
+ end
26
+
27
+ # Why not, in the words a developer can act on.
28
+ def unavailable_reason
29
+ return nil if available?
30
+ if entry_class.nil?
31
+ return "the audit ledger is not loaded — add `require \"reeve/audit\"` to your " \
32
+ "test helper, or pass a `ledger:` of your own"
33
+ end
34
+
35
+ "the #{entry_class.table_name} table does not exist — run `rails g reeve:install` " \
36
+ "and migrate"
37
+ end
38
+
39
+ # A position in the ledger, taken before an invocation so the entries that
40
+ # invocation wrote can be told apart from everything already there.
41
+ def marker
42
+ return nil unless available?
43
+
44
+ entry_class.maximum(:id)
45
+ end
46
+
47
+ def entries_after(marker)
48
+ return [] unless available?
49
+
50
+ scope = entry_class.order(:id)
51
+ marker.nil? ? scope.to_a : scope.where("id > ?", marker).to_a
52
+ end
53
+
54
+ def count
55
+ available? ? entry_class.count : 0
56
+ end
57
+
58
+ def contract_version
59
+ return nil unless entry_class.respond_to?(:contract_version)
60
+
61
+ entry_class.contract_version
62
+ end
63
+
64
+ def columns
65
+ return [] unless available?
66
+
67
+ entry_class.column_names.map(&:to_s)
68
+ end
69
+
70
+ private
71
+
72
+ def entry_class
73
+ return @entry_class if defined?(@entry_class)
74
+
75
+ @entry_class =
76
+ if Reeve.const_defined?(:Audit, false) && Reeve::Audit.const_defined?(:Entry, false)
77
+ Reeve::Audit::Entry
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ module Matchers
6
+ # FR-017, from RSpec:
7
+ #
8
+ # it { is_expected.to audit_every_call }
9
+ # it { is_expected.to audit_every_call.for_principal(alice).with(query: "AC") }
10
+ #
11
+ # Point it at the host's own call site to prove that *that* goes through the
12
+ # envelope, which is where an audit bypass actually lives:
13
+ #
14
+ # it { is_expected.to audit_every_call.invoked_by(MyServer.method(:dispatch)) }
15
+ class AuditEveryCall < Base
16
+ def self.check
17
+ Checks::AuditCoverage
18
+ end
19
+
20
+ def for_principal(principal)
21
+ @principal = principal
22
+ self
23
+ end
24
+
25
+ def description
26
+ "audit every call"
27
+ end
28
+
29
+ private
30
+
31
+ def check_for(tool)
32
+ Checks::AuditCoverage.new(
33
+ tool: tool, principal: @principal || default_principal, **options
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ module Matchers
6
+ # What every reeve matcher is: a check, and nothing else.
7
+ #
8
+ # +failure_message+ returns the check's own +message+ verbatim. No matcher composes
9
+ # a sentence, reformats one, or adds context (FR-019) — that is the whole reason the
10
+ # same violation reads identically here, in Minitest, and in a rake task.
11
+ #
12
+ # Written against RSpec's matcher protocol by duck-typing rather than by including
13
+ # +RSpec::Matchers::DSL+, so nothing under lib/reeve/testing needs RSpec loaded in
14
+ # order to be *parsed*; only a suite that uses these needs it present.
15
+ class Base
16
+ attr_reader :result
17
+
18
+ def initialize
19
+ @arguments = {}
20
+ @invoke = nil
21
+ @ledger = nil
22
+ end
23
+
24
+ # The arguments the tool is called with.
25
+ def with(**arguments)
26
+ @arguments = arguments
27
+ self
28
+ end
29
+
30
+ # How the tool is called. Point this at a host's own invoker to prove that *it*
31
+ # goes through the envelope.
32
+ def invoked_by(callable)
33
+ @invoke = callable
34
+ self
35
+ end
36
+
37
+ def reading(ledger)
38
+ @ledger = ledger
39
+ self
40
+ end
41
+
42
+ def matches?(subject)
43
+ @result = check_for(subject).call
44
+ @result.passed?
45
+ end
46
+
47
+ # No `does_not_match?`: RSpec negates `matches?` for us, and a check that had a
48
+ # separate negative path could drift from its positive one.
49
+ def failure_message
50
+ result.message
51
+ end
52
+
53
+ def failure_message_when_negated
54
+ "expected #{result.check} to fail, but it passed: #{result.message}"
55
+ end
56
+
57
+ def description
58
+ self.class.check.check_name
59
+ end
60
+
61
+ def supports_block_expectations?
62
+ false
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :arguments, :invoke, :ledger
68
+
69
+ def check_for(_subject)
70
+ raise NotImplementedError, "#{self.class} must build its check"
71
+ end
72
+
73
+ def options
74
+ { arguments: arguments, invoke: invoke, ledger: ledger }
75
+ end
76
+
77
+ # The principal a matcher falls back to when the example did not name one.
78
+ def default_principal
79
+ Testing.compliance_principals.first
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ module Matchers
6
+ # FR-016, from RSpec:
7
+ #
8
+ # it { is_expected.to deny_access_for(stranger).with(query: "AC") }
9
+ #
10
+ # Backed by CrossPrincipalLeak in its :nothing expectation — this principal may
11
+ # receive no record at all. A denial satisfies it; so does an allowed call that
12
+ # returns an empty scope, because from the agent's side those are the same thing,
13
+ # and FR-006 requires that they stay the same thing.
14
+ class DenyAccessFor < Base
15
+ def self.check
16
+ Checks::CrossPrincipalLeak
17
+ end
18
+
19
+ def initialize(principal)
20
+ super()
21
+ @principal = principal
22
+ end
23
+
24
+ def description
25
+ "deny access for #{@principal.inspect}"
26
+ end
27
+
28
+ private
29
+
30
+ def check_for(tool)
31
+ Checks::CrossPrincipalLeak.new(
32
+ tool: tool, principals: [@principal], expect: :nothing, **options
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ module Matchers
6
+ # The escape hatch that keeps SC-009 honest: every one of the seven checks is
7
+ # assertable from RSpec, not only the two with names of their own.
8
+ #
9
+ # expect(Reeve::Checks::RedactionHolds.new(tool: T, principal: alice))
10
+ # .to pass_reeve_check
11
+ class PassReeveCheck < Base
12
+ def self.check
13
+ Checks::Base
14
+ end
15
+
16
+ def description
17
+ "pass its reeve check"
18
+ end
19
+
20
+ def failure_message_when_negated
21
+ "expected #{result.check} to fail, but it passed: #{result.message}"
22
+ end
23
+
24
+ private
25
+
26
+ def check_for(check)
27
+ check
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "matchers/base"
4
+ require_relative "matchers/deny_access_for"
5
+ require_relative "matchers/audit_every_call"
6
+ require_relative "matchers/pass_reeve_check"
7
+
8
+ module Reeve
9
+ module Testing
10
+ # The RSpec front-end.
11
+ #
12
+ # require "reeve/rspec"
13
+ # RSpec.configure { |c| c.include Reeve::Testing::Matchers }
14
+ #
15
+ # RSpec.describe InvoiceSearchTool do
16
+ # it { is_expected.to deny_access_for(stranger).with(query: "AC") }
17
+ # it { is_expected.to audit_every_call }
18
+ # end
19
+ module Matchers
20
+ def deny_access_for(principal)
21
+ DenyAccessFor.new(principal)
22
+ end
23
+
24
+ def audit_every_call
25
+ AuditEveryCall.new
26
+ end
27
+
28
+ def pass_reeve_check
29
+ PassReeveCheck.new
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ # The aggregate of one compliance run (FR-018).
6
+ #
7
+ # +to_s+ is written to be the whole output of a failing CI step: a one-line verdict
8
+ # followed by every failure in full. A report that has to be cross-referenced with
9
+ # something else is a report nobody reads at 3am.
10
+ class Report
11
+ attr_reader :results
12
+
13
+ def initialize(results)
14
+ @results = results.freeze
15
+ freeze
16
+ end
17
+
18
+ def passed?
19
+ failures.empty?
20
+ end
21
+
22
+ def failed?
23
+ !passed?
24
+ end
25
+
26
+ def failures
27
+ results.reject(&:passed?)
28
+ end
29
+
30
+ def passes
31
+ results.select(&:passed?)
32
+ end
33
+
34
+ def size
35
+ results.size
36
+ end
37
+
38
+ def to_s
39
+ [summary, *failures.map { |result| detail(result) }].join("\n")
40
+ end
41
+
42
+ def inspect
43
+ "#<Reeve::Testing::Report #{summary}>"
44
+ end
45
+
46
+ private
47
+
48
+ def summary
49
+ "reeve compliance: #{size} #{size == 1 ? 'check' : 'checks'}, " \
50
+ "#{passes.size} passed, #{failures.size} failed"
51
+ end
52
+
53
+ def detail(result)
54
+ "\nFAIL #{result.check}\n #{result.message}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reeve
4
+ module Testing
5
+ # What a check hands back: whether the guarantee held, the sentence explaining it, and
6
+ # the structured facts behind the sentence.
7
+ #
8
+ # The message is built here and nowhere else. A front-end — an RSpec matcher, a
9
+ # Minitest assertion, a rake task — reads +message+ and prints it; none of them
10
+ # composes their own (FR-019). That is what makes the same violation read identically
11
+ # from all three.
12
+ class Result
13
+ attr_reader :check, :message, :details
14
+
15
+ def self.passed(check:, message:, details: {})
16
+ new(check: check, passed: true, message: message, details: details)
17
+ end
18
+
19
+ def self.failed(check:, message:, details: {})
20
+ new(check: check, passed: false, message: message, details: details)
21
+ end
22
+
23
+ def initialize(check:, passed:, message:, details: {})
24
+ @check = check.to_s
25
+ @passed = passed ? true : false
26
+ @message = message.to_s
27
+ @details = details.freeze
28
+ freeze
29
+ end
30
+
31
+ def passed?
32
+ @passed
33
+ end
34
+
35
+ def failed?
36
+ !@passed
37
+ end
38
+
39
+ def to_s
40
+ message
41
+ end
42
+
43
+ def inspect
44
+ "#<Reeve::Testing::Result #{check} #{passed? ? 'passed' : 'failed'} " \
45
+ "#{message.inspect}>"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../reeve"
4
+ require_relative "testing/result"
5
+ require_relative "testing/report"
6
+ require_relative "testing/ledger"
7
+ require_relative "testing/checks"
8
+
9
+ module Reeve
10
+ # The testing kit: framework-neutral checks, and thin front-ends over them.
11
+ #
12
+ # require "reeve/testing" # the checks, and nothing else
13
+ # require "reeve/rspec" # + matchers and the shared example group
14
+ # require "reeve/minitest" # + assertions and the compliance test case
15
+ #
16
+ # This file loads no test framework and no ActiveRecord, so the checks are runnable from
17
+ # a rake task, a CI script or a boot-time assertion in staging (FR-020, FR-026):
18
+ #
19
+ # require "reeve/testing"
20
+ # report = Reeve::Checks.run_all(principals: [alice, bob])
21
+ # abort report.to_s unless report.passed?
22
+ module Testing
23
+ class << self
24
+ # The two fixture principals the compliance suite runs every tool against. Set once,
25
+ # in the host's test helper, and both front-ends' compliance suites pick it up:
26
+ #
27
+ # Reeve::Testing.compliance_principals = -> { [users(:alice), users(:bob)] }
28
+ #
29
+ # A callable rather than a value, because in a Rails test suite the fixtures do not
30
+ # exist yet at the moment the helper is loaded.
31
+ attr_writer :compliance_principals
32
+
33
+ def compliance_principals
34
+ source = @compliance_principals || Reeve.config.compliance_principals
35
+ raise ConfigurationError, missing_principals_message if source.nil?
36
+
37
+ principals = Array(source.respond_to?(:call) ? source.call : source)
38
+ raise ConfigurationError, missing_principals_message if principals.size < 2
39
+
40
+ principals
41
+ end
42
+
43
+ def compliance_principals?
44
+ !(@compliance_principals || Reeve.config.compliance_principals).nil?
45
+ end
46
+
47
+ def reset!
48
+ @compliance_principals = nil
49
+ end
50
+
51
+ private
52
+
53
+ def missing_principals_message
54
+ "the reeve compliance suite needs two fixture principals with disjoint records. " \
55
+ "Set them in your test helper:\n\n " \
56
+ "Reeve::Testing.compliance_principals = -> { [users(:alice), users(:bob)] }"
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/reeve/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reeve
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/reeve.rb CHANGED
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "reeve/version"
4
+ require_relative "reeve/errors"
5
+ require_relative "reeve/decision"
6
+ require_relative "reeve/configuration"
7
+ require_relative "reeve/context"
8
+ require_relative "reeve/scope_result"
9
+ require_relative "reeve/invocation"
10
+ require_relative "reeve/authorization"
4
11
 
5
12
  # Reeve — per-record authorization and an append-only audit ledger for the MCP tools
6
13
  # a Rails application exposes to AI agents.
7
14
  #
8
- # 0.0.1 is a placeholder release reserving the gem name. It has no functionality yet.
9
- # See https://github.com/vicmaster/reeve for progress.
15
+ # The published 0.0.1 is a placeholder release reserving the gem name; the kernel below
16
+ # is unreleased work in progress. See https://github.com/vicmaster/reeve.
10
17
  module Reeve
11
- class Error < StandardError; end
12
18
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reeve
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Velazquez
@@ -27,7 +27,57 @@ files:
27
27
  - CHANGELOG.md
28
28
  - LICENSE.txt
29
29
  - README.md
30
+ - lib/generators/reeve/install/install_generator.rb
31
+ - lib/generators/reeve/install/templates/create_audit_entries.rb.tt
32
+ - lib/generators/reeve/install/templates/initializer.rb.tt
30
33
  - lib/reeve.rb
34
+ - lib/reeve/audit.rb
35
+ - lib/reeve/audit/entry.rb
36
+ - lib/reeve/audit/query.rb
37
+ - lib/reeve/audit/recorder.rb
38
+ - lib/reeve/audit/redactor.rb
39
+ - lib/reeve/authorization.rb
40
+ - lib/reeve/authorization/adapter.rb
41
+ - lib/reeve/authorization/adapters/plain.rb
42
+ - lib/reeve/authorization/adapters/pundit.rb
43
+ - lib/reeve/authorization/authorizer.rb
44
+ - lib/reeve/authorization/current.rb
45
+ - lib/reeve/authorization/declaration.rb
46
+ - lib/reeve/authorization/guard.rb
47
+ - lib/reeve/authorization/registry.rb
48
+ - lib/reeve/authorization/scoper.rb
49
+ - lib/reeve/configuration.rb
50
+ - lib/reeve/context.rb
51
+ - lib/reeve/decision.rb
52
+ - lib/reeve/errors.rb
53
+ - lib/reeve/fast_mcp.rb
54
+ - lib/reeve/integrations/fast_mcp/context_builder.rb
55
+ - lib/reeve/integrations/fast_mcp/tool_extension.rb
56
+ - lib/reeve/invocation.rb
57
+ - lib/reeve/minitest.rb
58
+ - lib/reeve/rspec.rb
59
+ - lib/reeve/scope_result.rb
60
+ - lib/reeve/testing.rb
61
+ - lib/reeve/testing/assertions.rb
62
+ - lib/reeve/testing/checks.rb
63
+ - lib/reeve/testing/checks/audit_coverage.rb
64
+ - lib/reeve/testing/checks/base.rb
65
+ - lib/reeve/testing/checks/contract_version.rb
66
+ - lib/reeve/testing/checks/cross_principal_leak.rb
67
+ - lib/reeve/testing/checks/guard_declared.rb
68
+ - lib/reeve/testing/checks/principal_required.rb
69
+ - lib/reeve/testing/checks/redaction_holds.rb
70
+ - lib/reeve/testing/checks/rule_present.rb
71
+ - lib/reeve/testing/compliance_assertions.rb
72
+ - lib/reeve/testing/compliance_suite.rb
73
+ - lib/reeve/testing/ledger.rb
74
+ - lib/reeve/testing/matchers.rb
75
+ - lib/reeve/testing/matchers/audit_every_call.rb
76
+ - lib/reeve/testing/matchers/base.rb
77
+ - lib/reeve/testing/matchers/deny_access_for.rb
78
+ - lib/reeve/testing/matchers/pass_reeve_check.rb
79
+ - lib/reeve/testing/report.rb
80
+ - lib/reeve/testing/result.rb
31
81
  - lib/reeve/version.rb
32
82
  homepage: https://github.com/vicmaster/reeve
33
83
  licenses:
@@ -37,6 +87,7 @@ metadata:
37
87
  source_code_uri: https://github.com/vicmaster/reeve
38
88
  changelog_uri: https://github.com/vicmaster/reeve/blob/main/CHANGELOG.md
39
89
  bug_tracker_uri: https://github.com/vicmaster/reeve/issues
90
+ rubygems_mfa_required: 'true'
40
91
  post_install_message:
41
92
  rdoc_options: []
42
93
  require_paths: