Class: Terret::Tools::Registry

Inherits:
Hames::Service
  • Object
show all
Defined in:
lib/terret/tools.rb

Overview

ctx.tools — scoped registry + guarded execution pipeline. Registration is an effect (unloading a plugin removes its tools). Execution runs the three-waterfall pipeline: pre_execute (validate / veto / rewrite) -> execute (a provider may replace execution wholesale) -> post_execute (truncate / redact).

Instance Method Summary collapse

Instance Method Details

#execute(call, ctx:) ⇒ Object

Execution runs the three-waterfall pipeline: pre_execute (validate / veto / rewrite) -> execute (a provider may replace execution wholesale) -> post_execute (truncate / redact). Waterfalls dispatch on ctx, which callers set to the AGENT's forked context so per-agent policy listeners ride the fork (root listeners still run first — fork dispatch chains parent-first). ctx is required — a forgotten kwarg must fail loudly, not silently skip per-agent policy.

One thing a handler is given beyond its arguments: a handler that declares a session_id: keyword receives the executing call's session. It is injected here and never model-supplied — it is not a property of any tool's params schema, and the merge in #handler_args puts the Call's own value LAST so an argument carrying that name cannot name somebody else's session.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/terret/tools.rb', line 63

def execute(call, ctx:)
  admitted = ctx.waterfall("tools/pre_execute", call)
  return Result.new(id: call.id, content: nil, error: admitted.reason) if admitted.is_a?(Veto)

  # The deny-by-default floor is AUTHORITATIVE. It runs here — on the
  # exact call the pre_execute waterfall admitted, rewrites included —
  # not as a waterfall listener a sibling could register ahead of and
  # short-circuit past. A pre_execute listener can make policy stricter
  # (a Veto above), never looser: it cannot admit a tool the floor
  # denies, because that admission never reaches execution. This is the
  # autonomous safety mechanism (docs/security.md); a listener from a
  # third-party bundle must not be able to defeat it.
  if @floor && (veto = @floor.call(admitted)).is_a?(Veto)
    return Result.new(id: call.id, content: nil, error: veto.reason)
  end

  result = ctx.waterfall("tools/execute", admitted) do |c|
    begin
      d = fetch(c.name)
      Result.new(id: c.id, content: d.handler.call(**handler_args(d, c)), error: nil)
    rescue Failure => e
      Result.new(id: c.id, content: nil, error: e.message)
    rescue => e
      Result.new(id: c.id, content: nil, error: "#{e.class}: #{e.message}")
    end
  end
  ctx.waterfall("tools/post_execute", result)
end

#fetch(name) ⇒ Object



47
# File 'lib/terret/tools.rb', line 47

def fetch(name) = @defs.fetch(name.to_s)

#install_floor(ctx = @ctx, &predicate) ⇒ Object

Install the authoritative deny-by-default floor. The floor is a single predicate #execute consults on the admitted call, deliberately NOT a tools/pre_execute listener: a listener is a peer another row's listener can register ahead of and short-circuit past (the mount-pass bypass that defeated the floor), whereas this gate sees the call that will actually run and its Veto is final. The predicate answers a Veto to deny and anything else to admit. Recorded as an effect of the mounting row (ctx defaults to the Registry's own root), so unloading that row removes the floor; a second install replaces the first and disposal restores whatever it replaced. Only one floor is active — the deny-by-default policy is one floor, with per-session and per-agent variation layered above it (policy/updated and per-fork AllowLists).



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/terret/tools.rb', line 104

def install_floor(ctx = @ctx, &predicate)
  ctx.effect do
    previous = @floor
    # The floor is the autonomous safety mechanism, so a second install
    # silently swapping it out is worth surfacing. Not refused — a
    # legitimate re-mount or hot reconfigure disposes the old floor first
    # (restoring @floor to nil) and then re-installs, so that path sees no
    # previous floor and stays quiet; only a genuine replace-while-active
    # warns.
    if previous
      warn "terret: install_floor replaced an active tool floor; the " \
           "deny-by-default safety floor is now the one just installed"
    end
    @floor = predicate
    -> { @floor = previous }
  end
end

#register(name:, description:, params: {}, mutating: false, approval: :never, concurrency: :serial, ctx: @ctx, &handler) ⇒ Object

Returns the registration's disposer. The roster itself stays global (visibility is the AllowList's job, not this method's) — but the effect that puts a Definition in the roster is recorded on ctx, which defaults to the registry's own root and so preserves every existing call site. A caller that passes its forked agent ctx ties OWNERSHIP to that fork: disposing the agent reaps the registration, closing the M6-recorded bleed where an agent-registered tool (one that can carry filesystem authority) outlived the agent that made it.



36
37
38
39
40
41
42
43
44
# File 'lib/terret/tools.rb', line 36

def register(name:, description:, params: {}, mutating: false,
             approval: :never, concurrency: :serial, ctx: @ctx, &handler)
  d = Definition.new(name: name.to_s, description:, params:, handler:,
                     mutating:, approval:, concurrency:)
  ctx.effect do
    @defs[d.name] = d
    -> { @defs.delete(d.name) }
  end
end

#schemasObject



46
# File 'lib/terret/tools.rb', line 46

def schemas = @defs.values.map(&:schema)

#start(ctx) ⇒ Object



22
23
24
25
26
# File 'lib/terret/tools.rb', line 22

def start(ctx)
  @ctx = ctx
  @defs = {}
  @floor = nil
end