reeve 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bea7ce4962daf51d8ac269ab8fa8d95198d3dd0ab22fe534c2ddd0a0f79a8981
4
- data.tar.gz: 8000630e05686b6d1a29c81e5b007f7a1b46f586aca9688870465c1b8b8d1d3a
3
+ metadata.gz: aaf976c70abfbb4d56236acface7e1ef95a4e8c177452e4ef918ca22f45933d0
4
+ data.tar.gz: 9076bbad0582db6c4cda3c38dc89f97840dc3a7e2dbb99ca98a16c5b642ece57
5
5
  SHA512:
6
- metadata.gz: 40886bd0fa070325c4e52eb41a9aa87e4f8cda355d71d68993641b62201aadd37243ce8988f0b9831d2aa33ef80a219d663a2c8b78cd4bd317dca411bbc70c48
7
- data.tar.gz: b12725979b28ffa95cbd6838c9e6f1faadc47675894bd1d824c99632f7b0c715441b8c690eabd8f9d8873139a8be97b9cfa75eba22e97b0fa0a254287003f65b
6
+ metadata.gz: 7957d35a7e9df67acaa1c6babcd18b1d2340ffd63595fc88a11e16bd5c81cfa78ddb6b6cf17742f74b1f300d5e35058442d124fe77d6cc7051005e3ed4d5b837
7
+ data.tar.gz: 543bb727d1abf718053d21a72fe3d3eb62b9e82ebe5a3658046b7d80eb8c897c91e76da7d6c55842f1ee0545b76af94a2d1127ffa2fc814a88bbadab3c827a01
data/CHANGELOG.md CHANGED
@@ -4,6 +4,85 @@ All notable changes are recorded here. This project follows [Semantic
4
4
  Versioning](https://semver.org), with one rule specific to what it does — see
5
5
  [Versioning policy](#versioning-policy).
6
6
 
7
+ ## [0.2.0] - 2026-08-12
8
+
9
+ Everything here came out of running 0.1.0 against a real Rails 8.1 application rather than
10
+ against its own test suite. The Pundit fix is the one that blocks adoption.
11
+
12
+ **Audit-entry contract version: `1` → `2`.** Two changes, one bump.
13
+
14
+ `metadata` was written NULL on every row through version 1 and now carries what the caller
15
+ passed. Anything mapping version 1 rows could reasonably have read that column as always
16
+ empty, and this project's own versioning rule calls a change in what a value means MAJOR —
17
+ hence 0.2.0 rather than a patch.
18
+
19
+ New non-null `contract_version` column, stamped on every row. Version 1 rows could not
20
+ name their own shape, which is exactly what made the `metadata` change ambiguous to a
21
+ reader: on a version 1 row a NULL `metadata` means "never recorded", on a version 2 row it
22
+ means "the caller passed none". Stamping the row settles that at read time instead of
23
+ requiring a reader to know when the writing gem was deployed, and it makes every future
24
+ bump legible on the row. It also gives `Checks::ContractVersion` real teeth — a stale
25
+ table now fails on the missing column even when a bump was purely semantic.
26
+
27
+ **Upgrading from 0.1.0:**
28
+
29
+ - Re-run `bin/rails generate reeve:install` and migrate, or add the column by hand:
30
+ `add_column :reeve_audit_entries, :contract_version, :integer, null: false, default: 1`
31
+ — `default: 1` is correct for existing rows, since that is the contract they were
32
+ written under. Drop the default afterwards if you prefer; the recorder always sets it.
33
+ - A host pinning `Reeve::Checks::ContractVersion.new(expected: 1)` moves it to `2`.
34
+ - **A custom `audit_recorder` that writes to `Reeve::Audit::Entry` must now set
35
+ `contract_version`.** The model rejects a row without it.
36
+
37
+ ### Added
38
+
39
+ - README: **Wrapping your own JSON-RPC server** — the integration path for a Rails app
40
+ that already exposes `/mcp` from its own controller and tool registry. Covers mapping
41
+ JSON-RPC tool names to `Reeve.invoke`, passing request headers as `metadata`, resolving
42
+ the principal from `Current.user` or a bearer token while keeping the host's existing
43
+ authentication, adopting one tool at a time through `:allow_with_warning`, and pointing
44
+ the compliance checks at the host's own dispatcher. The recipe is executed by
45
+ `spec/reeve/integrations/custom_dispatcher_spec.rb` rather than only written down.
46
+ ([#5](https://github.com/vicmaster/reeve/issues/5))
47
+
48
+ ### Fixed
49
+
50
+ - `metadata` passed to `Reeve.invoke` is written to the ledger. The column, the recorder
51
+ mapping and the contract check all existed, but `Context#to_h` did not carry the value,
52
+ so `metadata` was NULL on every call ever recorded — including the header hash the
53
+ fast-mcp adapter collects. It is redacted on the way in, by the same redactor and the
54
+ same declared names as the arguments, so `Authorization` does not land in the ledger.
55
+ This is the change behind the contract-version bump above.
56
+ - `contracts/audit-entry.md` no longer claims the contract version is "recorded in the
57
+ initializer". It never was — which is what prompted the `contract_version` column above,
58
+ so that from contract 2 the claim is simply true.
59
+ - The envelope-overhead spec compares the fastest call on each side rather than the mean
60
+ of two separately-timed windows. A single GC pause or scheduler preemption landing in
61
+ one window and not the other lands whole in the difference — 300ms over fifty runs is
62
+ 6ms per call against a 5ms budget, from a busy machine rather than a regression. It
63
+ loses no sensitivity: a regression is paid on every call, so it slows the fastest trial
64
+ too. Verified both ways — an injected one-off stall moves the measurement from 6.65ms to
65
+ 0.23ms, and 6ms added to every call still fails at 7.13ms.
66
+ - The spec suite declares UTF-8 (`spec/spec_helper.rb`). The README-spec encoding bug was
67
+ one instance of five: eleven examples across `readme_spec`, `contract_version_spec`,
68
+ `migration_spec`, `install_generator_spec`, `framework_neutrality_spec`,
69
+ `isolation_spec` and `verification_spec` died on `invalid byte sequence` whenever a file
70
+ was run on its own, and passed in a full run only because some other file happened to
71
+ set the encoding first. Every spec file now passes in isolation, and the suite passes
72
+ under `LC_ALL=C`.
73
+ - A Pundit policy that inherits its `Scope` from a base policy (`class LeadPolicy <
74
+ LeadBasePolicy`) is now recognised. Policy detection looked only at the policy's own
75
+ namespace, so the ordinary Pundit inheritance pattern was refused at declaration time
76
+ with `ConfigurationError`. Detection now walks the policy's ancestry, stopping before
77
+ `Object` so a top-level `Scope` constant still cannot make an unrelated class look
78
+ Pundit-shaped. ([#3](https://github.com/vicmaster/reeve/issues/3))
79
+ - `lib/reeve.rb` no longer describes the kernel as unreleased work in progress. The note
80
+ belonged to the 0.0.1 name-claim release and contradicted 0.1.0 for anyone reading the
81
+ installed gem. ([#4](https://github.com/vicmaster/reeve/issues/4))
82
+ - The README compliance specs read the file as UTF-8. Under a POSIX locale they read it
83
+ as US-ASCII and every example failed on the first em dash, so the guarantees they
84
+ exist to enforce were never actually checked.
85
+
7
86
  ## [0.1.0] - 2026-08-11
8
87
 
9
88
  First working release. The published 0.0.1 was a placeholder holding the gem name.
data/README.md CHANGED
@@ -7,7 +7,7 @@ A Ruby gem that makes it safe for a Rails application to expose MCP tools to AI
7
7
  declarative per-record authorization, an append-only audit ledger, and a testing kit that
8
8
  proves both hold.
9
9
 
10
- > **Status: 0.1.0, the first working release.** Read the known limitations in
10
+ > **Status: 0.2.0.** Read the known limitations in
11
11
  > [CHANGELOG.md](CHANGELOG.md) before adopting it — particularly the one about a
12
12
  > transaction wrapped around an invocation, if your application wraps requests in one.
13
13
 
@@ -219,6 +219,95 @@ Reeve.invoke(
219
219
  )
220
220
  ```
221
221
 
222
+ ## Wrapping your own JSON-RPC server
223
+
224
+ Plenty of Rails apps expose `/mcp` from a controller they wrote themselves, with their own
225
+ tool registry and their own bearer-token authentication. There is no adapter to install
226
+ for that, and none is needed: `Reeve.invoke` is the adapter interface. An MCP integration
227
+ is a function from a JSON-RPC request to one `Reeve.invoke` call.
228
+
229
+ Keep the authentication you have. Reeve does not do connection auth (see [What you have
230
+ not gained](#what-you-have-not-gained)) — the controller still decides whether the caller
231
+ gets in the door, and Reeve decides what they may touch once inside.
232
+
233
+ **Dispatch through the envelope.** Map the JSON-RPC tool name to the class, then call:
234
+
235
+ ```ruby
236
+ # app/controllers/mcp_controller.rb
237
+ def call_tool
238
+ tool = McpServer.registry.fetch(params.dig(:params, :name))
239
+
240
+ records = Reeve.invoke(
241
+ tool: tool,
242
+ arguments: params.dig(:params, :arguments).to_h.symbolize_keys,
243
+ agent: { id: request.headers["X-MCP-Client"] || "unknown" },
244
+ metadata: { headers: request.headers.to_h.slice(*AUDITED_HEADERS) }
245
+ )
246
+
247
+ render json: { jsonrpc: "2.0", id: params[:id], result: serialize(records) }
248
+ rescue Reeve::DeniedError => e
249
+ render json: { jsonrpc: "2.0", id: params[:id],
250
+ error: { code: -32_003, message: e.message } }
251
+ end
252
+ ```
253
+
254
+ Note what is *not* passed: `principal:`. Omit it and the resolver in your initializer runs,
255
+ which is what you want when the controller has already set `Current.user` — one place
256
+ decides who the principal is, and the ledger records the same answer the guard used.
257
+ Passing `principal:` explicitly overrides the resolver for that call, which is useful in
258
+ tests and in scripts.
259
+
260
+ **`metadata:` is transport detail, and it is recorded.** It reaches the resolver as
261
+ `context.metadata` and is written to the ledger's `metadata` column, so it is what a
262
+ reviewer has to reconstruct *which request* a row came from. It goes through the same
263
+ redactor as the arguments, so `Authorization` and friends are replaced by name — but pass
264
+ the headers you would want in an audit rather than all of them.
265
+
266
+ **Resolve the principal from whichever the controller established:**
267
+
268
+ ```ruby
269
+ Reeve.configure do |config|
270
+ config.principal_resolver = lambda do |context|
271
+ Current.user || ApiToken.find_by(
272
+ token: context.metadata.dig(:headers, "Authorization").to_s.delete_prefix("Bearer ")
273
+ )&.user
274
+ end
275
+ end
276
+ ```
277
+
278
+ A resolver that returns nil — or raises — denies with `no_principal` and still writes a
279
+ row. There is no configuration in which an unidentified caller reaches a tool.
280
+
281
+ **Adopt one tool at a time.** A registry of thirty tools does not need thirty policies
282
+ before any of this is worth turning on:
283
+
284
+ ```ruby
285
+ config.unguarded_tools = :allow_with_warning # migrating
286
+ ```
287
+
288
+ Tools with `guard_with` are authorized and scoped normally. Tools without one still run —
289
+ unscoped, which is the entire point of the warning — and are recorded with `guard: "none"`
290
+ and rule `unguarded_tool`, so the ledger itself is your worklist:
291
+
292
+ ```ruby
293
+ Reeve::Audit::Entry.where(guard: "none").distinct.pluck(:tool_name)
294
+ ```
295
+
296
+ Flip to `:deny` when that comes back empty, and the mode stops being reachable by accident.
297
+
298
+ The compliance checks work here too, and they take an `invoke:` argument precisely so they
299
+ run against your dispatcher rather than a synthetic call:
300
+
301
+ ```ruby
302
+ Reeve::Checks.run_all(
303
+ principals: [alice, bob],
304
+ invoke: ->(tool:, principal:, arguments:) { McpServer.dispatch(tool, principal, arguments) }
305
+ )
306
+ ```
307
+
308
+ That is the whole integration: one call site, your auth untouched, and the same three
309
+ guarantees the fast-mcp adapter gets.
310
+
222
311
  Policies are plain objects unless you want Pundit (`authorize` and `scope`, two methods).
223
312
  The ledger is an ActiveRecord table unless you supply your own recorder. Records are
224
313
  ActiveRecord unless they are not — a plain object with an `id` works.
@@ -14,7 +14,7 @@
14
14
  # GRANT INSERT, SELECT ON reeve_audit_entries TO app_role;
15
15
  # GRANT USAGE ON SEQUENCE reeve_audit_entries_id_seq TO app_role;
16
16
  #
17
- # Also explicitly out of scope for v1, so you can plan around it:
17
+ # Also explicitly out of scope for now, so you can plan around it:
18
18
  # * no retention, rotation or archival — this table grows until you prune it, and
19
19
  # pruning is your policy, run by a role that is allowed to delete;
20
20
  # * no cryptographic chaining or tamper-evidence. If that lands later it arrives as a
@@ -53,6 +53,14 @@ class CreateReeveAuditEntries < ActiveRecord::Migration[7.0]
53
53
  t.string :guard, null: false
54
54
  t.integer :duration_ms
55
55
  t.json :metadata
56
+
57
+ # The audit-entry contract version this row was written under. A ledger outlives
58
+ # the gem version that wrote it, and a row that cannot name its own shape has to be
59
+ # interpreted by guesswork: contract 1 wrote `metadata` NULL whatever the caller
60
+ # passed, so on a version 1 row NULL means "never recorded" and on a version 2 row
61
+ # it means "the caller passed none". Stamping each row keeps an export readable
62
+ # across an upgrade instead of collapsing that distinction.
63
+ t.integer :contract_version, null: false
56
64
  end
57
65
 
58
66
  add_index :reeve_audit_entries, :invocation_id, unique: true
@@ -16,7 +16,9 @@ module Reeve
16
16
  DENY = "deny"
17
17
  OUTCOMES = [ALLOW, DENY].freeze
18
18
 
19
- REQUIRED = %i[invocation_id occurred_at agent_id tool_name outcome rule guard].freeze
19
+ REQUIRED = %i[
20
+ invocation_id occurred_at agent_id tool_name outcome rule guard contract_version
21
+ ].freeze
20
22
 
21
23
  validates(*REQUIRED, presence: true)
22
24
  validates :invocation_id, uniqueness: true
@@ -24,8 +26,12 @@ module Reeve
24
26
 
25
27
  before_destroy { throw :abort }
26
28
 
27
- # The contract version this table's shape implements (FR-015). Exposed on the model
28
- # so an export can stamp the rows it carries.
29
+ # The contract version this build of the gem writes (FR-015).
30
+ #
31
+ # Class-level, and deliberately not the same question as `entry.contract_version`:
32
+ # this is what the gem implements *now*, while the column on each row is the shape
33
+ # that row was actually written under. They differ for every row written before an
34
+ # upgrade, which is the whole reason the column exists.
29
35
  def self.contract_version
30
36
  CONTRACT_VERSION
31
37
  end
@@ -89,7 +89,11 @@ module Reeve
89
89
  derived: attributes[:derived] ? true : false,
90
90
  guard: blank?(attributes[:guard]) ? "policy" : attributes[:guard].to_s,
91
91
  duration_ms: attributes[:duration_ms],
92
- metadata: attributes[:metadata]
92
+ metadata: redact_metadata(attributes),
93
+ # Stamped from the constant rather than from the caller: the row records the
94
+ # shape the gem that wrote it implements, which is not something an adapter or
95
+ # a host is in a position to assert.
96
+ contract_version: CONTRACT_VERSION
93
97
  )
94
98
  end
95
99
 
@@ -115,6 +119,20 @@ module Reeve
115
119
  Redactor.for(attributes[:tool_name], config: config).call(attributes[:arguments])
116
120
  end
117
121
 
122
+ # Metadata is transport detail, and the transport is where the credentials are: the
123
+ # fast-mcp bridge puts the whole header hash in here, `Authorization` included. It
124
+ # goes through the same redactor as the arguments — which recurses into nested
125
+ # hashes and already knows `authorization`, `token` and friends by name — so
126
+ # recording metadata does not turn the ledger into a place bearer tokens accumulate.
127
+ # nil stays nil rather than becoming `{}`: a call that carried no metadata should
128
+ # not be indistinguishable from one whose metadata was emptied.
129
+ def redact_metadata(attributes)
130
+ metadata = attributes[:metadata]
131
+ return nil if metadata.nil? || metadata.empty?
132
+
133
+ Redactor.for(attributes[:tool_name], config: config).call(metadata)
134
+ end
135
+
118
136
  # FR-014: identifiers are capped, never silently dropped — the count stays true and
119
137
  # the row says it was truncated. The scoper caps first; this is the backstop that
120
138
  # makes the guarantee hold at the ledger regardless of who produced the list.
data/lib/reeve/audit.rb CHANGED
@@ -43,16 +43,25 @@ module Reeve
43
43
  # table. Immutability is enforced at the library level; the generated migration
44
44
  # documents the `GRANT INSERT, SELECT` that enforces the rest where it can actually be
45
45
  # enforced, and the gem makes no stronger claim than that.
46
- # * No retention, rotation or archival in v1 — the table is host-owned and the host's
46
+ # * No retention, rotation or archival yet — the table is host-owned and the host's
47
47
  # existing policies apply.
48
- # * No cryptographic chaining or tamper-evidence in v1. If that lands it arrives as a
48
+ # * No cryptographic chaining or tamper-evidence yet. If that lands it arrives as a
49
49
  # nullable column, which the audit-entry contract's versioning already permits.
50
+ #
51
+ # ("yet" rather than "in v1": the gem version and the audit-entry contract version are
52
+ # different numbers that move independently, and writing v1 for one of them read as the
53
+ # other.)
50
54
  module Audit
51
55
  # The version of the audit-entry shape, as documented in
52
56
  # specs/001-guardrails-core/contracts/audit-entry.md (FR-015). Adding a nullable
53
57
  # column is a MINOR change and leaves this alone; removing or renaming a column, or
54
58
  # changing what a value means, is MAJOR and bumps it.
55
- CONTRACT_VERSION = 1
59
+ #
60
+ # 2 — `metadata` carries the transport detail the caller passed. Through version 1 it
61
+ # was written NULL on every row regardless of what was passed, so anything mapping
62
+ # version 1 rows could reasonably have read the column as "always empty". The shape
63
+ # did not change; what a value means did.
64
+ CONTRACT_VERSION = 2
56
65
 
57
66
  TABLE_NAME = "reeve_audit_entries"
58
67
 
@@ -22,15 +22,28 @@ module Reeve
22
22
  return false unless pundit_loaded?
23
23
  return false unless policy.is_a?(Class)
24
24
 
25
- # `false` matters: const_defined? otherwise walks up to Object, where any
26
- # top-level Scope constant would make an unrelated policy look Pundit-shaped.
27
- policy.const_defined?(:Scope, false) &&
25
+ scope_defined?(policy) &&
28
26
  policy.public_instance_methods.any? { |method| method.to_s.end_with?("?") }
29
27
  end
30
28
 
29
+ # Pundit policies ordinarily inherit their Scope from a base policy
30
+ # (`class LeadPolicy < LeadBasePolicy`), so asking only the policy's own namespace
31
+ # rejected the common case. This walks the ancestry instead, stopping before
32
+ # Object — which is the whole reason the check was narrow to begin with: a
33
+ # top-level `Scope` constant must never make an unrelated class look
34
+ # Pundit-shaped. `policy::Scope` resolves along the same ancestry, so what
35
+ # `supports?` accepts is exactly what `scope` can later reach.
36
+ def self.scope_defined?(policy)
37
+ policy_ancestry(policy).any? { |ancestor| ancestor.const_defined?(:Scope, false) }
38
+ end
39
+
40
+ def self.policy_ancestry(policy)
41
+ policy.ancestors.take_while { |ancestor| ancestor != Object }
42
+ end
43
+
31
44
  def self.missing_methods(policy)
32
45
  return [] if supports?(policy)
33
- return [:Scope] if policy.is_a?(Class) && !policy.const_defined?(:Scope, false)
46
+ return [:Scope] if policy.is_a?(Class) && !scope_defined?(policy)
34
47
 
35
48
  %i[query_method Scope]
36
49
  end
data/lib/reeve/context.rb CHANGED
@@ -56,6 +56,12 @@ module Reeve
56
56
 
57
57
  # The audit-facing projection. The recorder adds the outcome, rule and records;
58
58
  # everything here is known before the tool runs.
59
+ #
60
+ # `metadata` belongs here even though nothing in the envelope reads it: the ledger has
61
+ # a column for it, the recorder maps it, and the contract check requires it — but this
62
+ # method used to omit it, so the column was written NULL on every call ever made. The
63
+ # transport detail a reviewer most wants after an incident (which request, which
64
+ # headers, which client) was accepted at the front door and dropped before the write.
59
65
  def to_h
60
66
  {
61
67
  invocation_id: invocation_id,
@@ -65,7 +71,8 @@ module Reeve
65
71
  agent_name: agent_name,
66
72
  principal_type: principal_type,
67
73
  principal_id: principal_id,
68
- arguments: arguments
74
+ arguments: arguments,
75
+ metadata: metadata
69
76
  }
70
77
  end
71
78
 
@@ -11,20 +11,44 @@ module Reeve
11
11
  # upgraded the gem and skipped the migration has a ledger one shape behind, and every
12
12
  # other check in this kit would go on passing while columns quietly went unwritten.
13
13
  #
14
+ # What each half of this check is worth, stated plainly because it is easy to read
15
+ # more into the version number than it carries:
16
+ #
17
+ # * The **column list** is the real test. It is compared against the columns the
18
+ # host's table actually has, and it is written out by hand rather than read back
19
+ # off the model — a check that derives its expectation from the thing it is
20
+ # checking checks nothing.
21
+ # * The **version number** is compared against what the ledger model reports, which
22
+ # is this gem's own +Audit::CONTRACT_VERSION+ — so by default it compares the gem
23
+ # to itself and can only pass. It earns its keep when a host passes `expected:` to
24
+ # pin the version it built its exports against.
25
+ #
26
+ # Those two are not as separate as they look, and that is by design. A contract bump
27
+ # is defined as a change to the shape or to what a value means; every such bump also
28
+ # moves the `contract_version` column, which means the column list catches a stale
29
+ # table even when the bump was purely semantic. Contract 2 is the worked example: the
30
+ # change was `metadata`'s meaning, no existing column moved, and a host still on the
31
+ # contract 1 table fails here on a missing `contract_version` column rather than
32
+ # passing while writing rows nothing can interpret.
33
+ #
34
+ # The constant below is written out rather than read from +Audit+ for the same
35
+ # reason the column list is, and one more: the testing kit loads with no ledger and
36
+ # no ActiveRecord at all (spec/reeve/testing/isolation_spec.rb), so it cannot
37
+ # reference the audit module. Bumping the contract means editing both by hand, and
38
+ # spec/reeve/audit/contract_version_spec.rb fails if they drift.
39
+ #
14
40
  # Reeve::Checks::ContractVersion.new.call
41
+ # Reeve::Checks::ContractVersion.new(expected: 2).call # pinned by the host
15
42
  class ContractVersion < Base
16
43
  TABLE = "reeve_audit_entries"
17
44
 
18
- # Contract version 1, as documented in contracts/audit-entry.md. This list is
19
- # deliberately written out rather than read back off the model: a check that
20
- # derives its expectation from the thing it is checking checks nothing.
21
45
  COLUMNS = %w[
22
46
  invocation_id occurred_at agent_id agent_name principal_type principal_id
23
47
  tool_name arguments outcome rule detail record_type record_ids record_count
24
- truncated derived guard duration_ms metadata
48
+ truncated derived guard duration_ms metadata contract_version
25
49
  ].freeze
26
50
 
27
- EXPECTED_VERSION = 1
51
+ EXPECTED_VERSION = 2
28
52
 
29
53
  def initialize(tool: nil, expected: EXPECTED_VERSION, ledger: nil)
30
54
  super(tool: tool, ledger: ledger)
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.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/reeve.rb CHANGED
@@ -12,7 +12,8 @@ require_relative "reeve/authorization"
12
12
  # Reeve — per-record authorization and an append-only audit ledger for the MCP tools
13
13
  # a Rails application exposes to AI agents.
14
14
  #
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.
15
+ # A tool declares the policy that governs it with `guard_with`; every invocation is
16
+ # authorized, scoped to what the principal may see, and recorded. See
17
+ # https://github.com/vicmaster/reeve.
17
18
  module Reeve
18
19
  end
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.1.0
4
+ version: 0.2.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-08-11 00:00:00.000000000 Z
11
+ date: 2026-08-12 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)