otto 2.9.0 → 2.10.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +5 -0
  3. data/.github/workflows/ci.yml +11 -8
  4. data/.github/workflows/claude-code-review.yml +38 -13
  5. data/.github/workflows/claude.yml +10 -8
  6. data/.github/workflows/code-smells.yml +5 -5
  7. data/.github/workflows/release-gem.yml +2 -2
  8. data/.github/workflows/ruby-lint.yml +3 -3
  9. data/.github/workflows/yardoc.yml +5 -5
  10. data/.gitignore +1 -5
  11. data/.rubocop_todo.yml +7 -5
  12. data/AGENTS.md +22 -1
  13. data/CHANGELOG.rst +203 -0
  14. data/Gemfile +3 -3
  15. data/Gemfile.lock +11 -15
  16. data/README.md +75 -37
  17. data/docs/README.md +166 -0
  18. data/docs/adr/README.md +16 -0
  19. data/docs/adr/adr-001-route-authentication-at-handler-boundary.md +38 -0
  20. data/docs/adr/adr-002-multi-strategy-authentication-and-authorization.md +50 -0
  21. data/docs/adr/adr-003-caddy-tls-route-based-integration.md +48 -0
  22. data/docs/adr/adr-004-separate-compatibility-from-security-maintenance.md +58 -0
  23. data/docs/guides/authentication.md +377 -0
  24. data/docs/guides/caddy-tls.md +205 -0
  25. data/docs/guides/configuration_freezing.md +146 -0
  26. data/docs/guides/enrichment.md +161 -0
  27. data/docs/guides/forwarded-authority.md +249 -0
  28. data/docs/guides/geo-country.md +168 -0
  29. data/docs/guides/ip_privacy.md +39 -0
  30. data/docs/guides/ipaddr-encoding-quirk.md +56 -0
  31. data/docs/guides/mcp.md +282 -0
  32. data/docs/guides/privacy.md +193 -0
  33. data/docs/guides/routing.md +181 -0
  34. data/docs/guides/structured_logging.md +281 -0
  35. data/docs/guides/testing-guide.md +391 -0
  36. data/docs/maintainers/github-actions.md +41 -0
  37. data/docs/maintainers/investigations/.gitignore +2 -0
  38. data/docs/migrating/v2.0.0.md +337 -0
  39. data/docs/reference/authentication.md +290 -0
  40. data/docs/reference/route-syntax.md +181 -0
  41. data/docs/reference/runtime-and-dependency-security.md +86 -0
  42. data/examples/advanced_routes/README.md +43 -57
  43. data/examples/authentication_strategies/README.md +37 -196
  44. data/examples/basic/README.md +24 -39
  45. data/examples/basic/config.ru +0 -1
  46. data/examples/caddy_tls_demo/README.md +8 -2
  47. data/examples/lambda_handlers/README.md +11 -2
  48. data/examples/mcp_demo/README.md +75 -161
  49. data/examples/mcp_demo/config.ru +1 -0
  50. data/examples/security_features/README.md +36 -234
  51. data/lib/otto/caddy_tls/localhost_guard.rb +22 -24
  52. data/lib/otto/core/configuration.rb +36 -22
  53. data/lib/otto/core/file_safety.rb +89 -31
  54. data/lib/otto/core/middleware_stack.rb +36 -36
  55. data/lib/otto/core/router.rb +62 -19
  56. data/lib/otto/env_keys.rb +20 -2
  57. data/lib/otto/mcp/auth/token.rb +10 -4
  58. data/lib/otto/mcp/core.rb +23 -5
  59. data/lib/otto/mcp/endpoint.rb +41 -0
  60. data/lib/otto/mcp/errors.rb +15 -0
  61. data/lib/otto/mcp/options.rb +292 -0
  62. data/lib/otto/mcp/protocol.rb +52 -22
  63. data/lib/otto/mcp/rate_limiting.rb +175 -97
  64. data/lib/otto/mcp/registry.rb +14 -14
  65. data/lib/otto/mcp/schema_validation.rb +20 -12
  66. data/lib/otto/mcp/server.rb +131 -32
  67. data/lib/otto/optional_dependency.rb +57 -0
  68. data/lib/otto/privacy/config.rb +10 -8
  69. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  70. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  71. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  72. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  73. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  74. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  75. data/lib/otto/security/config.rb +328 -14
  76. data/lib/otto/security/configurator.rb +36 -6
  77. data/lib/otto/security/core.rb +19 -1
  78. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  79. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  80. data/lib/otto/security/rate_limiter.rb +81 -46
  81. data/lib/otto/utils.rb +50 -0
  82. data/lib/otto/version.rb +1 -1
  83. data/lib/otto.rb +9 -11
  84. data/otto.gemspec +0 -2
  85. metadata +31 -41
  86. data/docs/.gitignore +0 -10
  87. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  88. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  89. data/docs/enrichment.md +0 -128
  90. data/docs/geo-country.md +0 -181
  91. data/docs/ipaddr-encoding-quirk.md +0 -34
  92. data/docs/migrating/v2.0.0-pre1.md +0 -276
  93. data/docs/migrating/v2.0.0-pre2.md +0 -338
  94. data/docs/modern-authentication-authorization-landscape.md +0 -558
  95. data/docs/multi-strategy-authentication-design.md +0 -1401
  96. data/docs/reverse-proxy-network-services.md +0 -371
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bbfc031ec4ff48d3d0ce373a4a8cbbfac62571f80a07ef54427e71a3fc34444
4
- data.tar.gz: 4cf1c8c6171a8af0f18dcc61c8c00b86c9bc829b573e8e3b727b1047ad2ce230
3
+ metadata.gz: ebb9d40d464e54cb0a25d0014dfa6868905e833c400701739d61463ae56b7265
4
+ data.tar.gz: f111ef655d58256cece33215fe05a7711c5f1bd0e06925c41e58b0e492f6dfb5
5
5
  SHA512:
