textus 0.52.0 → 0.54.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 +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/retention/apply.rb +0 -52
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# Bulk-delete every leaf key under `prefix`.
|
|
4
|
-
class KeyDeletePrefix
|
|
5
|
-
extend Textus::Contract::DSL
|
|
6
|
-
|
|
7
|
-
verb :key_delete_prefix
|
|
8
|
-
summary "Bulk-delete every leaf key under prefix."
|
|
9
|
-
surfaces :cli, :mcp
|
|
10
|
-
cli "key delete-prefix"
|
|
11
|
-
arg :prefix, String, required: true, positional: true, description: "every leaf key under this dotted prefix is deleted"
|
|
12
|
-
arg :dry_run, :boolean, default: false,
|
|
13
|
-
description: "when true, returns the keys that would be deleted without deleting them; " \
|
|
14
|
-
"defaults to false, so omitting it deletes immediately"
|
|
15
|
-
view { |v, _i| v.to_h }
|
|
16
|
-
|
|
17
|
-
def initialize(container:, call:)
|
|
18
|
-
@container = container
|
|
19
|
-
@call = call
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call(prefix, dry_run: false)
|
|
23
|
-
raise UsageError.new("prefix required") if prefix.nil? || prefix.empty?
|
|
24
|
-
|
|
25
|
-
leaves = Read::List.new(container: @container)
|
|
26
|
-
.call(prefix: prefix)
|
|
27
|
-
.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
|
|
28
|
-
|
|
29
|
-
warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
|
|
30
|
-
steps = leaves.map { |k| { "op" => "delete", "key" => k } }
|
|
31
|
-
|
|
32
|
-
plan = Plan.new(steps: steps, warnings: warnings)
|
|
33
|
-
return plan if dry_run
|
|
34
|
-
|
|
35
|
-
steps.each do |s|
|
|
36
|
-
delete.call(s["key"])
|
|
37
|
-
end
|
|
38
|
-
plan
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def delete
|
|
44
|
-
Write::KeyDelete.new(container: @container, call: @call)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# Bulk-rename every leaf key under `from_prefix` to `to_prefix`.
|
|
4
|
-
# Calls Write::KeyMv directly for each entry — emits one audit row per file moved.
|
|
5
|
-
class KeyMvPrefix
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :key_mv_prefix
|
|
9
|
-
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "key mv-prefix"
|
|
12
|
-
arg :from_prefix, String, required: true, positional: true, description: "dotted prefix whose leaf keys are renamed"
|
|
13
|
-
arg :to_prefix, String, required: true, positional: true, description: "dotted prefix the keys are renamed to"
|
|
14
|
-
arg :dry_run, :boolean, default: false,
|
|
15
|
-
description: "when true, returns the planned moves without applying them; " \
|
|
16
|
-
"defaults to false, so omitting it applies the rename immediately"
|
|
17
|
-
view { |v, _i| v.to_h }
|
|
18
|
-
|
|
19
|
-
def initialize(container:, call:)
|
|
20
|
-
@container = container
|
|
21
|
-
@call = call
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def call(from_prefix, to_prefix, dry_run: false)
|
|
25
|
-
raise UsageError.new("from_prefix and to_prefix required") if from_prefix.nil? || to_prefix.nil?
|
|
26
|
-
|
|
27
|
-
leaves = list_leaves_under(from_prefix)
|
|
28
|
-
|
|
29
|
-
# When from_prefix is itself a leaf, `delete_prefix("#{from_prefix}.")`
|
|
30
|
-
# finds no trailing dot to strip, so the tail keeps the whole key and the
|
|
31
|
-
# move silently targets "to_prefix.<full-from_prefix>". Refuse it — a
|
|
32
|
-
# single-key rename is `mv`'s job, not the bulk prefix verb's.
|
|
33
|
-
if leaves.include?(from_prefix)
|
|
34
|
-
raise UsageError.new("from_prefix '#{from_prefix}' is itself a leaf — use `mv` to rename a single key")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
warnings = []
|
|
38
|
-
warnings << "no keys under #{from_prefix}" if leaves.empty?
|
|
39
|
-
|
|
40
|
-
steps = leaves.map do |old_key|
|
|
41
|
-
tail = old_key.delete_prefix("#{from_prefix}.")
|
|
42
|
-
new_key = "#{to_prefix}.#{tail}"
|
|
43
|
-
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
plan = Plan.new(steps: steps, warnings: warnings)
|
|
47
|
-
return plan if dry_run
|
|
48
|
-
|
|
49
|
-
steps.each do |s|
|
|
50
|
-
mv.call(s["from"], s["to"], dry_run: false)
|
|
51
|
-
end
|
|
52
|
-
plan
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def list_leaves_under(prefix)
|
|
58
|
-
Read::List.new(container: @container)
|
|
59
|
-
.call(prefix: prefix)
|
|
60
|
-
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def mv
|
|
64
|
-
Write::KeyMv.new(container: @container, call: @call)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
module Retention
|
|
6
|
-
# The destructive half of convergence: apply retention rows (drop/archive).
|
|
7
|
-
# Lifted verbatim from the legacy reconcile apply/archive_leaf so drain/serve and
|
|
8
|
-
# the `sweep` job handler share one path. Runs as the caller's role — never
|
|
9
|
-
# self-elevates (ADR 0079/0093: destructiveness decides authority).
|
|
10
|
-
class Apply
|
|
11
|
-
def initialize(container:, call:)
|
|
12
|
-
@container = container
|
|
13
|
-
@call = call
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(rows)
|
|
17
|
-
out = { dropped: [], archived: [], failed: [] }
|
|
18
|
-
delete = Write::KeyDelete.new(container: @container, call: @call)
|
|
19
|
-
rows.each do |row|
|
|
20
|
-
key = row["key"]
|
|
21
|
-
begin
|
|
22
|
-
case row["action"]
|
|
23
|
-
when "drop"
|
|
24
|
-
delete.call(key)
|
|
25
|
-
out[:dropped] << key
|
|
26
|
-
when "archive"
|
|
27
|
-
archive_leaf(row)
|
|
28
|
-
delete.call(key)
|
|
29
|
-
out[:archived] << key
|
|
30
|
-
end
|
|
31
|
-
rescue Textus::Error => e
|
|
32
|
-
out[:failed] << { "key" => key, "error" => e.message }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
out
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
# Copy the leaf into <store>/archive/<relative-path> before deletion.
|
|
41
|
-
def archive_leaf(row)
|
|
42
|
-
src = row["path"]
|
|
43
|
-
root = @container.root.to_s
|
|
44
|
-
rel = src.delete_prefix("#{root}/")
|
|
45
|
-
dest = File.join(root, "archive", rel)
|
|
46
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
47
|
-
FileUtils.cp(src, dest)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
# Compare the live manifest's `rules:` block against a candidate
|
|
6
|
-
# YAML string. Returns a Plan describing rule additions/removals/
|
|
7
|
-
# changes. Does NOT write anything.
|
|
8
|
-
class RuleLint
|
|
9
|
-
extend Textus::Contract::DSL
|
|
10
|
-
|
|
11
|
-
verb :rule_lint
|
|
12
|
-
summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
|
|
13
|
-
surfaces :cli, :mcp
|
|
14
|
-
cli "rule lint"
|
|
15
|
-
arg :candidate_yaml, String, required: true, wire_name: :against, source: :file,
|
|
16
|
-
description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
|
|
17
|
-
view { |v, _i| v.to_h }
|
|
18
|
-
|
|
19
|
-
def initialize(container:, call:)
|
|
20
|
-
@container = container
|
|
21
|
-
@call = call
|
|
22
|
-
@root = container.root
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def call(candidate_yaml:)
|
|
26
|
-
live_rules = current_rules
|
|
27
|
-
candidate_rules = parse_candidate(candidate_yaml)
|
|
28
|
-
|
|
29
|
-
live_by_match = live_rules.to_h { |r| [r["match"], r] }
|
|
30
|
-
candidate_by_match = candidate_rules.to_h { |r| [r["match"], r] }
|
|
31
|
-
|
|
32
|
-
steps = (candidate_by_match.keys - live_by_match.keys).map do |m|
|
|
33
|
-
{ "op" => "add_rule", "match" => m, "rule" => candidate_by_match[m] }
|
|
34
|
-
end
|
|
35
|
-
(live_by_match.keys - candidate_by_match.keys).each do |m|
|
|
36
|
-
steps << { "op" => "remove_rule", "match" => m }
|
|
37
|
-
end
|
|
38
|
-
(live_by_match.keys & candidate_by_match.keys).each do |m|
|
|
39
|
-
next if live_by_match[m] == candidate_by_match[m]
|
|
40
|
-
|
|
41
|
-
steps << { "op" => "change_rule", "match" => m,
|
|
42
|
-
"from" => live_by_match[m], "to" => candidate_by_match[m] }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Plan.new(steps: steps, warnings: [])
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def current_rules
|
|
51
|
-
raw = YAML.safe_load_file(File.join(@root, "manifest.yaml"),
|
|
52
|
-
permitted_classes: [Symbol], aliases: false)
|
|
53
|
-
Array(raw["rules"])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def parse_candidate(yaml_text)
|
|
57
|
-
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
58
|
-
raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
59
|
-
|
|
60
|
-
Array(raw["rules"])
|
|
61
|
-
rescue Psych::Exception => e
|
|
62
|
-
raise UsageError.new("candidate YAML parse error: #{e.message}")
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# The convergence daemon loop: seed scheduled work (TTL re-pull + sweep),
|
|
4
|
-
# reclaim crashed leases, drain the queue, sleep, repeat. `tick` is one
|
|
5
|
-
# iteration (unit-testable); `run` loops forever. Drains serially for the
|
|
6
|
-
# same reason as Drain — each produce job self-locks, so running them in turn
|
|
7
|
-
# keeps the build lock uncontended.
|
|
8
|
-
class Serve
|
|
9
|
-
def initialize(container:, call:)
|
|
10
|
-
@container = container
|
|
11
|
-
@call = call
|
|
12
|
-
@queue = Textus::Ports::Queue.new(root: container.root)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def tick
|
|
16
|
-
Textus::Jobs::Scheduler.new(container: @container, queue: @queue).run_once
|
|
17
|
-
@queue.reclaim(now: Textus::Ports::Clock.new.now)
|
|
18
|
-
Worker.for(container: @container, queue: @queue).drain
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def run(poll: nil)
|
|
22
|
-
interval = poll || @container.manifest.data.worker_config[:poll]
|
|
23
|
-
loop do
|
|
24
|
-
tick
|
|
25
|
-
sleep(interval)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# Drains the job queue: lease a job, look up its handler in the registry, run
|
|
4
|
-
# it (as the job's stamped authority — wired in a later phase), then ack on
|
|
5
|
-
# success or fail (requeue/dead-letter) on a raise. `drain` runs until the
|
|
6
|
-
# queue is empty and returns a summary. Delivery is at-least-once.
|
|
7
|
-
class Worker
|
|
8
|
-
Summary = Struct.new(:completed, :failed, keyword_init: true)
|
|
9
|
-
|
|
10
|
-
# The standard convergence worker: the closed handler allow-list plus the
|
|
11
|
-
# lease TTL from worker_config. Both `drain` and `serve` build it this way.
|
|
12
|
-
def self.for(container:, queue:)
|
|
13
|
-
new(
|
|
14
|
-
queue: queue, registry: Textus::Jobs::Handlers.registry,
|
|
15
|
-
container: container, lease_ttl: container.manifest.data.worker_config[:lease_ttl]
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize(queue:, registry:, container:, lease_ttl: 60)
|
|
20
|
-
@queue = queue
|
|
21
|
-
@registry = registry
|
|
22
|
-
@container = container
|
|
23
|
-
@lease_ttl = lease_ttl
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def drain(worker_id: "drain-#{Process.pid}")
|
|
27
|
-
completed = 0
|
|
28
|
-
failed = 0
|
|
29
|
-
loop do
|
|
30
|
-
leased = @queue.lease(worker_id: worker_id, lease_ttl: @lease_ttl)
|
|
31
|
-
break unless leased
|
|
32
|
-
|
|
33
|
-
case run_one(leased)
|
|
34
|
-
when :completed then completed += 1
|
|
35
|
-
when :dead_lettered then failed += 1
|
|
36
|
-
# :requeued -> a transient failure; it re-leases on a later iteration
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
Summary.new(completed: completed, failed: failed)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def drain_pool(pool: 4)
|
|
43
|
-
summaries = []
|
|
44
|
-
mutex = Mutex.new
|
|
45
|
-
threads = Array.new(pool) do |i|
|
|
46
|
-
Thread.new do
|
|
47
|
-
s = drain(worker_id: "pool-#{Process.pid}-#{i}")
|
|
48
|
-
mutex.synchronize { summaries << s }
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
threads.each(&:join)
|
|
52
|
-
Summary.new(
|
|
53
|
-
completed: summaries.sum(&:completed),
|
|
54
|
-
failed: summaries.sum(&:failed),
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Returns :completed on ack, or the queue's failure verdict (:requeued |
|
|
61
|
-
# :dead_lettered) on a raise. A requeued job re-leases on the next loop
|
|
62
|
-
# iteration, so a transient failure still drains; only a dead-letter is a
|
|
63
|
-
# terminal failure that counts toward the summary.
|
|
64
|
-
def run_one(leased)
|
|
65
|
-
entry = @registry.lookup(leased.job.type)
|
|
66
|
-
entry.handler.call(job: leased.job, container: @container)
|
|
67
|
-
@queue.ack(leased)
|
|
68
|
-
:completed
|
|
69
|
-
rescue StandardError => e
|
|
70
|
-
@queue.fail(leased, error: e.message)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
# Rename a zone — rewrites the manifest's zones[] entry, rewrites
|
|
6
|
-
# the `zone:` field on every entry under the old zone, and moves
|
|
7
|
-
# every file from zones/<old>/ to zones/<new>/.
|
|
8
|
-
class ZoneMv
|
|
9
|
-
extend Textus::Contract::DSL
|
|
10
|
-
|
|
11
|
-
verb :zone_mv
|
|
12
|
-
summary "Rename a zone — manifest + files. Refuses if destination exists."
|
|
13
|
-
surfaces :cli, :mcp
|
|
14
|
-
cli "zone mv"
|
|
15
|
-
arg :from, String, required: true, positional: true, description: "current zone name"
|
|
16
|
-
arg :to, String, required: true, positional: true, description: "new zone name; refused if a zone by this name already exists"
|
|
17
|
-
arg :dry_run, :boolean, default: false,
|
|
18
|
-
description: "when true, returns the planned zone move without applying it; " \
|
|
19
|
-
"defaults to false, so omitting it applies the move immediately"
|
|
20
|
-
view { |v, _i| v.to_h }
|
|
21
|
-
|
|
22
|
-
def initialize(container:, call:)
|
|
23
|
-
@container = container
|
|
24
|
-
@call = call
|
|
25
|
-
@manifest = container.manifest
|
|
26
|
-
@root = container.root
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def call(from, to, dry_run: false)
|
|
30
|
-
raise UsageError.new("from and to required") if from.nil? || to.nil? || from.empty? || to.empty?
|
|
31
|
-
raise UsageError.new("zone '#{from}' not declared") unless @manifest.data.declared_zone_kinds.key?(from)
|
|
32
|
-
|
|
33
|
-
dest_dir = File.join(@root, "zones", to)
|
|
34
|
-
raise UsageError.new("destination 'zones/#{to}' already exists") if File.exist?(dest_dir)
|
|
35
|
-
|
|
36
|
-
affected_keys = @manifest.data.entries.select { |e| e.zone == from }.map(&:key)
|
|
37
|
-
|
|
38
|
-
steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
|
|
39
|
-
steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
|
|
40
|
-
|
|
41
|
-
plan = Plan.new(steps: steps, warnings: [])
|
|
42
|
-
return plan if dry_run
|
|
43
|
-
|
|
44
|
-
rewrite_manifest!(from, to)
|
|
45
|
-
FileUtils.mv(File.join(@root, "zones", from), dest_dir)
|
|
46
|
-
plan
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def rewrite_manifest!(from, to)
|
|
52
|
-
path = File.join(@root, "manifest.yaml")
|
|
53
|
-
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
54
|
-
raw["zones"].each { |z| z["name"] = to if z["name"] == from }
|
|
55
|
-
raw["entries"].each do |e|
|
|
56
|
-
e["zone"] = to if e["zone"] == from
|
|
57
|
-
e["key"] = e["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
|
|
58
|
-
e["path"] = e["path"].sub(%r{\A#{Regexp.escape(from)}(/|\z)}, "#{to}\\1")
|
|
59
|
-
end
|
|
60
|
-
File.write(path, YAML.dump(raw))
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
data/lib/textus/maintenance.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Bulk and structural changes to a textus store. Each use case returns
|
|
3
|
-
# a Plan when called with dry_run: true, and applies the plan when
|
|
4
|
-
# called with dry_run: false.
|
|
5
|
-
module Maintenance
|
|
6
|
-
# A Plan is a JSON-shaped preview. Steps are op-tagged hashes the
|
|
7
|
-
# use case knows how to apply. Warnings are strings surfaced to
|
|
8
|
-
# the operator (skipped keys, ambiguities).
|
|
9
|
-
Plan = Data.define(:steps, :warnings) do
|
|
10
|
-
def to_h
|
|
11
|
-
{ "steps" => steps, "warnings" => warnings }
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
module Events
|
|
6
|
-
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
|
-
pubsub_events = Textus::Hooks::Catalog::PUBSUB.keys
|
|
8
|
-
events = entry.events
|
|
9
|
-
events.each_key do |evt|
|
|
10
|
-
next if pubsub_events.include?(evt.to_sym)
|
|
11
|
-
|
|
12
|
-
raise UsageError.new(
|
|
13
|
-
"entry '#{entry.key}': unknown event '#{evt}' in events: block. " \
|
|
14
|
-
"Known events: #{pubsub_events.join(", ")}.",
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/textus/mcp/catalog.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Derives the entire MCP tool surface from the per-verb contracts
|
|
4
|
-
# (ADR 0039). `tool_schemas` feeds tools/list; `call` is the generic
|
|
5
|
-
# tools/call dispatch: map JSON args -> (positional, keyword) per the
|
|
6
|
-
# contract, invoke the verb through the role scope, then shape the
|
|
7
|
-
# return value with the contract's default view. No per-tool code.
|
|
8
|
-
module Catalog
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
12
|
-
def specs
|
|
13
|
-
Textus::Dispatcher::VERBS.values
|
|
14
|
-
.select { |k| mcp_surfaced?(k) }
|
|
15
|
-
.map(&:contract)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def tool_schemas
|
|
19
|
-
specs.map do |s|
|
|
20
|
-
{ name: s.verb.to_s, description: s.summary, inputSchema: s.input_schema }
|
|
21
|
-
end.freeze
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def names
|
|
25
|
-
specs.map { |s| s.verb.to_s }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
29
|
-
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
30
|
-
# from this so it can never advertise a verb the agent cannot call, nor
|
|
31
|
-
# omit one it can (ADR 0056). Excludes Write/Maintenance.
|
|
32
|
-
def read_verbs
|
|
33
|
-
Textus::Dispatcher::VERBS
|
|
34
|
-
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Read::") }
|
|
35
|
-
.keys.map(&:to_s)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
39
|
-
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
40
|
-
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
41
|
-
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
42
|
-
# (ADR 0056, ADR 0057).
|
|
43
|
-
def write_verbs
|
|
44
|
-
Textus::Dispatcher::VERBS
|
|
45
|
-
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Write::") }
|
|
46
|
-
.keys.map(&:to_s)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def mcp_surfaced?(klass)
|
|
50
|
-
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def call(name, session:, store:, args:)
|
|
54
|
-
klass = Textus::Dispatcher::VERBS[name.to_sym]
|
|
55
|
-
raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
|
|
56
|
-
|
|
57
|
-
spec = klass.contract
|
|
58
|
-
inputs = Textus::Contract::Binder.inputs_from_wire(spec, args)
|
|
59
|
-
result = store.as(session.role).dispatch_bound(spec.verb, inputs, session: session)
|
|
60
|
-
Textus::Contract::View.render(spec, :default, result, inputs)
|
|
61
|
-
rescue Textus::Contract::MissingArgs => e
|
|
62
|
-
raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
63
|
-
rescue ContractDrift, CursorExpired
|
|
64
|
-
raise
|
|
65
|
-
rescue Textus::Error => e
|
|
66
|
-
raise ToolError.new("#{name}: #{e.message}")
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
data/lib/textus/mcp/errors.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Manifest fingerprint changed mid-session. Client should re-boot.
|
|
4
|
-
class ContractDrift < Textus::Error
|
|
5
|
-
JSONRPC_CODE = -32_001
|
|
6
|
-
|
|
7
|
-
def initialize(message, details: {})
|
|
8
|
-
super("contract_drift", message, details: details)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Audit cursor fell off the keep window. Client should re-boot and
|
|
13
|
-
# resume from the new latest_seq.
|
|
14
|
-
class CursorExpired < Textus::Error
|
|
15
|
-
JSONRPC_CODE = -32_002
|
|
16
|
-
|
|
17
|
-
def initialize(message, details: {})
|
|
18
|
-
super("cursor_expired", message, details: details)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
23
|
-
# underlying Textus::Error or generic StandardError.
|
|
24
|
-
class ToolError < Textus::Error
|
|
25
|
-
JSONRPC_CODE = -32_000
|
|
26
|
-
|
|
27
|
-
def initialize(message, details: {})
|
|
28
|
-
super("tool_error", message, details: details)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
data/lib/textus/mcp/server.rb
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module MCP
|
|
5
|
-
# Stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05. One line per
|
|
6
|
-
# message (NDJSON). Holds a single Session for the lifetime of stdin.
|
|
7
|
-
class Server
|
|
8
|
-
PROTOCOL_VERSION = "2024-11-05"
|
|
9
|
-
SERVER_INFO = { "name" => "textus", "version" => Textus::VERSION }.freeze
|
|
10
|
-
|
|
11
|
-
def initialize(store:, stdin: $stdin, stdout: $stdout, role: Textus::Role::DEFAULT)
|
|
12
|
-
@store = store
|
|
13
|
-
@stdin = stdin
|
|
14
|
-
@stdout = stdout
|
|
15
|
-
@role = role
|
|
16
|
-
@session = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run
|
|
20
|
-
@stdin.each_line do |line|
|
|
21
|
-
line = line.strip
|
|
22
|
-
next if line.empty?
|
|
23
|
-
|
|
24
|
-
handle_line(line)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def handle_line(line)
|
|
31
|
-
msg = JSON.parse(line)
|
|
32
|
-
rescue JSON::ParserError => e
|
|
33
|
-
emit_error(nil, -32_700, "parse error: #{e.message}")
|
|
34
|
-
else
|
|
35
|
-
dispatch(msg)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def dispatch(msg)
|
|
39
|
-
rid = msg["id"]
|
|
40
|
-
case msg["method"]
|
|
41
|
-
when "initialize" then handle_initialize(rid, msg["params"] || {})
|
|
42
|
-
when "tools/list" then handle_tools_list(rid)
|
|
43
|
-
when "tools/call" then handle_tools_call(rid, msg["params"] || {})
|
|
44
|
-
when "ping" then emit_result(rid, {})
|
|
45
|
-
when "shutdown" then emit_result(rid, nil)
|
|
46
|
-
when "notifications/initialized" then nil
|
|
47
|
-
else emit_error(rid, -32_601, "method not found: #{msg["method"]}")
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def handle_initialize(rid, _params)
|
|
52
|
-
# The acting role IS the resolved connection role (ADR 0040): the MCP
|
|
53
|
-
# transport defaults to `agent`, which can write the queue, so its
|
|
54
|
-
# propose_zone resolves directly. If a connection's role cannot propose,
|
|
55
|
-
# propose_zone is nil and the `propose` tool reports that honestly.
|
|
56
|
-
propose_zone = @store.manifest.policy.propose_zone_for(@role)
|
|
57
|
-
|
|
58
|
-
@session = Session.new(
|
|
59
|
-
role: @role,
|
|
60
|
-
cursor: @store.audit_log.latest_seq,
|
|
61
|
-
propose_zone: propose_zone,
|
|
62
|
-
contract_etag: contract_etag,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# ADR 0075: announce the connection to connect-time hooks with the
|
|
66
|
-
# resolved role. Distinct from :store_loaded (fired at Store.new under
|
|
67
|
-
# the default role, before any connection's role is known).
|
|
68
|
-
@store.events.publish(
|
|
69
|
-
:session_opened,
|
|
70
|
-
ctx: Hooks::Context.new(scope: @store.as(@role)),
|
|
71
|
-
role: @role,
|
|
72
|
-
cursor: @session.cursor,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
emit_result(rid, {
|
|
76
|
-
"protocolVersion" => PROTOCOL_VERSION,
|
|
77
|
-
"serverInfo" => SERVER_INFO,
|
|
78
|
-
"capabilities" => { "tools" => {} },
|
|
79
|
-
})
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def handle_tools_list(rid)
|
|
83
|
-
emit_result(rid, { "tools" => Catalog.tool_schemas })
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def handle_tools_call(rid, params)
|
|
87
|
-
unless @session
|
|
88
|
-
emit_error(rid, -32_002, "session not initialized; call 'initialize' first")
|
|
89
|
-
return
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
name = params["name"]
|
|
93
|
-
args = params["arguments"] || {}
|
|
94
|
-
|
|
95
|
-
# ADR 0083: the contract-drift guard gates mutating verbs — every MCP
|
|
96
|
-
# verb that is NOT a pure read (Write:: + the destructive Maintenance::
|
|
97
|
-
# verbs drain/zone_mv/key_*_prefix). Reads and boot bypass it (a stale
|
|
98
|
-
# read returns on-disk truth; boot re-orients). Keying on read_verbs
|
|
99
|
-
# (not write_verbs) keeps the destructive Maintenance:: verbs gated.
|
|
100
|
-
@session.check_etag!(contract_etag) unless Catalog.read_verbs.include?(name)
|
|
101
|
-
|
|
102
|
-
result = Catalog.call(name, session: @session, store: @store, args: args)
|
|
103
|
-
@session = @session.advance_cursor(@store.audit_log.latest_seq) if name == "pulse"
|
|
104
|
-
@session = @session.with(contract_etag: contract_etag) if name == "boot"
|
|
105
|
-
|
|
106
|
-
emit_result(rid, {
|
|
107
|
-
"content" => [{ "type" => "text", "text" => JSON.dump(result) }],
|
|
108
|
-
"isError" => false,
|
|
109
|
-
})
|
|
110
|
-
rescue ContractDrift => e
|
|
111
|
-
emit_error(rid, ContractDrift::JSONRPC_CODE, e.message)
|
|
112
|
-
rescue CursorExpired => e
|
|
113
|
-
emit_error(rid, CursorExpired::JSONRPC_CODE, e.message)
|
|
114
|
-
rescue ToolError => e
|
|
115
|
-
emit_error(rid, ToolError::JSONRPC_CODE, e.message)
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
emit_error(rid, -32_603, "internal: #{e.class}: #{e.message}")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def contract_etag
|
|
121
|
-
Textus::Etag.for_contract(@store.root)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def emit_result(rid, result)
|
|
125
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "result" => result })
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def emit_error(rid, code, message)
|
|
129
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "error" => { "code" => code, "message" => message } })
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def write(obj)
|
|
133
|
-
@stdout.puts(JSON.dump(obj))
|
|
134
|
-
@stdout.flush
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|