otto 2.9.0 → 2.11.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 (100) 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 +8 -5
  12. data/AGENTS.md +40 -1
  13. data/CHANGELOG.rst +235 -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 +157 -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 +293 -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 +40 -23
  53. data/lib/otto/core/error_handler.rb +40 -2
  54. data/lib/otto/core/file_safety.rb +105 -31
  55. data/lib/otto/core/middleware_stack.rb +36 -36
  56. data/lib/otto/core/router.rb +94 -18
  57. data/lib/otto/core/static_mounts.rb +172 -0
  58. data/lib/otto/core.rb +1 -0
  59. data/lib/otto/env_keys.rb +20 -2
  60. data/lib/otto/mcp/auth/token.rb +10 -4
  61. data/lib/otto/mcp/core.rb +23 -5
  62. data/lib/otto/mcp/endpoint.rb +41 -0
  63. data/lib/otto/mcp/errors.rb +15 -0
  64. data/lib/otto/mcp/options.rb +292 -0
  65. data/lib/otto/mcp/protocol.rb +52 -22
  66. data/lib/otto/mcp/rate_limiting.rb +175 -97
  67. data/lib/otto/mcp/registry.rb +14 -14
  68. data/lib/otto/mcp/schema_validation.rb +20 -12
  69. data/lib/otto/mcp/server.rb +131 -32
  70. data/lib/otto/optional_dependency.rb +57 -0
  71. data/lib/otto/privacy/config.rb +10 -8
  72. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
  73. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
  74. data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
  75. data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
  76. data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
  77. data/lib/otto/security/authentication/strategy_result.rb +58 -28
  78. data/lib/otto/security/config.rb +328 -14
  79. data/lib/otto/security/configurator.rb +36 -6
  80. data/lib/otto/security/core.rb +19 -1
  81. data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
  82. data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
  83. data/lib/otto/security/rate_limiter.rb +81 -46
  84. data/lib/otto/static.rb +38 -0
  85. data/lib/otto/utils.rb +50 -0
  86. data/lib/otto/version.rb +1 -1
  87. data/lib/otto.rb +130 -13
  88. data/otto.gemspec +0 -2
  89. metadata +32 -41
  90. data/docs/.gitignore +0 -10
  91. data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
  92. data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
  93. data/docs/enrichment.md +0 -128
  94. data/docs/geo-country.md +0 -181
  95. data/docs/ipaddr-encoding-quirk.md +0 -34
  96. data/docs/migrating/v2.0.0-pre1.md +0 -276
  97. data/docs/migrating/v2.0.0-pre2.md +0 -338
  98. data/docs/modern-authentication-authorization-landscape.md +0 -558
  99. data/docs/multi-strategy-authentication-design.md +0 -1401
  100. 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: b8303453d265fcf2d5b0fa0754aafdbdb3e24eb89be9dc5a7b5611b4f2bcc699
4
+ data.tar.gz: 2081706f65a3475f95555d96547841691f906b015f8b012739abc7804923fc76
5
5
  SHA512:
6
- metadata.gz: c4405b81202ecf23a1f09629ccc8392a70a1fa6d0287c46ba4590517f884e527ecca5d909475b6ff772ad857def5686991c99b9b93dde9489c040d3502662f73
7
- data.tar.gz: 155b0b637c48466d3ad68f91f28647c6f7a7a6cf4c60aac343af270c09a0be98f6fa5c1764d94e4886a148ee8d9f226c1567118e865404b3add638dbe50ee2a5
6
+ metadata.gz: 75c496ee29c9c4ec0b05a8effda77e4e1dafc02080fb341a2320313011437f2b0bc0511ad6ca7d1b53928d6f75130693c974c72ed053aa35e5c6d16f60af0a5e
7
+ data.tar.gz: 9d9615efa73c7ebafafe7f7e1eb3e098f8c07f3200ac62882c52c39954087c73053fc9574beda1b1e11ecc836e0b9bacb70fe267c90fa92600531ef14fca1b63
@@ -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@19dda84776b3518d98b8798e591daee763049ed3 # v1.0.220
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@19dda84776b3518d98b8798e591daee763049ed3 # v1.0.220
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'
@@ -860,9 +859,13 @@ RSpec/SpecFilePathFormat:
860
859
  - 'spec/otto/enhanced_routing_spec.rb'
861
860
  - 'spec/otto/error_handler_registration_spec.rb'
862
861
  - 'spec/otto/error_handling_spec.rb'
862
+ - 'spec/otto/fallback_response_isolation_spec.rb'
863
863
  - 'spec/otto/file_safety_spec.rb'
864
864
  - 'spec/otto/locale_config_spec.rb'
865
865
  - 'spec/otto/mcp/rate_limiting_spec.rb'
866
+ - 'spec/otto/mcp/protocol_spec.rb'
867
+ - 'spec/otto/mcp/registry_spec.rb'
868
+ - 'spec/otto/mcp/route_parser_spec.rb'
866
869
  - 'spec/otto/mcp/token_auth_spec.rb'
867
870
  - 'spec/otto/mcp_route_parsing_spec.rb'
868
871
  - '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:
@@ -48,6 +69,24 @@ Helper modules should avoid overriding these methods inherited from Rack::Reques
48
69
 
49
70
  No runtime validation is performed for performance reasons. Overriding these methods will cause undefined behavior.
50
71
 
72
+ ## Static File Registration
73
+
74
+ Files under the `public:` directory are served without registration. Use
75
+ `mount_static` to bind a URL prefix to a directory outside it, or to verify a
76
+ required asset directory at boot:
77
+
78
+ ```ruby
79
+ otto = Otto.new('routes.txt', public: 'public')
80
+ otto.mount_static('/assets', root: 'build/assets')
81
+ ```
82
+
83
+ - Roots are canonicalized at registration; a missing or unsafe root raises `ArgumentError`
84
+ - Precedence is fixed: literal routes, then mounts (longest prefix first), then `public:`, then dynamic routes
85
+ - Must be registered before first request (before configuration freezing)
86
+ - `add_static_path` was removed in v2.10.0 and has no shim
87
+
88
+ See `docs/guides/routing.md` for the full contract.
89
+
51
90
  ## Authentication Architecture
52
91
 
53
92
  Authentication is handled by `RouteAuthWrapper` at the handler level, NOT by middleware.
@@ -56,7 +95,7 @@ Authentication is handled by `RouteAuthWrapper` at the handler level, NOT by mid
56
95
 
57
96
  ```ruby
58
97
  otto.add_auth_strategy('session', SessionStrategy.new)
59
- otto.add_auth_strategy('apikey', APIKeyStrategy.new)
98
+ otto.add_auth_strategy('apikey', APIKeyStrategy.new(api_keys: ENV.fetch('API_KEYS').split(',')))
60
99
  ```
61
100
 
62
101
  - Strategy names must be unique