hitch-rails 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 +7 -0
- data/CHANGELOG.md +103 -0
- data/MIT-LICENSE +20 -0
- data/README.md +460 -0
- data/SECURITY.md +118 -0
- data/app/controllers/concerns/hitch/cors_support.rb +97 -0
- data/app/controllers/concerns/hitch/host_validation.rb +51 -0
- data/app/controllers/concerns/hitch/issuer_url.rb +26 -0
- data/app/controllers/concerns/hitch/mcp/endpoint.rb +355 -0
- data/app/controllers/concerns/hitch/oauth_form_admission.rb +83 -0
- data/app/controllers/concerns/hitch/oauth_parameter_validation.rb +26 -0
- data/app/controllers/concerns/hitch/registration_admission.rb +115 -0
- data/app/controllers/concerns/hitch/request_admission.rb +46 -0
- data/app/controllers/concerns/hitch/uri_validation.rb +116 -0
- data/app/controllers/hitch/application_controller.rb +59 -0
- data/app/controllers/hitch/authorizations_controller.rb +152 -0
- data/app/controllers/hitch/metadata_controller.rb +114 -0
- data/app/controllers/hitch/preflights_controller.rb +14 -0
- data/app/controllers/hitch/public_endpoint_controller.rb +36 -0
- data/app/controllers/hitch/registrations_controller.rb +135 -0
- data/app/controllers/hitch/revocations_controller.rb +31 -0
- data/app/controllers/hitch/tokens_controller.rb +89 -0
- data/app/models/hitch/access_token.rb +267 -0
- data/app/models/hitch/application_record.rb +7 -0
- data/app/models/hitch/authorization_request.rb +252 -0
- data/app/models/hitch/client/credentials.rb +30 -0
- data/app/models/hitch/client.rb +237 -0
- data/app/models/hitch/client_authentication.rb +80 -0
- data/app/models/hitch/client_id_metadata/cache.rb +69 -0
- data/app/models/hitch/client_id_metadata/fetcher.rb +277 -0
- data/app/models/hitch/client_id_metadata/throttle.rb +119 -0
- data/app/models/hitch/client_id_metadata.rb +316 -0
- data/app/models/hitch/client_redirect_uri.rb +14 -0
- data/app/models/hitch/mcp/context.rb +91 -0
- data/app/models/hitch/mcp/forbidden.rb +10 -0
- data/app/models/hitch/mcp/internal/bearer_challenge.rb +51 -0
- data/app/models/hitch/mcp/internal/cors_policy.rb +53 -0
- data/app/models/hitch/mcp/internal/endpoint_error_reporter.rb +40 -0
- data/app/models/hitch/mcp/internal/error_normalizer.rb +74 -0
- data/app/models/hitch/mcp/internal/header_field.rb +31 -0
- data/app/models/hitch/mcp/internal/hmac_identity.rb +37 -0
- data/app/models/hitch/mcp/internal/host_authority.rb +51 -0
- data/app/models/hitch/mcp/internal/json_values.rb +182 -0
- data/app/models/hitch/mcp/internal/local_diagnosis.rb +31 -0
- data/app/models/hitch/mcp/internal/media_type.rb +61 -0
- data/app/models/hitch/mcp/internal/observation.rb +333 -0
- data/app/models/hitch/mcp/internal/registry_runtime.rb +312 -0
- data/app/models/hitch/mcp/internal/result_normalizer.rb +167 -0
- data/app/models/hitch/mcp/internal/sanitized_report.rb +36 -0
- data/app/models/hitch/mcp/internal/schema_contract.rb +173 -0
- data/app/models/hitch/mcp/internal/sdk_adapter/response_normalizer.rb +173 -0
- data/app/models/hitch/mcp/internal/sdk_adapter.rb +222 -0
- data/app/models/hitch/mcp/internal/server_info.rb +49 -0
- data/app/models/hitch/mcp/internal/verified_request.rb +229 -0
- data/app/models/hitch/mcp/internal.rb +11 -0
- data/app/models/hitch/mcp/rate_limit_key.rb +29 -0
- data/app/models/hitch/mcp/registry.rb +70 -0
- data/app/models/hitch/mcp/result.rb +63 -0
- data/app/models/hitch/mcp/tool.rb +148 -0
- data/app/models/hitch/oauth_request_parameters.rb +74 -0
- data/app/views/hitch/authorizations/new.html.erb +57 -0
- data/config/routes.rb +37 -0
- data/db/migrate/20260817000000_create_hitch_tables.rb +77 -0
- data/docs/operator/doctor.md +82 -0
- data/docs/operator/rate_limiting.md +98 -0
- data/docs/public_api/0.2.0.md +322 -0
- data/docs/removing.md +43 -0
- data/lib/generators/hitch/generator_guards.rb +36 -0
- data/lib/generators/hitch/install/install_generator.rb +168 -0
- data/lib/generators/hitch/install/templates/controller.rb.tt +11 -0
- data/lib/generators/hitch/install/templates/initializer.rb +40 -0
- data/lib/generators/hitch/install/templates/registry.rb +6 -0
- data/lib/generators/hitch/tool/templates/tool.rb.tt +54 -0
- data/lib/generators/hitch/tool/templates/tool_test.rb.tt +58 -0
- data/lib/generators/hitch/tool_generator.rb +153 -0
- data/lib/hitch/configuration.rb +386 -0
- data/lib/hitch/doctor.rb +647 -0
- data/lib/hitch/dynamic_registration_rate_limit.rb +75 -0
- data/lib/hitch/engine.rb +154 -0
- data/lib/hitch/mcp/configuration.rb +190 -0
- data/lib/hitch/mcp/protocol.rb +36 -0
- data/lib/hitch/mcp/test_helper.rb +203 -0
- data/lib/hitch/pkce.rb +18 -0
- data/lib/hitch/rack_form_guard.rb +109 -0
- data/lib/hitch/rate_limit_store.rb +47 -0
- data/lib/hitch/resource_uri.rb +71 -0
- data/lib/hitch/version.rb +5 -0
- data/lib/hitch-rails.rb +6 -0
- data/lib/hitch.rb +51 -0
- data/lib/tasks/hitch.rake +197 -0
- metadata +230 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Request admission for the Hitch MCP endpoint
|
|
2
|
+
|
|
3
|
+
The MCP tools specification requires invocation rate limiting. Hitch provides
|
|
4
|
+
one authenticated, endpoint-wide fixed-window limit shared across
|
|
5
|
+
`server/discover`, `tools/list`, and `tools/call` for the validated principal
|
|
6
|
+
and client.
|
|
7
|
+
|
|
8
|
+
Hitch counts through your application's own cache store, the same way
|
|
9
|
+
`ActionController::RateLimiting` does. It adds no service to your deployment.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Nothing is required. An application that already sets `config.cache_store` is
|
|
14
|
+
already configured:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# config/environments/production.rb
|
|
18
|
+
config.cache_store = :solid_cache_store
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The limit itself lives in the generated MCP initializer:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
config.mcp.request_limit = { to: 120, within: 1.minute }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To keep MCP admission out of your general cache, point it at a dedicated store:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
config.mcp.rate_limit_store = ActiveSupport::Cache::RedisCacheStore.new(
|
|
31
|
+
url: ENV["HITCH_MCP_REDIS_URL"]
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Any `ActiveSupport::Cache` store responding to `increment` is accepted.
|
|
36
|
+
|
|
37
|
+
## What production requires
|
|
38
|
+
|
|
39
|
+
Production boot fails closed when the resolved store cannot count one
|
|
40
|
+
principal's requests across the processes serving them. Three stores are
|
|
41
|
+
refused:
|
|
42
|
+
|
|
43
|
+
| Store | Why it is refused |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `:memory_store` | Per process; each worker keeps its own count |
|
|
46
|
+
| `:null_store` | Retains nothing, so no window ever accumulates |
|
|
47
|
+
| `:file_store` | Reads and writes without a lock, so counts are lost under concurrency |
|
|
48
|
+
|
|
49
|
+
Development and test may use any of them. Where the store cannot count at all,
|
|
50
|
+
admission is not enforced rather than failing every request — the same posture
|
|
51
|
+
Rails takes, and safe because production refuses those stores at boot.
|
|
52
|
+
|
|
53
|
+
## Accuracy
|
|
54
|
+
|
|
55
|
+
Counting is approximate at window boundaries under concurrency, which is what a
|
|
56
|
+
rate limit needs to be. Solid Cache on PostgreSQL is the loosest case: it can
|
|
57
|
+
lose a few increments when a window's key is first created
|
|
58
|
+
([rails/solid_cache#297](https://github.com/rails/solid_cache/pull/297)), so a
|
|
59
|
+
burst at a boundary may admit a handful of extra calls before the limit engages.
|
|
60
|
+
Redis and Memcached count exactly if you would rather they did.
|
|
61
|
+
|
|
62
|
+
## Operational notes
|
|
63
|
+
|
|
64
|
+
- Keys are HMAC digests of the validated principal type/id and client ID. Raw
|
|
65
|
+
principal IDs, client IDs, and bearer tokens never reach the store. Token
|
|
66
|
+
rotation cannot reset a caller's quota.
|
|
67
|
+
- The quota deliberately spans every host scope and tool for that
|
|
68
|
+
principal/client. Per-tool quotas and distributed concurrency leases are
|
|
69
|
+
later work.
|
|
70
|
+
- A rejection is `429` with a conservative `Retry-After` equal to the
|
|
71
|
+
configured window.
|
|
72
|
+
- An error the store raises is `503` before request body parsing, Registry
|
|
73
|
+
work, SDK dispatch, or host behavior. Note that `RedisCacheStore` and Solid
|
|
74
|
+
Cache do not raise on a backend outage — they swallow the error and return
|
|
75
|
+
nil, so during an outage admission is not enforced. That is the same posture
|
|
76
|
+
`ActionController::RateLimiting` has on the same stores; if the limit must
|
|
77
|
+
hold through outages, use a store that raises.
|
|
78
|
+
- Avoid an eviction policy that silently removes live quota keys. Eviction can
|
|
79
|
+
reset a caller's active window without producing a store error. If your
|
|
80
|
+
general cache is under memory pressure, give MCP admission a dedicated store.
|
|
81
|
+
- One short-lived key exists per active principal/client window.
|
|
82
|
+
|
|
83
|
+
## Deployment check
|
|
84
|
+
|
|
85
|
+
Run migrations and the doctor in the same environment and with the same secret
|
|
86
|
+
injection as the application processes:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
bin/rails db:migrate
|
|
90
|
+
bin/rails hitch:doctor
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `rate_limit_store` check drives the real configured store rather than
|
|
94
|
+
describing it: it increments an isolated random `hitch:doctor:v1:*` key twice,
|
|
95
|
+
asserts the counts are `1` then `2`, and removes the key. It never touches the
|
|
96
|
+
`hitch:mcp:rate-limit:v1:*` application quota namespace. The check reports the
|
|
97
|
+
store class, whether it is shared across processes, and the two probe counts;
|
|
98
|
+
it emits no credentials or store message text.
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# hitch-rails 0.2 public API
|
|
2
|
+
|
|
3
|
+
This file documents the `0.2.0` compatibility surface — the gem's first
|
|
4
|
+
public release. The authenticated `Hitch::MCP::Endpoint` is the only MCP
|
|
5
|
+
endpoint: it is stateless POST/OPTIONS, and it performs no notification/202
|
|
6
|
+
response shaping.
|
|
7
|
+
|
|
8
|
+
For a confidential token exchange, `client_secret_basic` remains the only
|
|
9
|
+
supported secret-bearing method. The form may repeat the same scalar
|
|
10
|
+
`client_id` carried by the Basic username because the official Python MCP 2.0
|
|
11
|
+
client emits that interoperable shape. Hitch rejects a body `client_secret`, a
|
|
12
|
+
mismatched body ID, and duplicate or structured IDs before authorization-code
|
|
13
|
+
consumption.
|
|
14
|
+
|
|
15
|
+
The first configured `supported_scopes` entry is the base/default scope named
|
|
16
|
+
by the initial MCP bearer challenge. Protected-resource metadata continues to
|
|
17
|
+
advertise the complete supported set, and a known tool's 403 challenge names
|
|
18
|
+
that tool's complete static scope requirement. This keeps the first grant least
|
|
19
|
+
privileged while preserving explicit step-up.
|
|
20
|
+
|
|
21
|
+
## Implemented surface
|
|
22
|
+
|
|
23
|
+
`Hitch::Configuration#mcp` returns the one persistent
|
|
24
|
+
`Hitch::MCP::Configuration` object owned by the top-level Hitch configuration
|
|
25
|
+
with these runtime settings:
|
|
26
|
+
|
|
27
|
+
- `enabled=` accepts `true` or `false` (default `false`) and is the one
|
|
28
|
+
explicit switch for the authenticated endpoint runtime. While it is false,
|
|
29
|
+
no other MCP setting is required or validated at boot.
|
|
30
|
+
- `server_info=` accepts a static Hash with nonempty String `name` and
|
|
31
|
+
`version`, plus optional String `title` and `instructions`. It is validated
|
|
32
|
+
once at boot/prepare, alongside registry validation, and a malformed value
|
|
33
|
+
fails the boot rather than the first request; the request path reads the
|
|
34
|
+
validated frozen value. Unset, it defaults to the application's dasherized
|
|
35
|
+
name and version "1.0.0".
|
|
36
|
+
- `max_request_bytes=` accepts a positive Integer and defaults to 1 MiB. The
|
|
37
|
+
current value is read for every request; overflow returns 413 before JSON
|
|
38
|
+
parsing.
|
|
39
|
+
- `max_result_bytes=` accepts a positive Integer and defaults to 1 MiB. The
|
|
40
|
+
current value is read for every invocation; a Result whose exact serialized
|
|
41
|
+
JSON exceeds it becomes the generic tool error.
|
|
42
|
+
- `scope_resolver=` accepts a callable with `principal:`, `access_token:`, and
|
|
43
|
+
`request:` keywords, or `nil` (the default), which resolves a `nil` scope.
|
|
44
|
+
A configured resolver runs exactly once per admitted request and returns
|
|
45
|
+
one opaque host scope object or `nil`. Raising resolvers fail closed with a
|
|
46
|
+
generic protocol internal error before registry or SDK work.
|
|
47
|
+
- `request_limit=` accepts a mapping with positive Integer `to` and positive
|
|
48
|
+
whole-second Integer or `ActiveSupport::Duration` `within`. It defaults to
|
|
49
|
+
120 requests per 60 seconds and applies one fixed window across discovery,
|
|
50
|
+
listing, and calls for the validated principal/client.
|
|
51
|
+
- `rate_limit_store=` accepts any `ActiveSupport::Cache` store responding to
|
|
52
|
+
`increment`, or `nil` to count through the application's own
|
|
53
|
+
`config.action_controller.cache_store`. Hitch adds no service to a deployment
|
|
54
|
+
that has none. `rate_limit_store` reads back the resolved store. Production
|
|
55
|
+
refuses a store that cannot count one principal's requests across the
|
|
56
|
+
processes serving them — `:memory_store`, `:null_store`, and `:file_store` —
|
|
57
|
+
and a raised runtime store error fails closed with HTTP 503. A store that
|
|
58
|
+
returns nil instead of raising (Redis and Solid Cache stores swallow backend
|
|
59
|
+
outages) admits without a limit, as Rails' own limiter does. Counting is
|
|
60
|
+
approximate at window boundaries under concurrency.
|
|
61
|
+
|
|
62
|
+
Host MCP policy and behavior receive a fresh public `Hitch::MCP::Context` for
|
|
63
|
+
every admitted request. It is a frozen envelope with readers for `principal`, `access_token`,
|
|
64
|
+
`scope`, `granted_scopes`, `client_id`, `resource`, `request_id`, `remote_ip`,
|
|
65
|
+
`user_agent`, `protocol_version`, and `meta`. Principal, token, and scope are
|
|
66
|
+
opaque request-local references rather than objects Hitch claims to freeze.
|
|
67
|
+
All scalar request values, granted scopes, and untrusted metadata are copied;
|
|
68
|
+
metadata is recursively frozen. Granted scopes are captured from the validated
|
|
69
|
+
token before the scope resolver runs, so scope resolution cannot rewrite the
|
|
70
|
+
current request's static authorization. Client metadata never supplies
|
|
71
|
+
authority.
|
|
72
|
+
|
|
73
|
+
`config.mcp.registry=` accepts one nonempty String constant name. The named
|
|
74
|
+
constant must resolve to a `Hitch::MCP::Registry` subclass, whose only admission
|
|
75
|
+
path is `register ToolClass, scopes: [...]`. Registration stores the tool class
|
|
76
|
+
name and copied static scopes, never the reloadable class object. A Rails
|
|
77
|
+
`to_prepare` callback clears the prior snapshot, reconstantizes and validates
|
|
78
|
+
the entire registry, and publishes one immutable MCP-name-sorted replacement.
|
|
79
|
+
Failure raises and leaves the registry unavailable rather than serving a stale
|
|
80
|
+
or partial snapshot.
|
|
81
|
+
|
|
82
|
+
An auth-only adopter that leaves `config.mcp.enabled` false continues to boot
|
|
83
|
+
unchanged; other MCP settings are inert without the switch. Setting
|
|
84
|
+
`config.mcp.enabled = true` activates the endpoint runtime and requires a
|
|
85
|
+
named `registry` at boot. Production additionally requires that the resolved
|
|
86
|
+
admission store can count across processes.
|
|
87
|
+
|
|
88
|
+
Registered classes subclass `Hitch::MCP::Tool` and declare `tool_name`,
|
|
89
|
+
`description`, `input_schema`, optional `output_schema`, and optional
|
|
90
|
+
`annotations`. Names use 1–64 ASCII letters, digits, underscores, dots, or
|
|
91
|
+
dashes. Schemas are JSON Schema 2020-12 objects with same-document references
|
|
92
|
+
only and fixed limits of 64 levels, 10,000 schema objects, and 1 MiB serialized
|
|
93
|
+
bytes. A literal top-level `server_context` in `properties` or `required` is
|
|
94
|
+
refused at boot; nested `server_context` data remains valid, and an actual
|
|
95
|
+
top-level value is rejected at the verified call boundary even when a pattern
|
|
96
|
+
or open schema would admit it. Tool annotations accept only `title` and the four MCP Boolean hint
|
|
97
|
+
fields. Registry validation also rejects unsupported or duplicate scopes,
|
|
98
|
+
duplicate MCP names, unnamed/missing/non-tool classes, and any subclass
|
|
99
|
+
replacement of framework-owned `.call`.
|
|
100
|
+
|
|
101
|
+
`.available_to?(context)` is the request-local, argument-free visibility and
|
|
102
|
+
call-admission gate. Its framework default is `false`; hosts must explicitly
|
|
103
|
+
return a Boolean from the current reloadable tool class. Availability runs
|
|
104
|
+
before static scope filtering for list and call. A raise or non-Boolean result
|
|
105
|
+
fails the whole request with generic `-32603` and no registry or scope detail.
|
|
106
|
+
|
|
107
|
+
For `tools/list` and `server/discover`, Hitch constructs the SDK server from
|
|
108
|
+
only registered tools that are available to the current context and whose
|
|
109
|
+
static OAuth scopes are all granted. The order is MCP-name ascending, with
|
|
110
|
+
`cacheScope: "private"`, `ttlMs: 0`, and `resultType: "complete"`. An unknown
|
|
111
|
+
or unavailable call receives the same `-32602` shape. Only a registered,
|
|
112
|
+
available tool may receive a 403 `insufficient_scope` challenge naming its
|
|
113
|
+
complete required static scope set.
|
|
114
|
+
|
|
115
|
+
After the SDK validates a call against the registered input schema, the final
|
|
116
|
+
framework-owned `.call` copies the arguments into one recursively frozen Hash
|
|
117
|
+
whose keys are Strings at every depth. `.authorize!(context, arguments:)` runs
|
|
118
|
+
first and defaults to raising `Hitch::MCP::Forbidden`; its return value is
|
|
119
|
+
ignored when it returns normally. `.perform(context, arguments:)` then receives
|
|
120
|
+
the same Context and arguments object. Forbidden, unexpected policy failures,
|
|
121
|
+
invalid non-JSON arguments, and host exceptions all return the same generic
|
|
122
|
+
tool error, with no host work after policy denial and no exception message on
|
|
123
|
+
the wire.
|
|
124
|
+
|
|
125
|
+
`.perform` must return one exact `Hitch::MCP::Result`; Hashes, models,
|
|
126
|
+
exceptions, SDK responses, and objects with serialization methods are rejected.
|
|
127
|
+
`Result.text(string)` returns text, `Result.structured(value, text: nil)` accepts
|
|
128
|
+
only recursively copied JSON values and requires the tool's registered output
|
|
129
|
+
schema, and `Result.error(public_message)` returns the one explicit
|
|
130
|
+
host-approved error message. Hitch validates structured output itself,
|
|
131
|
+
measures `JSON.generate` of the final Result with an inclusive
|
|
132
|
+
`max_result_bytes` boundary, and runs that as the one output-schema
|
|
133
|
+
validation — the SDK's own result validation stays explicitly off. Invalid constructors or returns, missing/mismatched schemas,
|
|
134
|
+
serialization failures, cap violations, and unexpected host errors all become
|
|
135
|
+
the same generic tool error. Hitch reports a synthetic exception through
|
|
136
|
+
`Rails.error` with only a fixed category, validated tool name, and, when request
|
|
137
|
+
observation is active, Hitch's server-generated correlation ID. A client-supplied
|
|
138
|
+
JSON-RPC ID is never logged. The original exception, arguments, result,
|
|
139
|
+
principal, token, scope, and client metadata are never forwarded.
|
|
140
|
+
|
|
141
|
+
`Hitch::MCP::Endpoint` is a public concern for a dedicated
|
|
142
|
+
`ActionController::API` controller:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class McpController < ActionController::API
|
|
146
|
+
include Hitch::MCP::Endpoint
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The host owns a catch-all route placed before the root engine mount:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
match "/mcp", to: "mcp#handle", via: :all
|
|
154
|
+
mount Hitch::Engine => "/"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The concern owns stateless final-profile POST and OPTIONS handling. Its fixed
|
|
158
|
+
order is raw Host, exact Origin, method, bearer authentication with active
|
|
159
|
+
canonical audience and client binding, then authenticated fixed-window
|
|
160
|
+
admission. It
|
|
161
|
+
requires the request path/query to equal the canonical resource, validates
|
|
162
|
+
Content-Type and both required Accept media types, reads at most the configured
|
|
163
|
+
byte cap, rejects duplicate JSON members at any depth, validates modern request
|
|
164
|
+
metadata and MCP headers, and dispatches one frozen request through one fresh
|
|
165
|
+
SDK server. Forwarded headers never derive the challenge or server identity.
|
|
166
|
+
|
|
167
|
+
The host-owned Registry is the packaged endpoint's only tool-admission path.
|
|
168
|
+
|
|
169
|
+
`request.hitch_mcp` emits exactly once for every non-OPTIONS endpoint request.
|
|
170
|
+
Its version-1 payload keys are `schema_version`, `request_id`, `method`,
|
|
171
|
+
`tool_name`, `principal_type`, `principal_key`, `client_key`, `http_status`,
|
|
172
|
+
`protocol_code`, `outcome`, `request_bytes`, `response_bytes`, and
|
|
173
|
+
`duration_ms`. The request ID is framework-generated; principal/client keys are
|
|
174
|
+
HMAC digests. Method and tool name are present only after their owning boundary
|
|
175
|
+
validates them. `outcome` is a fixed HTTP/protocol category such as `complete`,
|
|
176
|
+
`unauthorized`, `rate_limited`, `invalid_params`, or `internal_error`.
|
|
177
|
+
|
|
178
|
+
`invocation.hitch_mcp` emits only when SDK schema validation and Hitch argument
|
|
179
|
+
normalization reach a registered available Tool. Its version-1 keys are
|
|
180
|
+
`schema_version`, `request_id`, `tool_name`, `availability`, `argument_policy`,
|
|
181
|
+
`executed`, `result_category`, and `duration_ms`. Availability is `available`;
|
|
182
|
+
argument policy is `allowed`, `denied`, or `failed`; result category is
|
|
183
|
+
`success`, `explicit_error`, or `generic_error`. The request IDs correlate the
|
|
184
|
+
two events. Neither event contains credentials, bodies, arguments, results,
|
|
185
|
+
metadata, exception messages, or backtraces. Subscriber exceptions produce
|
|
186
|
+
only a sanitized `Rails.error` report and do not alter the MCP response.
|
|
187
|
+
|
|
188
|
+
`hitch:install` is public operational API. One run creates
|
|
189
|
+
`config/initializers/hitch.rb`, a host-owned `ActionController::API` endpoint
|
|
190
|
+
controller, the empty `McpToolRegistry` at `app/tools/mcp_tool_registry.rb`,
|
|
191
|
+
and one routes insertion placing the `/mcp` route immediately before the
|
|
192
|
+
`Hitch::Engine` mount. The `--controller-name` option accepts an explicit
|
|
193
|
+
constant path ending in `Controller`. It refuses the entire install before
|
|
194
|
+
writing when the controller name is invalid or an owned file or constant
|
|
195
|
+
collides; an existing engine mount or host-owned `/mcp` route is skipped or
|
|
196
|
+
warned about rather than duplicated. `bin/rails destroy hitch:install`
|
|
197
|
+
reverses the install through the generator's revoke behavior — removing the
|
|
198
|
+
generated files and the `/mcp` route it always writes, while the engine
|
|
199
|
+
mount is deliberately left in place (with a status note) because a generated
|
|
200
|
+
mount and a pre-existing host-owned one are indistinguishable without an
|
|
201
|
+
install record. Beyond that, the host's version control is the rollback
|
|
202
|
+
story.
|
|
203
|
+
|
|
204
|
+
`hitch:tool NAME` is public operational API. NAME accepts valid snake-case,
|
|
205
|
+
kebab-case, constant, and nested `/` or `::` segments. The default root
|
|
206
|
+
namespace is `McpTools`; `--namespace Admin::McpTools` selects another valid
|
|
207
|
+
constant path. Input segments normalize to one deterministic Ruby class, an
|
|
208
|
+
`app/tools/` path, a Minitest integration-test path, and a 1–64 character
|
|
209
|
+
dotted MCP name. A normalized file, class, or test class collision — or a
|
|
210
|
+
missing Registry file — refuses the whole operation before writing.
|
|
211
|
+
|
|
212
|
+
The generated Tool declares a closed empty input schema, all four MCP hint
|
|
213
|
+
annotations, `.available_to? == true`, an `.authorize!` whose empty body
|
|
214
|
+
allows the call and says so, and a `.perform` that returns
|
|
215
|
+
`Hitch::MCP::Result.text`. The generator registers it in
|
|
216
|
+
`McpToolRegistry` with `scopes: [ "mcp" ]`, and the generated integration
|
|
217
|
+
test proves the tool is listed and responds through `post_mcp` over real
|
|
218
|
+
HTTP. `--deny-default` generates the hardened variant instead —
|
|
219
|
+
`.available_to? == false`, a raising `.authorize!`, a raising `.perform` —
|
|
220
|
+
still registered, with a test asserting it stays hidden. `bin/rails destroy
|
|
221
|
+
hitch:tool NAME` removes the generated files and the injected registration
|
|
222
|
+
line; if the registration line was edited or reformatted it refuses the
|
|
223
|
+
whole rollback before deleting anything, so a stranded registration can
|
|
224
|
+
never break the next boot.
|
|
225
|
+
|
|
226
|
+
`Hitch::MCP::TestHelper` is public test-only API loaded with
|
|
227
|
+
`require "hitch/mcp/test_helper"`. `mcp_headers(token:, method:, name: nil,
|
|
228
|
+
protocol_version: "2026-07-28")` returns exact Host, bearer, JSON, Accept,
|
|
229
|
+
protocol, method, and conditional tool-name headers for the current configured
|
|
230
|
+
resource. `post_mcp(method:, token:, params: {}, id: "hitch-test",
|
|
231
|
+
client_info: nil, capabilities: {}, protocol_version: "2026-07-28")` copies and
|
|
232
|
+
validates JSON test inputs, adds the final MCP metadata, posts one JSON-RPC 2.0
|
|
233
|
+
request to the current resource path/query, and returns the ordinary Rails
|
|
234
|
+
integration response. Invalid helper inputs raise `ArgumentError`; endpoint
|
|
235
|
+
HTTP/protocol failures are not reclassified. Neither helper logs or places the
|
|
236
|
+
bearer value in the request body. `mint_mcp_token(principal:, scopes:
|
|
237
|
+
Hitch.configuration.supported_scopes, client_id: "hitch-test-client")` mints
|
|
238
|
+
a real bearer token through the production authorization-code path
|
|
239
|
+
(`create_authorization!` plus PKCE exchange) for any persisted principal
|
|
240
|
+
record and returns the raw token `post_mcp` expects. Scopes outside
|
|
241
|
+
`supported_scopes` raise `ArgumentError` rather than minting a grant the
|
|
242
|
+
real flow could never issue.
|
|
243
|
+
|
|
244
|
+
`bin/rails hitch:doctor` is public operational API. Its default human
|
|
245
|
+
report and `HITCH_DOCTOR_FORMAT=json` machine report contain exactly these
|
|
246
|
+
ordered IDs: `versions`, `configuration`, `resource_discovery`, `route_order`,
|
|
247
|
+
`migrations`, `registry`, `hosts`, `origins`, and `rate_limit_store`.
|
|
248
|
+
Every check names something the host can act on;
|
|
249
|
+
gem-self-diagnosis (package integrity) lives in the repository's CI. The
|
|
250
|
+
`versions` bounds come from the loaded gemspec. JSON uses schema
|
|
251
|
+
`hitch.doctor.v1`; every check has `status`, stable `code`, `summary`, and
|
|
252
|
+
bounded structural `details`.
|
|
253
|
+
|
|
254
|
+
Statuses are `pass`, `warn`, `fail`, or `skip`. The task exits one only when at
|
|
255
|
+
least one check fails, and prints every check before exiting. It performs
|
|
256
|
+
internal read-only discovery requests and schema/route/Registry/package
|
|
257
|
+
inspection. Its admission-store diagnostic increments a random
|
|
258
|
+
`hitch:doctor:v1:*` key twice with a five-second expiry, asserts the counts are
|
|
259
|
+
`1` then `2`, removes the key, and never uses application quota keys. Output
|
|
260
|
+
omits exception messages, credentials, bodies, store credentials, and probe
|
|
261
|
+
keys. There is no fix or repair
|
|
262
|
+
mode. The exact operator contract is in `docs/operator/doctor.md`.
|
|
263
|
+
|
|
264
|
+
`bin/rails hitch:tokens:issue PRINCIPAL=Model:id` issues a long-lived access
|
|
265
|
+
token for a headless agent, and `Hitch::AccessToken.issue!(principal:,
|
|
266
|
+
client_id:, client_name:, scopes:, expires_in:)` is the same call from a
|
|
267
|
+
console. Both run the ordinary authorization-code exchange, so the row is an
|
|
268
|
+
ordinary token: revocable, audience-bound, digest-at-rest. The task defaults
|
|
269
|
+
to 90 days (capped at 3650 days), the first configured scope, and the
|
|
270
|
+
`client_id` `hitch-cli`; `issue!` takes `expires_in` in seconds and otherwise
|
|
271
|
+
applies `access_token_lifetime_seconds`. Disclosure writes once to a new
|
|
272
|
+
`0600` file or an attached terminal, never to stdout.
|
|
273
|
+
|
|
274
|
+
`Hitch::MCP::Protocol` is framework-owned: it names the protocol version and
|
|
275
|
+
method set for Hitch's own use, and may change with the profile Hitch
|
|
276
|
+
implements. Host test suites pin `Hitch::MCP::TestHelper::PROTOCOL_VERSION`
|
|
277
|
+
instead.
|
|
278
|
+
|
|
279
|
+
The operator tasks `hitch:doctor`, `hitch:cimd:check`,
|
|
280
|
+
`hitch:clients:create_confidential`, `hitch:clients:rotate_secret`, and
|
|
281
|
+
`Hitch::AccessToken.cleanup_expired!` are public and keep their names and
|
|
282
|
+
arguments within 0.2.x.
|
|
283
|
+
|
|
284
|
+
The gem directly depends on `mcp >= 1.2, < 2` and `json_schemer >= 2.4, < 3`.
|
|
285
|
+
It declares no Redis dependency. Hitch's SDK adapter, verified
|
|
286
|
+
request, runtime registry wrapper, and version-scoped response normalizer are private
|
|
287
|
+
implementation details, not public constants or extension points. Context is
|
|
288
|
+
the public exception; its construction remains framework-owned.
|
|
289
|
+
|
|
290
|
+
`tools/call.params.arguments` may be omitted, as allowed by the protocol. When
|
|
291
|
+
present it must be an object. Unsupported protocol versions keep Hitch's frozen
|
|
292
|
+
`error.data.supportedVersions` field and additionally expose the official
|
|
293
|
+
runner's `supported` list plus the echoed `requested` value.
|
|
294
|
+
|
|
295
|
+
The conformance harness is test infrastructure, not runtime API. Its five
|
|
296
|
+
fixture tools, separate runner-only missing-capability diagnostic, private
|
|
297
|
+
bearer-file bridge, npm lock, and checked patch are never packaged. The gate
|
|
298
|
+
runs only `server-stateless`, `http-header-validation`,
|
|
299
|
+
`dns-rebinding-protection`, `json-schema-2020-12`, `tools-list`,
|
|
300
|
+
`tools-call-simple-text`, and `tools-call-error`. It does not claim prompts,
|
|
301
|
+
resources, caching, subscriptions, or all-server-suite conformance.
|
|
302
|
+
|
|
303
|
+
The package/client harness is also repository test infrastructure, not
|
|
304
|
+
runtime API. `bin/package-smoke` installs the exact built gem in a disposable
|
|
305
|
+
Rails application and drives the full flow. `bin/package-smoke
|
|
306
|
+
--automated-clients` adds one ephemeral Redis service, exercising
|
|
307
|
+
`RedisCacheStore` as one supported admission backend, and the exhaustive
|
|
308
|
+
official TypeScript/Python SDK 2.0.0 public/confidential matrix. The
|
|
309
|
+
fixtures drive consent, PKCE/state/issuer validation, base-scope discovery,
|
|
310
|
+
listing and calls, confidential-secret rotation, and scope step-up through
|
|
311
|
+
documented host surfaces. Fixture inputs are private mode-0600 files; evidence
|
|
312
|
+
contains no credentials. Product/model clients remain outside that automated
|
|
313
|
+
claim and require separate approval.
|
|
314
|
+
|
|
315
|
+
## Distribution status
|
|
316
|
+
|
|
317
|
+
All `0.2.0.pre.1` through `0.2.0.pre.4` artifacts are internal checkpoints.
|
|
318
|
+
`0.2.0.pre.4` was the first public-eligible prerelease, and publication was
|
|
319
|
+
deferred so final `0.2.0` can become the first public RubyGems release. No
|
|
320
|
+
pre.4 tag, GitHub release, RubyGems publication, or downloaded-gem evidence
|
|
321
|
+
exists. Every future tag, GitHub release, or RubyGems publication still
|
|
322
|
+
requires an explicit maintainer decision after its milestone gates pass.
|
data/docs/removing.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Removing Hitch from a Rails application
|
|
2
|
+
|
|
3
|
+
Removal is intentionally non-destructive. Unmounting the engine and removing
|
|
4
|
+
the gem must not silently delete OAuth audit data or principal bindings.
|
|
5
|
+
|
|
6
|
+
1. Disable new authorization and registration traffic at ingress. Set DCR off
|
|
7
|
+
before the final deploy.
|
|
8
|
+
2. Revoke active Hitch tokens, or wait for their configured expiry if the MCP
|
|
9
|
+
endpoint has already been disabled.
|
|
10
|
+
3. Stop jobs and schedules that call `Hitch::AccessToken.cleanup_expired!` or
|
|
11
|
+
Hitch rake tasks.
|
|
12
|
+
4. Back up the `hitch_*` tables and decide their retention period under the
|
|
13
|
+
application's audit and privacy policy.
|
|
14
|
+
5. Remove generated tools with
|
|
15
|
+
`bin/rails destroy hitch:tool NAME [--namespace Namespace]` while the gem
|
|
16
|
+
is installed — each removes its files and its registration line, refusing
|
|
17
|
+
the rollback if the registration line was edited — then run
|
|
18
|
+
`bin/rails destroy hitch:install` to remove the initializer, endpoint
|
|
19
|
+
controller, registry, and the `/mcp` route. Review anything you customized
|
|
20
|
+
in your version control history first; destroy reverses what the
|
|
21
|
+
generators wrote.
|
|
22
|
+
6. Remove the `mount Hitch::Engine` line — destroy leaves it in place, since
|
|
23
|
+
it cannot tell a generated mount from one that pre-dates the install —
|
|
24
|
+
and any host controller inclusions of `Hitch::CorsSupport`.
|
|
25
|
+
7. Remove the Gemfile entry and bundle again.
|
|
26
|
+
|
|
27
|
+
`hitch:doctor` has no repair or uninstall mode. It is safe to use as a final
|
|
28
|
+
read-only inventory before step 5, but its findings do not authorize deletion.
|
|
29
|
+
The isolated diagnostic key is removed by the probe and has a five-second
|
|
30
|
+
expiry; Hitch's ordinary rate-limit keys expire under their configured windows
|
|
31
|
+
and require no uninstall sweep.
|
|
32
|
+
|
|
33
|
+
Hitch does not ship a table-dropping removal migration. Keep the tables while
|
|
34
|
+
old deployments, queued jobs, logs, or audit workflows may still refer to
|
|
35
|
+
their IDs. If the application's owner later chooses deletion, create a host
|
|
36
|
+
migration that names each `hitch_*` table explicitly, take a final backup, and
|
|
37
|
+
review that irreversible operation independently. Do not reuse those table
|
|
38
|
+
names for unrelated data.
|
|
39
|
+
|
|
40
|
+
If the application may reinstall Hitch, retaining the tables and migration
|
|
41
|
+
history is safer than dropping them. Reinstall the same or a forward-compatible
|
|
42
|
+
version and run `bin/rails db:migrate`; never restore only part of the Hitch
|
|
43
|
+
schema.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hitch
|
|
4
|
+
module Generators
|
|
5
|
+
# Pre-flight guards shared by Hitch generators: check the destination
|
|
6
|
+
# before writing so a refused run leaves no partial install behind.
|
|
7
|
+
# Including generators define +refusal_subject+.
|
|
8
|
+
module GeneratorGuards
|
|
9
|
+
REGISTRY_PATH = "app/tools/mcp_tool_registry.rb"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def constant_collision?(name)
|
|
14
|
+
return false unless Object.const_defined?(name, false)
|
|
15
|
+
return true if File.expand_path(destination_root) == File.expand_path(Rails.root)
|
|
16
|
+
|
|
17
|
+
source = Object.const_source_location(name, false)&.first
|
|
18
|
+
source && File.expand_path(source).start_with?("#{File.expand_path(destination_root)}/")
|
|
19
|
+
rescue NameError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destination_file?(relative_path)
|
|
24
|
+
File.file?(destination_path(relative_path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def destination_path(relative_path)
|
|
28
|
+
File.expand_path(relative_path, destination_root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def refuse!(operation, errors)
|
|
32
|
+
raise ::Thor::Error, "#{refusal_subject} #{operation} refused:\n- #{errors.join("\n- ")}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
require_relative "../generator_guards"
|
|
6
|
+
|
|
7
|
+
module Hitch
|
|
8
|
+
module Generators
|
|
9
|
+
# One install: the initializer, the /mcp endpoint controller, an empty
|
|
10
|
+
# explicit tool registry, and the routes, in order. Migrations are
|
|
11
|
+
# auto-appended by the engine; run db:migrate after install.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bin/rails generate hitch:install
|
|
15
|
+
# bin/rails destroy hitch:install
|
|
16
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
17
|
+
include Hitch::Generators::GeneratorGuards
|
|
18
|
+
|
|
19
|
+
# Declared explicitly so the command is always `hitch:install`,
|
|
20
|
+
# independent of generator-discovery ordering.
|
|
21
|
+
namespace "hitch:install"
|
|
22
|
+
|
|
23
|
+
source_root File.expand_path("templates", __dir__)
|
|
24
|
+
|
|
25
|
+
class_option :controller_name,
|
|
26
|
+
type: :string,
|
|
27
|
+
default: "McpController",
|
|
28
|
+
desc: "Host controller constant to create (must end in Controller)"
|
|
29
|
+
|
|
30
|
+
INITIALIZER_PATH = "config/initializers/hitch.rb"
|
|
31
|
+
ROUTES_PATH = "config/routes.rb"
|
|
32
|
+
CONTROLLER_PATTERN = /\A[A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*Controller\z/
|
|
33
|
+
MCP_ROUTE_PATTERN = /["']\/?mcp\/?["']/
|
|
34
|
+
|
|
35
|
+
def self.exit_on_failure?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def install_or_revoke
|
|
40
|
+
prepare_identity!
|
|
41
|
+
|
|
42
|
+
if behavior == :revoke
|
|
43
|
+
remove_files
|
|
44
|
+
remove_routes
|
|
45
|
+
print_removal_steps
|
|
46
|
+
else
|
|
47
|
+
preflight!
|
|
48
|
+
create_files
|
|
49
|
+
add_routes
|
|
50
|
+
print_next_steps
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def prepare_identity!
|
|
57
|
+
controller_name = options.fetch("controller_name")
|
|
58
|
+
unless CONTROLLER_PATTERN.match?(controller_name)
|
|
59
|
+
refuse!(operation, [ "controller name must be a constant path ending in Controller" ])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@controller_name = controller_name
|
|
63
|
+
@controller_modules = controller_name.split("::")[0...-1]
|
|
64
|
+
@controller_leaf = controller_name.split("::").last
|
|
65
|
+
@controller_path = "app/controllers/#{controller_name.underscore}.rb"
|
|
66
|
+
@route_target = controller_name.delete_suffix("Controller").underscore
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def preflight!
|
|
70
|
+
errors = []
|
|
71
|
+
[ INITIALIZER_PATH, @controller_path, REGISTRY_PATH ].each do |path|
|
|
72
|
+
errors << "file collision: #{path}" if destination_file?(path)
|
|
73
|
+
end
|
|
74
|
+
errors << "constant collision: #{@controller_name}" if constant_collision?(@controller_name)
|
|
75
|
+
errors << "constant collision: McpToolRegistry" if constant_collision?("McpToolRegistry")
|
|
76
|
+
errors << "#{ROUTES_PATH} is missing a Rails.application.routes.draw block" unless
|
|
77
|
+
active_routes.include?(".routes.draw do")
|
|
78
|
+
refuse!("install", errors) if errors.any?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Behavior-aware Thor actions: create in invoke, remove in revoke.
|
|
82
|
+
def create_files
|
|
83
|
+
template "initializer.rb", INITIALIZER_PATH
|
|
84
|
+
template "controller.rb.tt", @controller_path
|
|
85
|
+
template "registry.rb", REGISTRY_PATH
|
|
86
|
+
end
|
|
87
|
+
alias_method :remove_files, :create_files
|
|
88
|
+
|
|
89
|
+
# The /mcp route must precede the engine mount, so both go into one
|
|
90
|
+
# insertion at the top of the draw block.
|
|
91
|
+
def add_routes
|
|
92
|
+
active = active_routes
|
|
93
|
+
match_missing = !MCP_ROUTE_PATTERN.match?(active)
|
|
94
|
+
mount_missing = !active.include?("mount Hitch::Engine")
|
|
95
|
+
|
|
96
|
+
say_status :warn, "#{ROUTES_PATH} already routes /mcp; leaving it as is", :yellow unless match_missing
|
|
97
|
+
say_status :skip, "Hitch::Engine is already mounted", :yellow unless mount_missing
|
|
98
|
+
return unless match_missing || mount_missing
|
|
99
|
+
|
|
100
|
+
route(
|
|
101
|
+
[
|
|
102
|
+
(match_line if match_missing),
|
|
103
|
+
(mount_line if mount_missing)
|
|
104
|
+
].compact.join("\n")
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Removes only the line this generator always writes. With no install
|
|
109
|
+
# record there is no way to tell a generated engine mount from one the
|
|
110
|
+
# host already had, and deleting a host-owned mount would take down its
|
|
111
|
+
# /oauth/* and discovery routes — so the mount is left in place and
|
|
112
|
+
# said so.
|
|
113
|
+
def remove_routes
|
|
114
|
+
active = active_routes
|
|
115
|
+
route(match_line) if active.include?(match_line)
|
|
116
|
+
return unless active.include?("mount Hitch::Engine")
|
|
117
|
+
|
|
118
|
+
say_status :skip,
|
|
119
|
+
"mount Hitch::Engine left in place (it may pre-date this install); remove it manually if unwanted",
|
|
120
|
+
:yellow
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def match_line
|
|
124
|
+
%(match "/mcp", to: "#{@route_target}#handle", via: :all)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def mount_line
|
|
128
|
+
'mount Hitch::Engine => "/"'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def active_routes
|
|
132
|
+
return "" unless destination_file?(ROUTES_PATH)
|
|
133
|
+
|
|
134
|
+
File.binread(destination_path(ROUTES_PATH))
|
|
135
|
+
.each_line.reject { |line| line.lstrip.start_with?("#") }.join
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def operation
|
|
139
|
+
behavior == :revoke ? "removal" : "install"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def refusal_subject = "Hitch"
|
|
143
|
+
|
|
144
|
+
# Removing the initializer leaves the engine's boot validation with no
|
|
145
|
+
# resource_uri, so the app will not start until the gem is gone too.
|
|
146
|
+
# Saying so here is the difference between a clean uninstall and an
|
|
147
|
+
# application that suddenly refuses to boot.
|
|
148
|
+
def print_removal_steps
|
|
149
|
+
say ""
|
|
150
|
+
say "hitch-rails files removed.", :green
|
|
151
|
+
say "The application will not boot until you also:"
|
|
152
|
+
say " 1. Remove the hitch-rails line from your Gemfile, then: bundle install"
|
|
153
|
+
say " 2. Roll back Hitch's tables if you no longer want them"
|
|
154
|
+
say " 3. Delete any tools you generated under app/tools/"
|
|
155
|
+
say "See docs/removing.md for the full order."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def print_next_steps
|
|
159
|
+
say ""
|
|
160
|
+
say "hitch-rails installed.", :green
|
|
161
|
+
say "Next steps:"
|
|
162
|
+
say " 1. Set resource_uri and brand_name in #{INITIALIZER_PATH}"
|
|
163
|
+
say " 2. Run: bin/rails db:migrate"
|
|
164
|
+
say " 3. Generate your first tool: bin/rails generate hitch:tool echo"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|