6
- metadata.gz: c4405b81202ecf23a1f09629ccc8392a70a1fa6d0287c46ba4590517f884e527ecca5d909475b6ff772ad857def5686991c99b9b93dde9489c040d3502662f73
7
- data.tar.gz: 155b0b637c48466d3ad68f91f28647c6f7a7a6cf4c60aac343af270c09a0be98f6fa5c1764d94e4886a148ee8d9f226c1567118e865404b3add638dbe50ee2a5
6
+ metadata.gz: c1381ef06000d127d7411f50e797dbf14720821873bc8f9b70e1e01a3a232fb7a8f2ebaee7e0ec4ed8a9d61dfc8cf8d67753de21702ef253dd42b4e1736949ea
7
+ data.tar.gz: 7105c5561be5e5f3cfe771b41cc1d576c0ede262cecc92347cc518cbdd0c57deaefca50a923a535661a47ac6a977a1171a6dab9cb3f9ff59ca993c5a1a2d326a
@@ -9,6 +9,11 @@ updates:
9
9
  directory: "/" # Location of package manifests
10
10
  schedule:
11
11
  interval: "weekly"
12
+ # All action bumps arrive as one PR instead of one per action.
13
+ groups:
14
+ github-actions:
15
+ patterns:
16
+ - "*"
12
17
  - package-ecosystem: "bundler"
13
18
  directory: "/" # Location of package manifests
14
19
  schedule:
@@ -28,15 +28,18 @@ jobs:
28
28
  fail-fast: false
29
29
  matrix:
30
30
  # Each Ruby runs twice: once against the committed Gemfile.lock
31
- # (floor of the declared version range, reproducible) and once
32
- # with the lockfile removed so Bundler resolves fresh inside the
33
- # gemspec's pessimistic constraints (ceiling, what a downstream
34
- # user will actually hit). The unlocked cells catch upstream
35
- # releases that satisfy `~> X.Y` but break Otto at load time -
31
+ # (the reproducible maintainer snapshot) and once with the lockfile
32
+ # removed so Bundler resolves currently available versions inside the
33
+ # gemspec constraints. Neither resolution is a patched security floor;
34
+ # consumers must audit their own lockfiles. The unlocked cells catch
35
+ # upstream releases that satisfy `~> X.Y` but break Otto at load time -
36
36
  # e.g. facets 3.2.0 shipping a self-referential
37
37
  # `require_relative 'file/write.rb'` against a file deleted in
38
38
  # the same release, the reason 2.0.2 exists.
39
39
  include:
40
+ # Ruby 3.2 is EOL upstream but remains a blocking compatibility
41
+ # target. Ruby 3.5 and 4.0 remain provisional, non-blocking targets.
42
+ # See docs/reference/runtime-and-dependency-security.md.
40
43
  - ruby: "3.2"
41
44
  experimental: false
42
45
  lockfile: "locked"
@@ -69,9 +72,9 @@ jobs:
69
72
  lockfile: "unlocked"
70
73
 
71
74
  steps:
