hecks 2.6.0 → 2.8.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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. data/lib/hecks/query_ir.rb +0 -482
@@ -0,0 +1,32 @@
1
+ require_relative "../mcp_stdio_guard"
2
+
3
+ module Hecks
4
+ module CLI
5
+ # The command behind `bin/hecks_mcp_door` and `hecks mcp`: the MCP door onto the
6
+ # storehouse bus, over stdio only.
7
+ #
8
+ # This file loads nothing but `McpStdioGuard`, so the transport check runs before
9
+ # the framework does. The door itself is `McpDoor`, required only once the check
10
+ # passes.
11
+ module Mcp
12
+ # The server name the guard's refusals and warnings carry.
13
+ SERVER = "hecks-mcp-door".freeze
14
+
15
+ module_function
16
+
17
+ # Refuses any transport but stdio, then serves JSON-RPC requests from stdin
18
+ # until it closes.
19
+ #
20
+ # @param argv [Array<String>] the arguments after the command; only `--stdio`
21
+ # is accepted
22
+ # @return [void]
23
+ # @raise [SystemExit] with `McpStdioGuard::EXIT_STATUS` when the process is not
24
+ # set up for stdio
25
+ def call(argv)
26
+ McpStdioGuard.enforce_stdio!(server: SERVER, argv: argv)
27
+ require_relative "mcp_door"
28
+ McpDoor.serve
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,461 @@
1
+ require "json"
2
+ require_relative "../../hecks"
3
+ require_relative "mcp"
4
+
5
+ module Hecks
6
+ module CLI
7
+ # The MCP door onto the storehouse bus: one MCP server for every booted domain,
8
+ # not one per command, hand-rolled JSON-RPC over stdio (newline-delimited JSON,
9
+ # flushed after every write, no gem dependency).
10
+ #
11
+ # ## Plumbing only
12
+ #
13
+ # All the logic and the JSON shape it answers with live in `Storehouse`, the bus
14
+ # itself, transport-blind. A door of another shape (a plain CLI, an HTTP door)
15
+ # would sit beside this one, calling `Storehouse` the same way and sharing its
16
+ # audit log and caller-identity handling.
17
+ #
18
+ # ## Identity is asserted, not verified
19
+ #
20
+ # `dispatch` and `query` take `role:` and `actor_id:`, bound for the call through
21
+ # `Hecks.as_caller`, so a role-gated command is checked against them rather than
22
+ # merely documented by `describe`; `dispatch` refuses a role-declaring command
23
+ # called without one. Nothing here verifies the claim beyond the string (or, with
24
+ # Governance attached, a `Governance::RoleAssignment` lookup). The readers take
25
+ # no role, and `query` gates on none.
26
+ #
27
+ # ## Stdio only, enforced
28
+ #
29
+ # The door has no authentication. `Mcp.call` runs
30
+ # `McpStdioGuard.enforce_stdio!` before this file loads, and `serve` prints the
31
+ # warning to stderr, never stdout, which carries the protocol. `domain:` is
32
+ # confined to `Storehouse::BOOT_ROOT` (`confine!`), but a domain within that
33
+ # root still boots real Ruby, so this is a code execution surface for anyone who
34
+ # can write to stdin. A network transport needs authentication first; see
35
+ # `docs/decisions/0062-mcp-servers-need-real-authentication-before-any-network-transport.md`.
36
+ #
37
+ # ## Reader mode
38
+ #
39
+ # Whoever spawns the door can narrow it with `HECKS_DOOR_TOOLS=readers` and
40
+ # `HECKS_DOOR_DOMAINS=<dir>[:<dir>...]` (`Hecks::McpDoorScope`, ADR 0072 decision 2).
41
+ # A reader door serves only `McpDoorScope::READER_TOOLS` (`query`, `events`,
42
+ # `state`, `catalog`, `describe`, `validate`, `domains`, `history`, `follow`), lists
43
+ # only those in `tools/list`, and refuses `dispatch` (so `dry_run` and `steps` too),
44
+ # `behaviors` and any other name with an answer that names the mode. Its `domain:`
45
+ # must resolve to one of the named directories, checked before anything boots, so no
46
+ # other domain's Ruby is loaded. The door keeps no domain loaded between calls, so
47
+ # the named list is the whole allowed set. This limits what one spawned agent can
48
+ # reach; it identifies no one and is not authentication. The names sit outside
49
+ # `HECKS_MCP_*` because the stdio guard refuses every name in that prefix but the
50
+ # transport, and that refusal belongs to ADR 0062. With neither variable set, the
51
+ # door serves every tool as described above.
52
+ #
53
+ # ## One boot per call
54
+ #
55
+ # `Hecks.boot(domain, install_facade: false)` is cheap, and it lets one server
56
+ # answer calls against two domains back to back with no shared, staling state.
57
+ module McpDoor
58
+ PROTOCOL_VERSION = "2024-11-05".freeze
59
+
60
+ SUMMARY_PROPERTY = {
61
+ type: "string",
62
+ description: "One line: why this call is happening. Required — it is what makes the audit trail legible later."
63
+ }.freeze
64
+
65
+ DOMAIN_PROPERTY = {
66
+ type: "string",
67
+ description: "The domain directory, containing a .hecksagon (e.g. \"examples/banking\"). The `domains` tool " \
68
+ "lists what's available."
69
+ }.freeze
70
+
71
+ ARGS_PROPERTY = {
72
+ type: "object",
73
+ description: "The command/query's declared arguments, nested value-object literals included " \
74
+ "(e.g. {\"reference\":{\"value\":\"BUG#1\"}})."
75
+ }.freeze
76
+
77
+ SOURCE_PROPERTY = {
78
+ type: "string",
79
+ enum: Hecks::Storehouse::SOURCE_TAGS,
80
+ description: "Optional: who is calling, for the audit log `follow` reads back."
81
+ }.freeze
82
+
83
+ ROLE_PROPERTY = {
84
+ type: "string",
85
+ description: "Bind a caller identity for the call's duration, so a role-gated command's authorization is " \
86
+ "checked rather than merely documented. Required by `dispatch` for any command that declares a " \
87
+ "role — omitting it refuses the call rather than running it unchecked. Also required if " \
88
+ "`actor_id` is given."
89
+ }.freeze
90
+
91
+ ACTOR_ID_PROPERTY = {
92
+ type: "string",
93
+ description: "Optional, alongside `role`: WHO holds that role — once the domain attaches Governance, checked " \
94
+ "against a real Governance::RoleAssignment instead of a bare string match. Requires `role`."
95
+ }.freeze
96
+
97
+ TOOLS = [
98
+ {
99
+ name: "dispatch",
100
+ description: "Issue a command against a booted domain — or, with `steps`, several in one call. `command` is " \
101
+ "the short or qualified verb name (the catalog/describe tools show what's available). Set " \
102
+ "`dry_run: true` to check whether it WOULD succeed without committing anything. Answers the " \
103
+ "record's id, its resulting state, and the events it announced — or a structured refusal, " \
104
+ "never a crash.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ domain: DOMAIN_PROPERTY,
109
+ command: { type: "string", description: "The command's short or qualified name, e.g. \"open_account\". " \
110
+ "Ignored when `steps` is given." },
111
+ args: ARGS_PROPERTY,
112
+ summary: SUMMARY_PROPERTY,
113
+ source: SOURCE_PROPERTY,
114
+ role: ROLE_PROPERTY,
115
+ actor_id: ACTOR_ID_PROPERTY,
116
+ dry_run: { type: "boolean", description: "Preview only — check the command would succeed, commit nothing. " \
117
+ "Ignored when `steps` is given." },
118
+ steps: {
119
+ type: "array",
120
+ description: "Run several commands in one call, in order. Each item: {command, args}. When given, " \
121
+ "`command`/`args`/`dry_run` above are ignored.",
122
+ items: {
123
+ type: "object",
124
+ properties: { command: { type: "string" }, args: ARGS_PROPERTY },
125
+ required: %w[command]
126
+ }
127
+ }
128
+ },
129
+ required: %w[domain summary]
130
+ }
131
+ },
132
+ {
133
+ name: "query",
134
+ description: "Ask a declared question of a booted domain. `question` is the short or qualified query name. " \
135
+ "Answers the row array the query itself declares.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ domain: DOMAIN_PROPERTY,
140
+ question: { type: "string", description: "The query's short or qualified name." },
141
+ args: ARGS_PROPERTY,
142
+ summary: SUMMARY_PROPERTY,
143
+ source: SOURCE_PROPERTY,
144
+ role: ROLE_PROPERTY,
145
+ actor_id: ACTOR_ID_PROPERTY
146
+ },
147
+ required: %w[domain question summary]
148
+ }
149
+ },
150
+ {
151
+ name: "events",
152
+ description: "What actually HAPPENED, with payloads, to one aggregate/record — events THIS DOOR witnessed " \
153
+ "(every real dispatch ever routed through it, sourced from its own audit log — not a full " \
154
+ "domain-wide event-sourcing replay). Distinct from `state` (what's stored now) and `history` " \
155
+ "(append-only operation snapshots, no payload). Narrow with `aggregate` and (requires " \
156
+ "`aggregate`) `id`; `limit` keeps only the most recent.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ domain: DOMAIN_PROPERTY,
161
+ aggregate: { type: "string", description: "Narrow to one aggregate, e.g. \"Account\"." },
162
+ id: { type: "string", description: "Narrow to one record's own events. Requires `aggregate`." },
163
+ limit: { type: "integer", description: "Keep only the most recent N events. Omit for all of them." }
164
+ },
165
+ required: %w[domain]
166
+ }
167
+ },
168
+ {
169
+ name: "state",
170
+ description: "Read what an aggregate actually has stored, no verb involved. Pass `id` for one record; " \
171
+ "omit it to list every record the aggregate currently holds.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ domain: DOMAIN_PROPERTY,
176
+ aggregate: { type: "string", description: "The aggregate's name, e.g. \"Account\"." },
177
+ id: { type: "string", description: "A specific record's id. Omit to list every record." },
178
+ summary: SUMMARY_PROPERTY
179
+ },
180
+ required: %w[domain aggregate summary]
181
+ }
182
+ },
183
+ {
184
+ name: "catalog",
185
+ description: "Zoom level one: every aggregate a domain declares, and every command/query name each " \
186
+ "answers to. No summary needed — this changes nothing and commits nothing to any log.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: { domain: DOMAIN_PROPERTY },
190
+ required: %w[domain]
191
+ }
192
+ },
193
+ {
194
+ name: "describe",
195
+ description: "Zoom level two: one aggregate's full contract — every command's arguments, the states it " \
196
+ "may be issued from, and every way it can refuse. Omit `aggregate` for the whole domain.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ domain: DOMAIN_PROPERTY,
201
+ aggregate: { type: "string", description: "Narrow to one aggregate. Omit for the whole domain." }
202
+ },
203
+ required: %w[domain]
204
+ }
205
+ },
206
+ {
207
+ name: "validate",
208
+ description: "Zoom level three: is this domain's wiring sound — every bind names a declared aggregate, " \
209
+ "every adapter satisfies its port, the default adapter is usable. {valid:false, error:…} " \
210
+ "on a real wiring defect, never a bare crash. Set `deep: true` to also model-check the IR " \
211
+ "for dead lifecycle transitions, unreachable saga states, and dispatches to nowhere.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ domain: DOMAIN_PROPERTY,
216
+ deep: { type: "boolean", description: "Also run the static model checker (lifecycle/saga/policy IR)." }
217
+ },
218
+ required: %w[domain]
219
+ }
220
+ },
221
+ {
222
+ name: "domains",
223
+ description: "Zoom level zero: every domain directory found under a root (default \"examples\"), so a " \
224
+ "caller who doesn't already know a path can find one.",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: { under: { type: "string", description: "Root directory to scan. Defaults to \"examples\"." } }
228
+ }
229
+ },
230
+ {
231
+ name: "history",
232
+ description: "The full write history for every aggregate in a domain — every operation an append-only " \
233
+ "adapter ever recorded, not just current state. An aggregate on a non-append-only adapter " \
234
+ "answers an empty history honestly.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: { domain: DOMAIN_PROPERTY },
238
+ required: %w[domain]
239
+ }
240
+ },
241
+ {
242
+ name: "behaviors",
243
+ description: "Run a domain's hand-curated `.behaviors` example tests and report pass/fail/error per test. " \
244
+ "`target` is a single `.behaviors` file or a directory to sweep.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: { target: { type: "string", description: "A .behaviors file, or a directory to sweep." } },
248
+ required: %w[target]
249
+ }
250
+ },
251
+ {
252
+ name: "follow",
253
+ description: "Tail this door's own audit log for a domain — the last `limit` dispatch/query/state/dry-run " \
254
+ "calls made through it, each carrying its summary, source, and outcome.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ domain: DOMAIN_PROPERTY,
259
+ limit: { type: "integer", description: "How many recent entries to return. Defaults to 20." }
260
+ },
261
+ required: %w[domain]
262
+ }
263
+ }
264
+ ].freeze
265
+
266
+ NOTES = [
267
+ "Identity is self-asserted: role:/actor_id: are claims made by the caller and are not verified.",
268
+ "state, events, history, follow, describe, catalog and query run with no role check at all.",
269
+ "domain: boots real Ruby under #{Storehouse::BOOT_ROOT}; anyone who can write to stdin can run it."
270
+ ].freeze
271
+
272
+ module_function
273
+
274
+ # The scope this door was spawned with: every tool and any domain, or a reader
275
+ # door narrowed by `HECKS_DOOR_TOOLS`/`HECKS_DOOR_DOMAINS`. Read once, on first use,
276
+ # so a bad setting refuses to start the door before anything is written to stdout.
277
+ #
278
+ # @return [McpDoorScope]
279
+ def scope
280
+ @scope ||= McpDoorScope.start!(server: Mcp::SERVER)
281
+ end
282
+
283
+ # The notes the stdio warning carries: the standing ones, then what this door's
284
+ # scope says about `domain:`.
285
+ #
286
+ # @return [Array<String>]
287
+ def warning_notes
288
+ NOTES[0, 2] + (scope.restricted? ? scope.notes : [NOTES[2]])
289
+ end
290
+
291
+ # Prints the stdio warning to stderr, then answers one JSON-RPC request per
292
+ # line of `$stdin` until it closes.
293
+ #
294
+ # @return [void]
295
+ def serve
296
+ McpStdioGuard.warn!(server: Mcp::SERVER, notes: warning_notes)
297
+ $stdout.sync = true
298
+
299
+ $stdin.each_line do |line|
300
+ line = line.strip
301
+ next if line.empty?
302
+
303
+ begin
304
+ request = JSON.parse(line)
305
+ rescue JSON::ParserError => e
306
+ send_error(nil, -32_700, "Parse error: #{e.message}")
307
+ next
308
+ end
309
+
310
+ begin
311
+ handle(request)
312
+ rescue StandardError => e
313
+ send_error(request["id"], -32_603, "Internal error: #{e.class}: #{e.message}")
314
+ end
315
+ end
316
+ end
317
+
318
+ # Boots one domain for a single call, confined to `Storehouse::BOOT_ROOT`.
319
+ #
320
+ # @param domain [String] the domain directory, e.g. `"examples/banking"`
321
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
322
+ # @raise [Runtime::TypeMismatch] if `domain` resolves outside `Storehouse::BOOT_ROOT`, or,
323
+ # on a reader door, outside `HECKS_DOOR_DOMAINS`
324
+ # @raise [Runtime::WiringError] if the domain fails to boot
325
+ def boot(domain)
326
+ Hecks.boot(Storehouse.confine!(scope.admit_domain!(domain), "domain"), install_facade: false)
327
+ end
328
+
329
+ # Runs one registered MCP tool by delegating to the matching `Storehouse` call.
330
+ #
331
+ # A closed-set dispatch table, one `when` per tool, so the size is the tool
332
+ # count and the shared `rescue` catches every branch's escape once.
333
+ #
334
+ # @param name [String] the tool name, one of `TOOLS`'s own `:name` values
335
+ # @param args [Hash{String => Object}] the tool's JSON arguments, shaped per that
336
+ # tool's `inputSchema`
337
+ # @return [Hash{Symbol => Object}] the delegated `Storehouse` result on a known
338
+ # tool name; `{ok: false, error: String}` for an unknown tool name or any
339
+ # `StandardError` this door did not anticipate (a domain refusal is already
340
+ # `{ok: false, ...}` from inside `Storehouse` itself)
341
+ def call_tool(name, args) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
342
+ case name
343
+ when "dispatch"
344
+ runtime = boot(args["domain"])
345
+ if args["steps"]
346
+ Storehouse.dispatch_batch(runtime: runtime, steps: args["steps"], summary: args["summary"],
347
+ source: args["source"], role: args["role"], actor_id: args["actor_id"])
348
+ else
349
+ Storehouse.dispatch(runtime: runtime, command: args["command"], summary: args["summary"],
350
+ args: args["args"] || {}, source: args["source"],
351
+ dry_run: args["dry_run"] == true, role: args["role"], actor_id: args["actor_id"])
352
+ end
353
+ when "query"
354
+ Storehouse.query(runtime: boot(args["domain"]), question: args["question"],
355
+ summary: args["summary"], args: args["args"] || {}, source: args["source"],
356
+ role: args["role"], actor_id: args["actor_id"])
357
+ when "events"
358
+ Storehouse.events(runtime: boot(args["domain"]), aggregate: args["aggregate"], id: args["id"],
359
+ limit: args["limit"])
360
+ when "state"
361
+ Storehouse.state(runtime: boot(args["domain"]), aggregate: args["aggregate"],
362
+ summary: args["summary"], id: args["id"])
363
+ when "catalog"
364
+ Storehouse.catalog(runtime: boot(args["domain"]))
365
+ when "describe"
366
+ Storehouse.describe(runtime: boot(args["domain"]), aggregate: args["aggregate"])
367
+ when "validate"
368
+ Storehouse.validate(domain: scope.admit_domain!(args["domain"]), deep: args["deep"] == true)
369
+ when "domains"
370
+ Storehouse.domains(under: args["under"] || "examples")
371
+ when "history"
372
+ Storehouse.history(runtime: boot(args["domain"]))
373
+ when "behaviors"
374
+ Storehouse.behaviors(target: args["target"])
375
+ when "follow"
376
+ Storehouse.follow(runtime: boot(args["domain"]), limit: args["limit"] || 20)
377
+ else
378
+ { ok: false, error: "no such tool: #{name.inspect} — known: #{TOOLS.map { |t| t[:name] }.join(', ')}" }
379
+ end
380
+ rescue StandardError => e
381
+ # **A defect, not a refusal** — every domain refusal is already answered as
382
+ # `{ok: false, error: "..."}` inside `Storehouse`. Reaching here means a bad
383
+ # domain path or an argument shape `Facade::JsonDoor` could not symbolize,
384
+ # still answered as structured content rather than an MCP-level `isError`,
385
+ # so a calling agent reads one shape for every outcome.
386
+ { ok: false, error: "#{e.class}: #{e.message}" }
387
+ end
388
+
389
+ # Runs one MCP tool call if this door's mode serves it, and refuses it otherwise.
390
+ #
391
+ # @param name [String] the tool name from the `tools/call` request
392
+ # @param args [Hash{String => Object}] the tool's JSON arguments
393
+ # @return [Hash{Symbol => Object}] the `call_tool` result, or `scope.refusal(name)`
394
+ # when a reader door does not serve `name`
395
+ def answer_tool(name, args)
396
+ return scope.refusal(name) unless scope.permits_tool?(name)
397
+
398
+ call_tool(name, args)
399
+ end
400
+
401
+ # Wraps a `call_tool` payload in the MCP `tools/call` result shape.
402
+ #
403
+ # @param payload [Hash{Symbol => Object}] a `call_tool` result
404
+ # @return [Hash{Symbol => Object}] `{content: [{type: "text", text: ...}], isError: Boolean}`,
405
+ # pretty-printing `payload` as the text and setting `isError` from `payload[:ok]`
406
+ def tool_result(payload)
407
+ { content: [{ type: "text", text: JSON.pretty_generate(payload) }], isError: payload[:ok] == false }
408
+ end
409
+
410
+ # Writes one JSON-RPC success response line to stdout.
411
+ #
412
+ # @param id [String, Integer, nil] the request's own `id`, echoed back
413
+ # @param result [Object] the JSON-serializable result payload
414
+ # @return [void]
415
+ def send_response(id, result)
416
+ puts JSON.generate({ jsonrpc: "2.0", id: id, result: result })
417
+ end
418
+
419
+ # Writes one JSON-RPC error response line to stdout.
420
+ #
421
+ # @param id [String, Integer, nil] the request's own `id`, echoed back; nil when
422
+ # the request itself could not be parsed
423
+ # @param code [Integer] the JSON-RPC error code
424
+ # @param message [String] the error message
425
+ # @return [void]
426
+ def send_error(id, code, message)
427
+ puts JSON.generate({ jsonrpc: "2.0", id: id, error: { code: code, message: message } })
428
+ end
429
+
430
+ # Dispatches one parsed JSON-RPC request to its method and writes the response.
431
+ #
432
+ # @param request [Hash{String => Object}] one parsed JSON-RPC request, with
433
+ # `"id"`, `"method"`, and optional `"params"`
434
+ # @return [void]
435
+ def handle(request)
436
+ id = request["id"]
437
+ method = request["method"]
438
+ params = request["params"] || {}
439
+
440
+ case method
441
+ when "initialize"
442
+ send_response(id, {
443
+ protocolVersion: PROTOCOL_VERSION,
444
+ capabilities: { tools: {} },
445
+ serverInfo: { name: Mcp::SERVER, version: "1.1.0" }
446
+ })
447
+ when "notifications/initialized"
448
+ nil # a notification — no id, no response
449
+ when "tools/list"
450
+ send_response(id, { tools: TOOLS.select { |tool| scope.permits_tool?(tool[:name]) } })
451
+ when "tools/call"
452
+ send_response(id, tool_result(answer_tool(params["name"], params["arguments"] || {})))
453
+ when "ping"
454
+ send_response(id, {})
455
+ else
456
+ send_error(id, -32_601, "Method not found: #{method}") unless id.nil?
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end