Class: KairosMcp::InvocationContext

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/invocation_context.rb

Overview

Tracks invocation chain metadata for internal tool-to-tool calls. Carries depth, caller, mandate, and policy (whitelist/blacklist) through the entire invocation chain. Created by BaseTool#invoke_tool, threaded through ToolRegistry#call_tool.

Defined Under Namespace

Classes: DepthExceededError, PolicyDeniedError

Constant Summary collapse

MAX_DEPTH =
10

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(depth: 0, caller_tool: nil, mandate_id: nil, token_budget: nil, whitelist: nil, blacklist: nil, root_invocation_id: nil, mode: nil, idem_key: nil) ⇒ InvocationContext

Returns a new instance of InvocationContext.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/kairos_mcp/invocation_context.rb', line 17

def initialize(depth: 0, caller_tool: nil, mandate_id: nil,
               token_budget: nil, whitelist: nil, blacklist: nil,
               root_invocation_id: nil, mode: nil, idem_key: nil)
  @depth = depth
  @caller_tool = caller_tool
  @mandate_id = mandate_id
  @token_budget = token_budget
  @whitelist = whitelist
  @blacklist = blacklist
  @root_invocation_id = root_invocation_id || SecureRandom.hex(8)
  # mode: :direct (default), :daemon, :agent, ... — identifies the
  # top-level runner of the invocation chain.  Kept as a Symbol in
  # Ruby space; serialized as a String in to_h/from_h.
  @mode = mode.nil? ? nil : mode.to_sym
  # idem_key: optional client-supplied key that lets the daemon
  # deduplicate retries of the same logical command.
  @idem_key = idem_key
end

Instance Attribute Details

#blacklistObject (readonly)

Returns the value of attribute blacklist.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def blacklist
  @blacklist
end

#caller_toolObject (readonly)

Returns the value of attribute caller_tool.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def caller_tool
  @caller_tool
end

#depthObject (readonly)

Returns the value of attribute depth.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def depth
  @depth
end

#idem_keyObject (readonly)

Returns the value of attribute idem_key.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def idem_key
  @idem_key
end

#mandate_idObject (readonly)

Returns the value of attribute mandate_id.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def mandate_id
  @mandate_id
end

#modeObject (readonly)

Returns the value of attribute mode.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def mode
  @mode
end

#root_invocation_idObject (readonly)

Returns the value of attribute root_invocation_id.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def root_invocation_id
  @root_invocation_id
end

#token_budgetObject (readonly)

Returns the value of attribute token_budget.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def token_budget
  @token_budget
end

#whitelistObject (readonly)

Returns the value of attribute whitelist.



13
14
15
# File 'lib/kairos_mcp/invocation_context.rb', line 13

def whitelist
  @whitelist
end

Class Method Details

.from_h(hash) ⇒ Object

Reconstruct policy from a Hash (e.g., parsed from tool arguments). Only restores policy fields — depth and caller are not transferred.



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/kairos_mcp/invocation_context.rb', line 117

def self.from_h(hash)
  return nil if hash.nil?

  new(
    whitelist: hash['whitelist'],
    blacklist: hash['blacklist'],
    mandate_id: hash['mandate_id'],
    token_budget: hash['token_budget'],
    mode: hash['mode'],
    idem_key: hash['idem_key']
  )
end

.from_json(json_string) ⇒ Object



130
131
132
133
# File 'lib/kairos_mcp/invocation_context.rb', line 130

def self.from_json(json_string)
  require 'json'
  from_h(JSON.parse(json_string))
end

Instance Method Details

#allowed?(tool_name) ⇒ Boolean

Check if a tool is allowed by whitelist/blacklist policy. Blacklist is checked first (deny wins). Both use fnmatch patterns. For namespaced tools (e.g., “peer1/agent_start”), also checks the bare name (“agent_start”) to prevent blacklist bypass via remote proxy tool namespace prefix.

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/kairos_mcp/invocation_context.rb', line 140