72
- - uses: actions/checkout@v7.0.1
75
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
73
76
  - name: Set up Ruby
74
- uses: ruby/setup-ruby@v1
77
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
75
78
  continue-on-error: ${{ matrix.experimental }}
76
79
  with:
77
80
  ruby-version: ${{ matrix.ruby }}
@@ -80,7 +83,7 @@ jobs:
80
83
  bundler-cache: ${{ !matrix.experimental && matrix.lockfile == 'locked' }}
81
84
 
82
85
  - name: Setup tmate session
83
- uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3
86
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24
84
87
  if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
85
88
  with:
86
89
  detached: true
@@ -30,13 +30,20 @@ jobs:
30
30
  # CLAUDE_CODE_OAUTH_TOKEN is empty and the action would fail.
31
31
  # 3. The event is a freshly opened PR, a 'claude-review' label add, or a push
32
32
  # to a PR that already carries the 'claude-review' label.
33
+ # A manual workflow_dispatch run bypasses 2 and 3 (it can only be started by a
34
+ # user with write access, on a branch in this repo) but still skips bots.
33
35
  if: ${{
34
36
  !endsWith(github.actor, '[bot]') &&
35
- !github.event.pull_request.head.repo.fork &&
36
37
  (
37
- (github.event.action == 'opened') ||
38
- (github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
39
- (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'claude-review'))
38
+ github.event_name == 'workflow_dispatch' ||
39
+ (
40
+ !github.event.pull_request.head.repo.fork &&
41
+ (
42
+ (github.event.action == 'opened') ||
43
+ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
44
+ (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'claude-review'))
45
+ )
46
+ )
40
47
  )
41
48
  }}
42
49
 
@@ -48,13 +55,13 @@ jobs:
48
55
 
49
56
  steps:
50
57
  - name: Checkout repository
51
- uses: actions/checkout@v7.0.1
58
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
52
59
  with:
53
60
  fetch-depth: 1
54
61
 
55
62
  - name: Run Claude Code Review
56
63
  id: claude-review
57
- uses: anthropics/claude-code-action@beta
64
+ uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1.0.216
58
65
  with:
59
66
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
60
67
 
@@ -62,11 +69,26 @@ jobs:
62
69
  # runs) -> the CLAUDE_MODEL repo variable -> this built-in default.
63
70
  # Pinning a current id avoids the action's frozen default, which 404s
64
71
  # ("model: claude-sonnet-4-20250514"). Fall back to Sonnet on overload.
65
- model: "${{ inputs.model || vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
66
- fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
72
+ # v1 removed the model/fallback_model inputs; both are now CLI flags
73
+ # passed through claude_args.
74
+ claude_args: |
75
+ --model ${{ inputs.model || vars.CLAUDE_MODEL || 'claude-opus-4-6' }}
76
+ --fallback-model ${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}
77
+
78
+ # Prompt for automated review (no @claude mention needed).
79
+ # v1 renamed direct_prompt -> prompt. track_progress restores the
80
+ # tracking/sticky comment that v0 automation mode posted by default; it
81
+ # only applies to pull_request/issue events, so a workflow_dispatch run
82
+ # has no PR to comment on and reports to the job log instead.
83
+ track_progress: true
84
+ prompt: |
85
+ REPO: ${{ github.repository }}
86
+ PR NUMBER: ${{ github.event.pull_request.number || 'none (manual run)' }}
87
+
88
+ If no PR number is given, use `gh` to find an open pull request whose
89
+ head is ${{ github.ref_name }}; if there is none, say so and stop.
90
+ (The checkout is shallow, so compare via the API, not local git.)
67
91
 
68
- # Direct prompt for automated review (no @claude mention needed)
69
- direct_prompt: |
70
92
  Please review this pull request and provide feedback on:
71
93
  - Code quality and best practices
72
94
  - Potential bugs or issues
@@ -80,9 +102,12 @@ jobs:
80
102
  use_sticky_comment: true
81
103
 
82
104
  - name: Remove claude-review label
83
- # Remove label whether success or failure - prevents getting stuck
84
- if: always() && github.event.action != 'opened'
85
- uses: actions/github-script@v9
105
+ # Remove label whether success or failure - prevents getting stuck.
106
+ # Only for pull_request events: a workflow_dispatch payload has no
107
+ # issue number, so context.issue.number would be undefined and the
108
+ # removeLabel call would fail (silently, below) with the label intact.
109
+ if: always() && github.event_name == 'pull_request' && github.event.action != 'opened'
110
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
86
111
  with:
