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.
- checksums.yaml +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- 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
|