Module: PWN::AI::Agent::ToolGuard

Defined in:
lib/pwn/ai/agent/tool_guard.rb

Overview

Shared pre-dispatch guards for the two high-volume runtime tools (shell / pwn_eval). Rejects placeholder payloads, aliases wrong schema keys, shares the recon authorization gate, and names the shell that will actually run the command.

Constant Summary collapse

ALIASES =
{
  'command' => %w[value cmd input],
  'code' => %w[value source ruby input],
  'query' => %w[value q text]
}.freeze
PLACEHOLDER_RX =

Token-level junk the model keeps emitting instead of a real command.

/
  \A\s*(?:\.{3}|…|\{\s*\.{3}\s*\}|\{\s*…\s*\}|<\.{3}>)\s*\z
  |(?:^|[\s;|&])(?:\.{3}|…|\{\s*\.{3}\s*\}|\{\s*…\s*\})(?:$|[\s;|&])
/x
BASHISM_RX =

Conservative bash-only constructs. POSIX $(()) is allowed.

/
  \bPIPESTATUS\b
  |\[\[(?:\s|\z)
  |(?:^|[\s;|&])source\s+\S
  |<\([^)]
  |&>
/x
RECON_RX =
%r{
  (?:^|[;&|\s])(?:sudo\s+)?hping3?(?:\s|$).*(?:-1|--icmp|--flood|/[0-9]{1,2}|seq\s+|for\s+)
  |(?:^|[;&|\s])(?:sudo\s+)?nmap\b[^\n]*(?:-sn|-sP|-PE|-PP|-PM)[^\n]*(?:/[0-9]{1,2}|\d+\.\d+\.\d+\.\d+)
  |(?:^|[;&|\s])(?:sudo\s+)?masscan\b
  |(?:^|[;&|\s])(?:sudo\s+)?nping\b
  |(?:^|[;&|\s])(?:sudo\s+)?fping\b[^\n]*/
  |for\s+\w+\s+in\s+\$?\(?seq[^)]*\)?[^;]*;\s*do\s[^;]*(?:hping|ping\s+-c|nmap)
  |PWN::Plugins::NmapIt
  |\bNmapIt\.(?:scan|new|help)
}ix

Class Method Summary collapse

Class Method Details

.authorsObject



141
142
143
# File 'lib/pwn/ai/agent/tool_guard.rb', line 141

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.bashism?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


54
55
56
57
58
# File 'lib/pwn/ai/agent/tool_guard.rb', line 54

public_class_method def self.bashism?(opts = {})
  BASHISM_RX.match?(opts[:text].to_s)
rescue StandardError
  false
end

.coerce_args(opts = {}) ⇒ Object

Coerce common wrong keys onto the first required schema field. Returns the args hash; sets :__schema_error when still missing.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/pwn/ai/agent/tool_guard.rb', line 90

public_class_method def self.coerce_args(opts = {})
  args = (opts[:args] || {}).dup
  args = args.each_with_object({}) { |(k, v), m| m[k.to_sym] = v } unless args.empty?
  req = Array(opts[:required]).map(&:to_s)
  req.each do |key|
    next if present?(value: args[key.to_sym])

    hit = Array(ALIASES[key]).find { |a| present?(value: args[a.to_sym]) }
    args[key.to_sym] = args[hit.to_sym] if hit
  end
  missing = req.reject { |k| present?(value: args[k.to_sym]) }
  unless missing.empty?
    args[:__schema_error] = "missing required #{missing.join(', ')}"
    args[:__expected] = req
    args[:__schema_hint] =
      "Expected keys: #{req.join(', ')}. " \
      'Do not send value/placeholder/ellipsis. ' \
      'Example: shell(command="uname -r") or pwn_eval(code="1+1").'
  end
  args
rescue StandardError
  opts[:args] || {}
end

.helpObject



145
146
147
148
149
150
151
152
153
# File 'lib/pwn/ai/agent/tool_guard.rb', line 145

public_class_method def self.help
  puts <<~USAGE
    USAGE:
      PWN::AI::Agent::ToolGuard.placeholder?(text: '...')
      PWN::AI::Agent::ToolGuard.recon_authorized?
      PWN::AI::Agent::ToolGuard.coerce_args(args: { value: 'id' }, required: %w[command])
      #{self}.authors
  USAGE
end

.invalid_payload(opts = {}) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/pwn/ai/agent/tool_guard.rb', line 114

public_class_method def self.invalid_payload(opts = {})
  hint = opts[:hint].to_s
  {
    stdout: '',
    stderr: hint,
    exit: 2,
    error: 'invalid_payload',
    hint: hint,
    shell: opts[:shell] || shell_name
  }
end

.placeholder?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


48
49
50
51
52
# File 'lib/pwn/ai/agent/tool_guard.rb', line 48

public_class_method def self.placeholder?(opts = {})
  PLACEHOLDER_RX.match?(opts[:text].to_s)
rescue StandardError
  false
end

.present?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


43
44
45
46
# File 'lib/pwn/ai/agent/tool_guard.rb', line 43

public_class_method def self.present?(opts = {})
  value = opts.is_a?(Hash) ? opts[:value] : opts
  !value.nil? && !value.to_s.strip.empty?
end

.recon_authorized?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


66
67
68
69
70
71
72
73
74
75
# File 'lib/pwn/ai/agent/tool_guard.rb', line 66

public_class_method def self.recon_authorized?(opts = {})
  return true if Thread.current[:pwn_recon_authorized] == true
  return true if defined?(Loop) && Loop.respond_to?(:recon_authorized?) &&
                 Loop.recon_authorized?(request: opts[:request].to_s)

  v = (PWN::Env.dig(:ai, :agent, :recon_authorized) if defined?(PWN::Env))
  v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
rescue StandardError
  false
end

.recon_blocked(opts = {}) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/pwn/ai/agent/tool_guard.rb', line 126

public_class_method def self.recon_blocked(opts = {})
  refused = opts[:text].to_s[0, 200]
  msg = '[pwn-ai/guard] blocked unauthorized recon/sweep. ' \
        'Need explicit in-scope authorization on the user request or ' \
        'PWN::Env[:ai][:agent][:recon_authorized]=true. ' \
        "Refused: #{refused}"
  {
    stdout: '',
    stderr: msg,
    exit: 126,
    error: 'unauthorized_recon_blocked',
    hint: msg
  }
end

.recon_text?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
# File 'lib/pwn/ai/agent/tool_guard.rb', line 60

public_class_method def self.recon_text?(opts = {})
  RECON_RX.match?(opts[:text].to_s)
rescue StandardError
  false
end

.shell_bash?Boolean

Returns:

  • (Boolean)


77
78
79
80
81
82
# File 'lib/pwn/ai/agent/tool_guard.rb', line 77

public_class_method def self.shell_bash?
  v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
  v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
rescue StandardError
  false
end

.shell_nameObject



84
85
86
# File 'lib/pwn/ai/agent/tool_guard.rb', line 84

public_class_method def self.shell_name
  shell_bash? ? 'bash -lc' : '/bin/sh'
end