87
112
  script: |
88
113
  try {
@@ -26,13 +26,13 @@ jobs:
26
26
  actions: read # Required for Claude to read CI results on PRs
27
27
  steps:
28
28
  - name: Checkout repository
29
- uses: actions/checkout@v7.0.1
29
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30
30
  with:
31
31
  fetch-depth: 1
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@beta
35
+ uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1.0.216
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -40,8 +40,11 @@ jobs:
40
40
  # editing this file. Pinning a current id avoids the action's frozen
41
41
  # default, which 404s ("model: claude-sonnet-4-20250514"). Fall back to
42
42
  # Sonnet if the primary is unavailable or overloaded.
43
- model: "${{ vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
44
- fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
43
+ # v1 removed the model/fallback_model inputs; both are now CLI flags
44
+ # passed through claude_args.
45
+ claude_args: |
46
+ --model ${{ vars.CLAUDE_MODEL || 'claude-opus-4-6' }}
47
+ --fallback-model ${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}
45
48
 
46
49
  # This is an optional setting that allows Claude to read CI results on PRs
47
50
  additional_permissions: |
@@ -50,7 +53,6 @@ jobs:
50
53
  # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
51
54
  # prompt: 'Update the pull request description to include a summary of changes.'
52
55
 
53
- # Optional: Add claude_args to customize behavior and configuration
54
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
55
- # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
56
- # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
56
+ # More claude_args options (tool allowlists, --max-turns, --mcp-config):
57
+ # https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
58
+ # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line
@@ -21,10 +21,10 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Checkout code
24
- uses: actions/checkout@v7.0.1
24
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
25
 
26
26
  - name: Set up Ruby
27
- uses: ruby/setup-ruby@v1
27
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
28
28
  with:
29
29
  ruby-version: 3.4
30
30
 
@@ -71,7 +71,7 @@ jobs:
71
71
  continue-on-error: true
72
72
 
73
73
  - name: Upload Reek report as artifact
74
- uses: actions/upload-artifact@v7
74
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
75
75
  if: always()
76
76
  with:
77
77
  name: reek-report
@@ -88,10 +88,10 @@ jobs:
88
88
 
89
89
  steps:
90
90
  - name: Checkout code
91
- uses: actions/checkout@v7.0.1
91
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
92
92
 
93
93
  - name: Set up Ruby
94
- uses: ruby/setup-ruby@v1
94
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
95
95
  with:
96
96
  ruby-version: 3.4
97
97
  bundler-cache: true
@@ -139,7 +139,7 @@ jobs:
139
139
  persist-credentials: false
140
140
 
141
141
  - name: Set up Ruby
142
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
142
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
143
143
  with:
144
144
  bundler-cache: true
145
145
  # Pinned to 3.3 (oldest non-experimental Ruby in the CI matrix).
@@ -169,4 +169,4 @@ jobs:
169
169
  echo "Releasing otto ${gem_version} from tag ${RELEASE_TAG}"
170
170
 
171
171
  - name: Build and push gem to RubyGems
172
- uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
172
+ uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1
@@ -44,15 +44,15 @@ jobs:
44
44
 
45
45
  steps:
46
46
  - name: Checkout code
47
- uses: actions/checkout@v7.0.1
47
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
48
48
 
49
- - uses: ruby/setup-ruby@v1
49
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
50
50
  with:
51
51
  ruby-version: ${{ matrix.ruby }}
52
52
  bundler-cache: true
53
53
 
54
54
  - name: Setup tmate session
55
- uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3
55
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24
56
56
  if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
57
57
  with:
58
58
  detached: true
@@ -42,12 +42,12 @@ jobs:
42
42
 
43
43
  steps:
44
44
  - name: Checkout repository
45
- uses: actions/checkout@v7.0.1
45
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
46
  with:
47
47
  fetch-depth: 0
48
48
 
49
49
  - name: Set up Ruby environment
50
- uses: ruby/setup-ruby@v1
50
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
51
51
  with:
52
52
  ruby-version: '3.4'
53
53
  bundler-cache: true
@@ -82,10 +82,10 @@ jobs:
82
82
  echo "::endgroup::"
83
83
 
84
84
  - name: Setup GitHub Pages configuration
85
- uses: actions/configure-pages@v6
85
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
86
86
 
87
87
  - name: Upload documentation artifact
88
- uses: actions/upload-pages-artifact@v5
88
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
89
89
  with:
90
90
  path: './doc'
91
91
 
@@ -102,7 +102,7 @@ jobs:
102
102
  steps:
103
103
  - name: Deploy to GitHub Pages
104
104
  id: deployment
105
- uses: actions/deploy-pages@v5
105
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
106
106
 
107
107
  notify-completion:
108
108
  timeout-minutes: 5
data/.gitignore CHANGED
@@ -1,19 +1,14 @@
1
1
  .DS_Store
2
2
  .bundle
3
- .byebug*
4
3
  .history
5
4
  .devcontainer
6
5
  .vscode
7
6
  *.env
8
7
  *.log
9
- *.md
10
8
  *.txt
11
9
  .*.json
12
10
  !LICENSE.txt
13
11
  !spec/fixtures/*.txt
14
- !examples/**/*.md
15
- !README.md
16
- !CLAUDE.md
17
12
  .ruby-version
18
13
  appendonlydir
19
14
  data/
@@ -27,3 +22,4 @@ vendor
27
22
  .mcp.json
28
23
  .claude
29
24
  .serena
25
+ .yardoc
data/.rubocop_todo.yml CHANGED
@@ -296,15 +296,11 @@ Lint/DuplicateMethods:
296
296
  Exclude:
297
297
  - 'lib/otto/privacy/geo_resolver.rb'
298
298
 
299
- # Offense count: 5
299
+ # Offense count: 1
300
300
  # Configuration parameters: AllowedParentClasses.
301
301
  Lint/MissingSuper:
302
302
  Exclude:
303
303
  - 'lib/otto/mcp/rate_limiting.rb'
304
- - 'lib/otto/security/authentication/strategies/api_key_strategy.rb'
305
- - 'lib/otto/security/authentication/strategies/permission_strategy.rb'
306
- - 'lib/otto/security/authentication/strategies/role_strategy.rb'
307
- - 'lib/otto/security/authentication/strategies/session_strategy.rb'
308
304
 
309
305
  # Offense count: 12
310
306
  # This cop supports unsafe autocorrection (--autocorrect-all).
@@ -601,6 +597,9 @@ RSpec/DescribeMethod:
601
597
  - 'spec/otto/initialization_spec.rb'
602
598
  - 'spec/otto/locale_config_spec.rb'
603
599
  - 'spec/otto/mcp/rate_limiting_spec.rb'
600
+ - 'spec/otto/mcp/protocol_spec.rb'
601
+ - 'spec/otto/mcp/registry_spec.rb'
602
+ - 'spec/otto/mcp/route_parser_spec.rb'
604
603
  - 'spec/otto/mcp_route_parsing_spec.rb'
605
604
  - 'spec/otto/rate_limiting_spec.rb'
606
605
  - 'spec/otto/response_integration_spec.rb'
@@ -863,6 +862,9 @@ RSpec/SpecFilePathFormat:
863
862
  - 'spec/otto/file_safety_spec.rb'
864
863
  - 'spec/otto/locale_config_spec.rb'
865
864
  - 'spec/otto/mcp/rate_limiting_spec.rb'
865
+ - 'spec/otto/mcp/protocol_spec.rb'
866
+ - 'spec/otto/mcp/registry_spec.rb'
867
+ - 'spec/otto/mcp/route_parser_spec.rb'
866
868
  - 'spec/otto/mcp/token_auth_spec.rb'
867
869
  - 'spec/otto/mcp_route_parsing_spec.rb'
868
870
  - 'spec/otto/rate_limiting_spec.rb'
data/AGENTS.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  This file provides essential guidance to AI agents when working with Otto.
4
4
 
5
+ ## Project claims and source authority
6
+
7
+ ### Attribution
8
+
9
+ Do not infer project terminology or guarantees from repetition. Before attributing a claim to the project, locate an authoritative primary source and provide its exact wording. Treat delivery notes, commit messages, agent output, and documents created or modified during the current task as leads, not evidence. If the wording is absent, call it an interpretation or proposal. Never place paraphrases in quotation marks.
10
+
11
+ ### Authoritative sources
12
+
13
+ Authoritative sources must be identified explicitly; repository presence alone does not confer authority. Accepted specifications and ADRs may establish project claims only within their stated scope. Delivery notes, commit messages, issue discussions, summaries, and agent-authored text are non-authoritative unless an authoritative source incorporates them explicitly.
14
+
15
+ ### Normative claims
16
+
17
+ For normative claims concerning security, privacy, compatibility, persistence, or data loss:
18
+
19
+ 1. Cite the authoritative source and its exact wording.
20
+ 2. Distinguish quotations, paraphrases, interpretations, and proposals.
21
+ 3. Do not use material created or modified during the current task to validate that task’s claims.
22
+ 4. If no authoritative wording exists, report the claim as unsupported.
23
+
24
+
25
+
5
26
  ## Error Handler Registration
6
27
 
7
28
  Register handlers for expected business logic errors to avoid logging them as 500 errors:
@@ -56,7 +77,7 @@ Authentication is handled by `RouteAuthWrapper` at the handler level, NOT by mid
56
77
 
57
78
  ```ruby
58
79
  otto.add_auth_strategy('session', SessionStrategy.new)
59
- otto.add_auth_strategy('apikey', APIKeyStrategy.new)
80
+ otto.add_auth_strategy('apikey', APIKeyStrategy.new(api_keys: ENV.fetch('API_KEYS').split(',')))
60
81
  ```
61
82
 
62
83
  - Strategy names must be unique
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,171 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
7
7
 
8
8
  <!--scriv-insert-here-->
9
9
 
10
+ .. _changelog-2.10.0:
11
+
12
+ 2.10.0 — 2026-09-04
13
+ ===================
14
+
15
+ Added
16
+ -----
17
+
18
+ - ``APIKeyStrategy`` now accepts a block or ``resolver:`` callable instead of a
19
+ static ``api_keys:`` list, enabling database-, repository-, or cache-backed
20
+ key lookup. The resolved account becomes the authenticated user.
21
+
22
+ - ``APIKeyStrategy.digest(key)`` returns a full SHA-256 hex digest for stores
23
+ that look up generated API keys by digest. See
24
+ ``docs/guides/authentication.md`` for the resolver contract and credential
25
+ storage guidance.
26
+
27
+ - Directly exposed applications can now set ``trusted_proxies: :none`` or call
28
+ ``trust_no_proxies!`` to distrust every peer. Otto then ignores forwarded
29
+ client IPs and strips forwarded host, scheme, and port metadata. Leaving proxy
30
+ trust unconfigured continues to preserve forwarded metadata. Applications
31
+ behind a reverse proxy, including one on loopback, must explicitly trust it.
32
+ The sentinel is only valid as the whole option; a list containing it, such
33
+ as ``['none']``, is rejected at configuration time. See
34
+ ``docs/guides/forwarded-authority.md`` for configuration guidance. (#259)
35
+
36
+ Changed
37
+ -------
38
+
39
+ - MCP options now accept String or Symbol keys consistently and fail at boot
40
+ for unknown, conflicting, or invalid values. Constructor gating options must
41
+ be exactly ``true`` or ``false``; nil or blank token values are rejected.
42
+ Before upgrading, check option names against the `MCP guide
43
+ <docs/guides/mcp.md>`__. (#258)
44
+
45
+ - ``enable_mcp!`` now rejects constructor-only gating options and repeated
46
+ enablement. Pass all MCP settings in one ``Otto.new`` or ``enable_mcp!`` call.
47
+ (#258)
48
+
49
+ - MCP ``Rack::Attack`` throttle names are now endpoint-qualified, such as
50
+ ``mcp_requests:/_mcp`` and ``mcp_tool_calls:/_mcp``. Update integrations that
51
+ inspect throttle names directly. (#258)
52
+
53
+ - Otto applications in one process that resolve proxied requests must now use
54
+ the same forwarded-header family; incompatible configurations fail during
55
+ configuration. CIDR-based proxy trust supports only ``X-Forwarded-*`` headers; use
56
+ depth-based trust for ``Forwarded`` or both families. See
57
+ ``docs/guides/forwarded-authority.md`` for configuration guidance. (#252)
58
+
59
+ - Literal routes now consistently take precedence over static files at the same
60
+ path, regardless of which static files were requested earlier. Static files
61
+ continue to take precedence over dynamic routes. (#260)
62
+
63
+ - MaxMind-backed database paths now require ``maxmind-db ~> 1.2``. Before
64
+ upgrading, declare that range or provide a compatible reader object. See the
65
+ `geo-country <docs/guides/geo-country.md>`__ and
66
+ `enrichment <docs/guides/enrichment.md>`__ guides. (#255)
67
+
68
+ Removed
69
+ -------
70
+
71
+ - Removed ``add_static_path`` and the ``routes_static`` cache interface. Static
72
+ files are discovered directly from the configured ``public`` directory and
73
+ no longer need registration. (#260)
74
+
75
+ - Removed the unused ``rack-parser`` and ``rexml`` runtime dependencies.
76
+ Applications using either gem directly must now declare it themselves.
77
+ (#255)
78
+
79
+ Fixed
80
+ -----
81
+
82
+ - MCP middleware now executes rate limiting, authentication, and schema
83
+ validation in that order. Configured endpoints with a trailing slash are also
84
+ routable. (#258)
85
+
86
+ - Repeated MCP rate-limit configuration no longer produces duplicate throttle
87
+ log entries. (#258)
88
+
89
+ - MCP JSON-RPC errors now consistently use HTTP 400 for protocol errors, 404
90
+ for unknown resources or tools, and 500 for handler or server failures.
91
+ Unknown tools no longer return 500, failing resource handlers no longer
92
+ appear missing, and handler exception details are logged instead of returned
93
+ to clients. (#257)
94
+
95
+ - ``StrategyResult#has_role?`` and ``#has_permission?`` now derive their answer
96
+ from ``#roles`` and ``#permissions``, so the predicates agree with the
97
+ accessors for object-backed users exposing ``#roles`` and for ``Set`` or
98
+ other non-Array collections. A user model defining its own ``#has_role?`` or
99
+ ``#has_permission?`` is still consulted first.
100
+
101
+ - MCP rate-limited requests now emit one ``rack.attack`` log entry instead of
102
+ two. (#255)
103
+
104
+ Security
105
+ --------
106
+
107
+ - ``APIKeyStrategy`` now requires exactly one key source and rejects empty
108
+ static key lists at construction. Invalid presented credentials terminate a
109
+ multi-strategy authentication chain. Static keys are compared in constant
110
+ time as fixed-width SHA-256 digests, so configured key lengths are not
111
+ observable through timing. Blank credentials, empty or whitespace-only, are
112
+ rejected before any key source is consulted. (#256)
113
+
114
+ - Successful authentication results no longer expose the raw API key in
115
+ strategy-generated fields. Static-list callers should replace
116
+ ``user[:api_key]`` with ``user[:api_key_fingerprint]``. (#256)
117
+
118
+ - ``APIKeyStrategy`` now reads only the configured header by default. Applications
119
+ that still accept query or form parameters must opt in with ``param_name:``;
120
+ see ``docs/guides/authentication.md`` for configuration guidance. (#256)
121
+
122
+ - Fixed MCP bearer-token authentication being silently omitted when configured
123
+ through ``Otto.new`` or with String-keyed options. Deployments exposing MCP
124
+ beyond a trusted local environment should upgrade and verify their
125
+ ``auth_tokens`` configuration. Intentionally open endpoints now warn unless
126
+ acknowledged with ``allow_unauthenticated: true``. See the `MCP guide
127
+ <docs/guides/mcp.md>`__. (#258)
128
+
129
+ - MCP authentication now fails closed when its authenticator is unavailable.
130
+ Authentication, validation, and rate limiting also apply only to the exact
131
+ routed MCP endpoint. (#258)
132
+
133
+ - MCP rate limiting now honors configured limits and protects custom endpoints,
134
+ Rack-mounted applications, and distinct MCP endpoints in the same process.
135
+ Mount ``Rack::Attack`` inside the same ``map`` block as Otto, and use distinct
136
+ endpoint paths when applications require isolated counters. (#258)
137
+
138
+ - Static-file serving now rejects symlinks that resolve outside the configured
139
+ public directory, preventing files outside that root from being served on
140
+ first access or after another file has populated the directory cache.
141
+ Symlinks that resolve within the public directory, including a symlinked
142
+ public root, remain supported. (#257)
143
+
144
+ - When proxy trust is configured, forwarded host, scheme, and port metadata
145
+ from untrusted peers is now stripped before it can affect Rack's request
146
+ authority. Requests from trusted peers retain this metadata; see
147
+ ``docs/guides/forwarded-authority.md`` for deployment guidance. (#252)
148
+
149
+ - ``IPPrivacyMiddleware`` enforces its own proxy trust posture even when an
150
+ outer instance already resolved ``otto.client_ip``. ``trusted_proxies: :none``
151
+ always strips forwarded authority; a CIDR configuration that can no longer
152
+ match the connecting peer treats it as untrusted and logs a warning. (#259)
153
+
154
+ - ``env['otto.peer_relayed']`` records whether a request carried any relay
155
+ marker header, evaluated before forwarded carriers may be deleted, so
156
+ ``Otto::CaddyTLS::LocalhostGuard`` still refuses a relayed loopback call once
157
+ the carriers are stripped. The relay markers cover every carrier the scrub
158
+ deletes, including ``X-Forwarded-Host`` and the other authority headers, not
159
+ only the client-IP carriers. (#259)
160
+
161
+ - MCP now stops configuration with ``Otto::OptionalDependencyError`` rather
162
+ than starting without enabled schema validation or rate limiting. Before
163
+ upgrading, add ``json_schemer ~> 2.0`` and ``rack-attack ~> 6.7`` for the
164
+ default protections, or explicitly set ``enable_validation: false`` or
165
+ ``enable_rate_limiting: false`` only when that protection is not required.
166
+ See the `MCP guide <docs/guides/mcp.md>`__. (#255)
167
+
168
+ Documentation
169
+ -------------
170
+
171
+ - Added the `MCP guide <docs/guides/mcp.md>`__ covering secure enablement,
172
+ supported options, authentication, validation, rate limiting, mounted
173
+ applications, and error behavior. (#258)
174
+
10
175
  .. _changelog-2.9.0:
11
176
 
12
177
  2.9.0 — 2026-08-18
@@ -790,6 +955,44 @@ Added
790
955
  2.0.0 — 2026-03-14
791
956
  ==================
792
957
 
958
+ This is the stable release of Otto v2, the culmination of 10 pre-releases
959
+ since September 2025.
960
+
961
+ Highlights
962
+ ----------
963
+
964
+ - **Modular architecture**: the core ``Otto`` class is now a thin composition
965
+ of focused modules (Router, FileSafety, Configuration, ErrorHandler,
966
+ UriGenerator).
967
+ - **Security by default**: IP masking, user agent anonymization, CSRF
968
+ protection, input validation, and backtrace sanitization.
969
+ - **Privacy by default**: public IP masking, country-level geo-location only
970
+ (no external APIs), daily-rotating IP hashes for analytics.
971
+ - **Handler-level authentication**: authentication moved from middleware to
972
+ ``RouteAuthWrapper``, so it runs after routing.
973
+ - **Configuration freezing**: configuration is frozen after the first request
974
+ to prevent runtime security bypasses.
975
+ - **MCP support**: JSON-RPC 2.0 endpoints for CLI automation and integrations.
976
+ - **Base error classes**: ``NotFoundError``, ``BadRequestError``,
977
+ ``ForbiddenError`` and friends, with automatic HTTP status codes.
978
+ - **Request/response helpers**: extensible ``Otto::Request`` and
979
+ ``Otto::Response`` with application-specific helper registration.
980
+
981
+ Breaking changes
982
+ ----------------
983
+
984
+ Individual breaking changes are documented in the pre-release entries below.
985
+ The migrations most applications need:
986
+
987
+ - Logic class constructor: ``initialize(session, user, params, locale)`` →
988
+ ``initialize(context, params, locale)``
989
+ - Middleware stack: ``otto.middleware_stack <<`` → ``otto.use()``
990
+ - Request callbacks: ``Otto.on_request_complete`` → ``otto.on_request_complete``
991
+ (instance method)
992
+
993
+ See `docs/migrating/v2.0.0.md <docs/migrating/v2.0.0.md>`__ for the full
994
+ upgrade guide.
995
+
793
996
  Added
794
997
  -----
795
998
 
data/Gemfile CHANGED
@@ -18,9 +18,9 @@ end
18
18
  # bundle config set with 'optional'
19
19
  group :development, :test, optional: true do
20
20
  # Keep gems that need to be in both environments
21
- gem 'json_schemer'
21
+ gem 'json_schemer', '~> 2.0'
22
22
  gem 'maxmind-db', '~> 1.2' # Optional geo DB reader; exercised by geo specs
23
- gem 'rack-attack'
23
+ gem 'rack-attack', '~> 6.7'
24
24
  gem 'reek', '~> 6.5'
25
25
  end
26
26
 
@@ -29,7 +29,7 @@ group :development do
29
29
  gem 'debug'
30
30
  gem 'rackup' # Used to boot examples/ apps; not needed by specs
31
31
  gem 'rake', '~> 13.4', require: false # Provides `rake release` for release-gem.yml
32
- gem 'rubocop', '~> 1.89.0', require: false
32
+ gem 'rubocop', '~> 1.90.0', require: false
33
33
  gem 'rubocop-performance', require: false
34
34
  gem 'rubocop-rspec', require: false
35
35
  gem 'rubocop-thread_safety', require: false