def allowed?(tool_name)
  names = [tool_name]
  names << tool_name.split('/').last if tool_name.include?('/')

  if @blacklist
    return false if names.any? { |n| @blacklist.any? { |pat| File.fnmatch(pat, n) } }
  end
  if @whitelist
    return names.any? { |n| @whitelist.any? { |pat| File.fnmatch(pat, n) } }
  end
  true
end

#child(caller_tool:) ⇒ Object

Create a child context for a nested invocation. Inherits all policy from the parent; increments depth.

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/kairos_mcp/invocation_context.rb', line 38

def child(caller_tool:)
  raise DepthExceededError, "Max invocation depth (#{MAX_DEPTH}) exceeded" if @depth >= MAX_DEPTH

  self.class.new(
    depth: @depth + 1,
    caller_tool: caller_tool,
    mandate_id: @mandate_id,
    token_budget: @token_budget,
    whitelist: @whitelist&.dup,
    blacklist: @blacklist&.dup,
    root_invocation_id: @root_invocation_id,
    mode: @mode,
    idem_key: @idem_key
  )
end

#derive(blacklist_remove: [], blacklist_add: []) ⇒ Object

Derive a new context with modified blacklist, preserving all other fields. Used by agent ACT phase to selectively unblock autoexec tools. Does NOT increment depth — child() does that at invoke_tool time.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/kairos_mcp/invocation_context.rb', line 79

def derive(blacklist_remove: [], blacklist_add: [])
  new_blacklist = Array(@blacklist).dup
  blacklist_remove.each { |pat| new_blacklist.delete(pat) }
  blacklist_add.each { |pat| new_blacklist << pat unless new_blacklist.include?(pat) }

  self.class.new(
    depth: @depth,
    caller_tool: @caller_tool,
    mandate_id: @mandate_id,
    token_budget: @token_budget,
    whitelist: @whitelist&.dup,
    blacklist: new_blacklist.empty? ? nil : new_blacklist,
    root_invocation_id: @root_invocation_id,
    mode: @mode,
    idem_key: @idem_key
  )
end

#derive_for_phase(whitelist: nil, blacklist_add: []) ⇒ Object

Derive a phase-specific context with an optional whitelist and additional blacklist. Used by agent OODA phases to restrict tool access per phase (e.g., OBSERVE, ORIENT). Invariant: effective set = whitelist ∩ complement(parent_blacklist ∪ blacklist_add) Parent deny always takes precedence — a phase whitelist cannot override a parent blacklist. Does NOT increment depth — child() does that at invoke_tool time.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/kairos_mcp/invocation_context.rb', line 59

def derive_for_phase(whitelist: nil, blacklist_add: [])
  new_blacklist = Array(@blacklist).dup
  blacklist_add.each { |pat| new_blacklist << pat unless new_blacklist.include?(pat) }

  self.class.new(
    depth: @depth,
    caller_tool: @caller_tool,
    mandate_id: @mandate_id,
    token_budget: @token_budget,
    whitelist: whitelist ? whitelist.dup : @whitelist&.dup,
    blacklist: new_blacklist.empty? ? nil : new_blacklist,
    root_invocation_id: @root_invocation_id,
    mode: @mode,
    idem_key: @idem_key
  )
end

#to_hObject

Serialize to a plain Hash for passing through tool arguments. Only includes policy-relevant fields (whitelist, blacklist, mandate_id, token_budget).



99
100
101
102
103
104
105
106
107
108
# File 'lib/kairos_mcp/invocation_context.rb', line 99

def to_h
  {
    'whitelist' => @whitelist,
    'blacklist' => @blacklist,
    'mandate_id' => @mandate_id,
    'token_budget' => @token_budget,
    'mode' => @mode&.to_s,
    'idem_key' => @idem_key
  }
end

#to_json(*args) ⇒ Object



110
111
112
113
# File 'lib/kairos_mcp/invocation_context.rb', line 110

def to_json(*args)
  require 'json'
  to_h.to_json(*args)
end