jazari 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +21 -28
- data/lib/jazari/mcp/actions.rb +124 -0
- data/lib/jazari/mcp/handler.rb +9 -13
- data/lib/jazari/version.rb +1 -1
- data/lib/jazari.rb +5 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 781a303d10e42b7e3337381b7d167aaeb227a5cf538e4122f1b37c2143216f8f
|
|
4
|
+
data.tar.gz: b6c2d6ca19ccc6d097ea30ddc644f2f58144d6fc9d6a74722e2f625c7502ba18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d42855d9257d25cb193423c6864db7451006ae2b07fe9b013442b2cf3c3c1e8f1ceca6208e5e23b1a8f60d1d57b5502ea31e5859e81734dd3a1412f0e8accd4
|
|
7
|
+
data.tar.gz: 156a565d0e8ab2b8f8f51dbb335c0410f60484da523ba9a1de6fd216bf3377f720cc3909722bed78f3854b5a836ea126a89106831e57c7c3ac9fa160802731f9
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@ codes, the resolved-value shape, how revisions are computed, and the schema the
|
|
|
8
8
|
generator emits — changes to any of those are breaking even when the method
|
|
9
9
|
signatures do not move.
|
|
10
10
|
|
|
11
|
+
## [0.2.0] - 2026-08-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`Jazari::Mcp::Actions`** — every action declared as data (`scope`, `effect`,
|
|
16
|
+
`confirm`, parameter schemas, summary), with `schema_fragment` for merging into
|
|
17
|
+
a host's own MCP tool. A host with an existing MCP surface keeps its tool name,
|
|
18
|
+
envelope, dispatch, and permissions, and calls `Jazari.*` directly — no
|
|
19
|
+
coupling to this gem's handler or reply shape.
|
|
20
|
+
- `Jazari::Mcp::Actions.summaries` for a tool description or paired skill.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **The MCP layer is now genuinely optional.** Descriptors load with the gem;
|
|
25
|
+
the dispatcher does not. `require "jazari/mcp/handler"` to opt in.
|
|
26
|
+
- `Mcp::Handler` validates and scopes against the same declarations, so a
|
|
27
|
+
published schema and the implemented behaviour cannot drift. A test asserts the
|
|
28
|
+
handler dispatches every declared action.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `mcp/handler.rb` now requires `mcp/actions`. With the layer opt-in, a host
|
|
33
|
+
requiring the handler alone would have raised `NameError`.
|
|
34
|
+
|
|
11
35
|
## [0.1.0] - 2026-08-10
|
|
12
36
|
|
|
13
37
|
First release. Proven against one host adoption.
|
data/README.md
CHANGED
|
@@ -10,6 +10,14 @@ Requires Ruby 3.2+, Rails 7.1+, and PostgreSQL.
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## Guides
|
|
14
|
+
|
|
15
|
+
The README is the pitch; the [guides](guide/) are the working documents —
|
|
16
|
+
[concepts](guide/01-concepts.md) (start here: one word means something
|
|
17
|
+
different than you expect), [adoption](guide/02-adoption.md),
|
|
18
|
+
[anchors](guide/03-anchors.md), [runs and evidence](guide/04-runs.md),
|
|
19
|
+
[MCP](guide/05-mcp.md), and [migrating an existing checklist](guide/06-migrating.md).
|
|
20
|
+
|
|
13
21
|
## The problem
|
|
14
22
|
|
|
15
23
|
You have a procedure. Verify a backup by restoring it. Provision a server.
|
|
@@ -221,34 +229,19 @@ out of coverage.
|
|
|
221
229
|
## What this is not
|
|
222
230
|
|
|
223
231
|
Not an execution framework. Jazari holds the *state* of a procedure — the canon,
|
|
224
|
-
the overrides, the runs, the evidence. It does not SSH anywhere
|
|
225
|
-
commands.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
That is not why the gem carries his name.
|
|
238
|
-
|
|
239
|
-
He also wrote *The Book of Knowledge of Ingenious Mechanical Devices*, finished
|
|
240
|
-
the year he died: fifty machines, each with numbered construction steps and
|
|
241
|
-
drawings detailed enough that a stranger who had never met him could rebuild the
|
|
242
|
-
device. He wrote down the *procedure*, not just the result — including, in his
|
|
243
|
-
own words, the steps he had gotten wrong first.
|
|
244
|
-
|
|
245
|
-
Eight hundred years later people have built working machines from those pages.
|
|
246
|
-
|
|
247
|
-
That is the whole idea here. A procedure is not lore in someone's head or prose
|
|
248
|
-
in a document nobody opens. It is a written, versioned, checkable artefact that
|
|
249
|
-
somebody else can execute and prove they executed.
|
|
250
|
-
|
|
251
|
-
`runbook` was taken on RubyGems — by the execution framework above, fittingly.
|
|
232
|
+
the overrides, the runs, the evidence. It does not SSH anywhere, shell out, or
|
|
233
|
+
run your commands.
|
|
234
|
+
|
|
235
|
+
That boundary is deliberate. Execution is already well served by whatever you
|
|
236
|
+
have — CI, a rake task, Ansible, a deploy tool — and those differ per shop.
|
|
237
|
+
What none of them keep is a durable, checkable, addressable record of *which
|
|
238
|
+
procedure* was run, by whom, and what came back. Jazari keeps that, and stays
|
|
239
|
+
out of the way of however you actually run things.
|
|
240
|
+
|
|
241
|
+
(The name `runbook` was already taken on RubyGems, by a DSL for executing
|
|
242
|
+
operational procedures. That gem's last release was 2021 and its last commit
|
|
243
|
+
2022, so it is not a dependency worth taking — but the name is still occupied,
|
|
244
|
+
which is one reason this gem is called jazari.)
|
|
252
245
|
|
|
253
246
|
## License
|
|
254
247
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jazari
|
|
4
|
+
module Mcp
|
|
5
|
+
# Declarative descriptors for every action, so a host can absorb jazari into
|
|
6
|
+
# ITS OWN tool instead of adopting this gem's handler.
|
|
7
|
+
#
|
|
8
|
+
# Without these, a host has two bad choices: take `Mcp::Handler`'s reply
|
|
9
|
+
# shape wholesale, or hand-write the action list and let it drift from the
|
|
10
|
+
# gem it describes. Neither is acceptable for a host that already has an
|
|
11
|
+
# MCP surface with its own envelope, naming, and permission model.
|
|
12
|
+
#
|
|
13
|
+
# These descriptors are the single source of truth: `Handler` validates and
|
|
14
|
+
# scopes against them too, so the schema a host publishes and the behaviour
|
|
15
|
+
# the gem implements cannot diverge.
|
|
16
|
+
module Actions
|
|
17
|
+
# effect drives how a host should annotate the action to its clients:
|
|
18
|
+
# :read — no writes
|
|
19
|
+
# :additive — writes, but cannot destroy prior state
|
|
20
|
+
# :overwrite — may replace an operator's content; annotate cautiously
|
|
21
|
+
# :destructive — removes something; gate it
|
|
22
|
+
Action = Data.define(:name, :scope, :effect, :summary, :params, :confirm) do
|
|
23
|
+
def read? = scope == :read
|
|
24
|
+
def confirm? = confirm == true
|
|
25
|
+
def to_h = { name: name, scope: scope, effect: effect, summary: summary,
|
|
26
|
+
params: params, confirm: confirm }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
REVISION = { type: "string",
|
|
30
|
+
description: "The revision from the read immediately before this call." }.freeze
|
|
31
|
+
ITEM_ID = { type: "string", description: "Opaque checklist item id." }.freeze
|
|
32
|
+
RUN_ID = { type: "integer", description: "The run returned by start." }.freeze
|
|
33
|
+
ACTOR = { type: "string", description: "Opaque identity of who is acting." }.freeze
|
|
34
|
+
|
|
35
|
+
ALL = [
|
|
36
|
+
Action.new(name: "get", scope: :read, effect: :read, confirm: false,
|
|
37
|
+
summary: "Resolve the operating procedure for a target, with progress and last run.",
|
|
38
|
+
params: {}),
|
|
39
|
+
Action.new(name: "last_run", scope: :read, effect: :read, confirm: false,
|
|
40
|
+
summary: "The most recent run for a target — answers whether the ritual actually happened.",
|
|
41
|
+
params: {}),
|
|
42
|
+
Action.new(name: "set", scope: :write, effect: :overwrite, confirm: false,
|
|
43
|
+
summary: "Replace this subject's procedure. Materialises an override on first use.",
|
|
44
|
+
params: { expected_revision: REVISION,
|
|
45
|
+
topic: { type: "string", description: "Short title." },
|
|
46
|
+
description: { type: "string", description: "Markdown body." },
|
|
47
|
+
checklist: { type: "array", description: "Full checklist; replaces the existing one." } }),
|
|
48
|
+
Action.new(name: "add_item", scope: :write, effect: :additive, confirm: false,
|
|
49
|
+
summary: "Append one checklist step.",
|
|
50
|
+
params: { expected_revision: REVISION,
|
|
51
|
+
text: { type: "string", description: "The step." },
|
|
52
|
+
required: { type: "boolean", description: "Whether the step is required. Default true." } }),
|
|
53
|
+
Action.new(name: "remove_item", scope: :write, effect: :destructive, confirm: false,
|
|
54
|
+
summary: "Delete one checklist step.",
|
|
55
|
+
params: { expected_revision: REVISION, item_id: ITEM_ID }),
|
|
56
|
+
Action.new(name: "check_item", scope: :write, effect: :additive, confirm: false,
|
|
57
|
+
summary: "Mark a step done or not done.",
|
|
58
|
+
params: { expected_revision: REVISION, item_id: ITEM_ID,
|
|
59
|
+
done: { type: "boolean", description: "Default true." } }),
|
|
60
|
+
Action.new(name: "reset", scope: :write, effect: :destructive, confirm: true,
|
|
61
|
+
summary: "Discard this subject's override and reveal the current canon.",
|
|
62
|
+
params: { expected_revision: REVISION,
|
|
63
|
+
confirm: { type: "boolean", description: "Must be true — this discards operator content." } }),
|
|
64
|
+
Action.new(name: "start", scope: :write, effect: :additive, confirm: false,
|
|
65
|
+
summary: "Open a run. Under a once-per-day recipe this returns the existing run instead of erroring.",
|
|
66
|
+
params: { actor_ref: ACTOR }),
|
|
67
|
+
Action.new(name: "tick", scope: :write, effect: :additive, confirm: false,
|
|
68
|
+
summary: "Record a step done within a run. Does not touch the subject's own checklist.",
|
|
69
|
+
params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
|
|
70
|
+
done: { type: "boolean", description: "Default true." },
|
|
71
|
+
actor_ref: ACTOR,
|
|
72
|
+
note: { type: "string", description: "Optional free text." } }),
|
|
73
|
+
Action.new(name: "evidence", scope: :write, effect: :additive, confirm: false,
|
|
74
|
+
summary: "Attach evidence to a run: output, url, sha, count, or note.",
|
|
75
|
+
params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
|
|
76
|
+
kind: { type: "string", description: "One of: output, url, sha, count, note." },
|
|
77
|
+
value: { type: "string", description: "The evidence itself." } }),
|
|
78
|
+
Action.new(name: "finish", scope: :write, effect: :additive, confirm: false,
|
|
79
|
+
summary: "Close a run with an outcome: completed, abandoned, or failed.",
|
|
80
|
+
params: { run_id: RUN_ID, expected_revision: REVISION,
|
|
81
|
+
outcome: { type: "string", description: "completed | abandoned | failed" } })
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
NAMES = ALL.map(&:name).freeze
|
|
85
|
+
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
# scope: :read advertises only the read-only subset. A read-scoped
|
|
89
|
+
# connection should not SEE mutations in its tool list — offering them and
|
|
90
|
+
# then refusing is a worse experience than not offering them.
|
|
91
|
+
def all(scope: :write)
|
|
92
|
+
scope.to_s == "read" ? ALL.select(&:read?) : ALL
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def names(scope: :write) = all(scope: scope).map(&:name)
|
|
96
|
+
|
|
97
|
+
def fetch(name)
|
|
98
|
+
ALL.find { |action| action.name == name.to_s } or
|
|
99
|
+
raise ArgumentError, "unknown runbook action #{name.inspect}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A fragment a host merges into its OWN tool's input schema. It deliberately
|
|
103
|
+
# returns only the action enum and the parameter properties — the host owns
|
|
104
|
+
# the tool name, the description, its own target params, and its reply
|
|
105
|
+
# envelope. Nothing here presumes this gem's handler is in the loop.
|
|
106
|
+
#
|
|
107
|
+
# frag = Jazari::Mcp::Actions.schema_fragment
|
|
108
|
+
# MY_TOOL[:input_schema][:properties].merge!(frag[:properties])
|
|
109
|
+
# MY_TOOL[:input_schema][:properties][:action][:enum] += frag[:enum]
|
|
110
|
+
def schema_fragment(scope: :write)
|
|
111
|
+
actions = all(scope: scope)
|
|
112
|
+
properties = actions.each_with_object({}) do |action, acc|
|
|
113
|
+
action.params.each { |key, spec| acc[key] ||= spec }
|
|
114
|
+
end
|
|
115
|
+
{ enum: actions.map(&:name), properties: properties }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Human-readable action list for a tool description or a paired skill.
|
|
119
|
+
def summaries(scope: :write)
|
|
120
|
+
all(scope: scope).map { |a| "#{a.name} — #{a.summary}" }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/jazari/mcp/handler.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# The handler dispatches against Mcp::Actions, so it must require it: a host may
|
|
4
|
+
# `require "jazari/mcp/handler"` on its own now that this layer is opt-in.
|
|
5
|
+
require "jazari/mcp/actions"
|
|
6
|
+
|
|
3
7
|
module Jazari
|
|
4
8
|
module Mcp
|
|
5
9
|
# Transport-neutral adapter between an MCP action name and the domain.
|
|
@@ -13,23 +17,15 @@ module Jazari
|
|
|
13
17
|
# That split is deliberate: two products sharing this handler still present
|
|
14
18
|
# their own tool, their own subject vocabulary, and their own permissions.
|
|
15
19
|
class Handler
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Advertise only what the granted scope may call. A read-scoped connection
|
|
21
|
-
# should not see mutations in its tool list at all — hiding them is not
|
|
22
|
-
# security, but offering them and refusing is a worse experience.
|
|
23
|
-
def self.actions_for(scope)
|
|
24
|
-
scope.to_s == "read" ? READ_ACTIONS : ACTIONS
|
|
25
|
-
end
|
|
20
|
+
# Actions are declared once, in Mcp::Actions, so the schema a host
|
|
21
|
+
# publishes and the behaviour implemented here cannot drift apart.
|
|
22
|
+
def self.actions_for(scope) = Actions.names(scope: scope)
|
|
26
23
|
|
|
27
24
|
def call(action:, target:, arguments: {})
|
|
28
25
|
args = symbolize(arguments)
|
|
29
|
-
|
|
30
|
-
raise ArgumentError, "unknown runbook action #{action.inspect}" unless ACTIONS.include?(name)
|
|
26
|
+
descriptor = Actions.fetch(action) # raises on an unknown action
|
|
31
27
|
|
|
32
|
-
reply(public_send(:"handle_#{name}", target, args))
|
|
28
|
+
reply(public_send(:"handle_#{descriptor.name}", target, args))
|
|
33
29
|
rescue Jazari::Error => error
|
|
34
30
|
# The closed taxonomy crosses the wire as a code, never as a message
|
|
35
31
|
# that could disclose a record, a query, or whether a target exists.
|
data/lib/jazari/version.rb
CHANGED
data/lib/jazari.rb
CHANGED
|
@@ -10,7 +10,11 @@ require "jazari/resolved_runbook"
|
|
|
10
10
|
require "jazari/recipe_registry"
|
|
11
11
|
require "jazari/runs"
|
|
12
12
|
require "jazari/operations"
|
|
13
|
-
|
|
13
|
+
# The MCP layer is OPTIONAL. Descriptors are cheap and a host may want them to
|
|
14
|
+
# build its own tool, so they load; the handler does not, because a host with
|
|
15
|
+
# its own MCP surface never calls it.
|
|
16
|
+
# require "jazari/mcp/handler" # only if you want the ready-made dispatcher
|
|
17
|
+
require "jazari/mcp/actions"
|
|
14
18
|
require "jazari/railtie" if defined?(::Rails::Railtie)
|
|
15
19
|
|
|
16
20
|
# Addressable operating procedures.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jazari
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nauman Tariq
|
|
@@ -60,6 +60,7 @@ files:
|
|
|
60
60
|
- lib/jazari/anchors.rb
|
|
61
61
|
- lib/jazari/checklist.rb
|
|
62
62
|
- lib/jazari/errors.rb
|
|
63
|
+
- lib/jazari/mcp/actions.rb
|
|
63
64
|
- lib/jazari/mcp/handler.rb
|
|
64
65
|
- lib/jazari/operations.rb
|
|
65
66
|
- lib/jazari/railtie.rb
|