pwn 0.5.685 → 0.5.686
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/documentation/General-PWN-Usage.md +1 -1
- data/documentation/pwn-REPL.md +1 -1
- data/lib/pwn/ai/agent/loop.rb +76 -13
- data/lib/pwn/ai/agent/prompt_builder.rb +3 -0
- data/lib/pwn/ai/anthropic.rb +0 -118
- data/lib/pwn/ai/gemini.rb +0 -15
- data/lib/pwn/ai/grok.rb +0 -158
- data/lib/pwn/ai/ollama.rb +0 -12
- data/lib/pwn/ai/open_ai.rb +0 -110
- data/lib/pwn/ai/open_web_ui.rb +0 -12
- data/lib/pwn/ai.rb +0 -86
- data/lib/pwn/plugins/log.rb +302 -0
- data/lib/pwn/plugins/repl.rb +28 -8
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +43 -5
- data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
- data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
- data/spec/lib/pwn/ai/grok_spec.rb +2 -2
- data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
- data/spec/lib/pwn/ai_spec.rb +4 -21
- data/spec/lib/pwn/plugins/log_spec.rb +139 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +31 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
- data/third_party/pwn_rdoc.jsonl +26 -20
- metadata +1 -1
data/lib/pwn/ai/open_ai.rb
CHANGED
|
@@ -544,114 +544,6 @@ module PWN
|
|
|
544
544
|
raise e
|
|
545
545
|
end
|
|
546
546
|
|
|
547
|
-
# Supported Method Parameters::
|
|
548
|
-
# usage = PWN::AI::OpenAI.get_plan_usage(
|
|
549
|
-
# timeout: 'optional - seconds (default 8)'
|
|
550
|
-
# )
|
|
551
|
-
#
|
|
552
|
-
# Subscription / billing usage for the PS1 percent suffix.
|
|
553
|
-
# Tries (1) dashboard billing hard-limit vs month-to-date spend,
|
|
554
|
-
# then (2) Admin organization costs. Returns {available:false}
|
|
555
|
-
# when neither yields used+limit (never prompts for credentials).
|
|
556
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
557
|
-
timeout = opts[:timeout] || 8
|
|
558
|
-
return { available: false, engine: :openai } unless plan_usage_credentials?
|
|
559
|
-
|
|
560
|
-
now = Time.now.utc
|
|
561
|
-
start_of_month = Time.utc(now.year, now.month, 1)
|
|
562
|
-
|
|
563
|
-
sub = parse_plan_usage_json(
|
|
564
|
-
raw: open_ai_rest_call(
|
|
565
|
-
rest_call: 'dashboard/billing/subscription',
|
|
566
|
-
timeout: timeout,
|
|
567
|
-
spinner: false,
|
|
568
|
-
quiet: true,
|
|
569
|
-
non_interactive: true
|
|
570
|
-
)
|
|
571
|
-
)
|
|
572
|
-
usage = parse_plan_usage_json(
|
|
573
|
-
raw: open_ai_rest_call(
|
|
574
|
-
rest_call: 'dashboard/billing/usage',
|
|
575
|
-
params: {
|
|
576
|
-
start_date: start_of_month.strftime('%Y-%m-%d'),
|
|
577
|
-
end_date: now.strftime('%Y-%m-%d')
|
|
578
|
-
},
|
|
579
|
-
timeout: timeout,
|
|
580
|
-
spinner: false,
|
|
581
|
-
quiet: true,
|
|
582
|
-
non_interactive: true
|
|
583
|
-
)
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
if sub.is_a?(Hash) && usage.is_a?(Hash)
|
|
587
|
-
limit_usd = (
|
|
588
|
-
sub[:hard_limit_usd] ||
|
|
589
|
-
sub[:system_hard_limit_usd] ||
|
|
590
|
-
sub[:soft_limit_usd] ||
|
|
591
|
-
sub[:system_soft_limit_usd]
|
|
592
|
-
).to_f
|
|
593
|
-
used_usd = usage[:total_usage].to_f / 100.0
|
|
594
|
-
normalized = PWN::AI.normalize_plan_usage(
|
|
595
|
-
used: used_usd,
|
|
596
|
-
limit: limit_usd,
|
|
597
|
-
source: 'dashboard/billing',
|
|
598
|
-
engine: :openai
|
|
599
|
-
)
|
|
600
|
-
return normalized if normalized[:available]
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
costs = parse_plan_usage_json(
|
|
604
|
-
raw: open_ai_rest_call(
|
|
605
|
-
rest_call: 'organization/costs',
|
|
606
|
-
params: { start_time: start_of_month.to_i },
|
|
607
|
-
timeout: timeout,
|
|
608
|
-
spinner: false,
|
|
609
|
-
quiet: true,
|
|
610
|
-
non_interactive: true
|
|
611
|
-
)
|
|
612
|
-
)
|
|
613
|
-
if costs.is_a?(Hash)
|
|
614
|
-
spent = Array(costs[:data]).sum do |row|
|
|
615
|
-
amt = row.is_a?(Hash) ? (row[:amount] || row[:results] || {}) : {}
|
|
616
|
-
if amt.is_a?(Hash)
|
|
617
|
-
(amt[:value] || amt[:amount] || 0).to_f
|
|
618
|
-
else
|
|
619
|
-
amt.to_f
|
|
620
|
-
end
|
|
621
|
-
end
|
|
622
|
-
limit_usd = sub.is_a?(Hash) ? (sub[:hard_limit_usd] || sub[:system_hard_limit_usd]).to_f : 0.0
|
|
623
|
-
normalized = PWN::AI.normalize_plan_usage(
|
|
624
|
-
used: spent,
|
|
625
|
-
limit: limit_usd,
|
|
626
|
-
source: 'organization/costs',
|
|
627
|
-
engine: :openai
|
|
628
|
-
)
|
|
629
|
-
return normalized if normalized[:available]
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
{ available: false, engine: :openai }
|
|
633
|
-
rescue StandardError
|
|
634
|
-
{ available: false, engine: :openai }
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
private_class_method def self.plan_usage_credentials?
|
|
638
|
-
engine = PWN::Env.dig(:ai, :openai) || {}
|
|
639
|
-
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
640
|
-
real_config_value?(value: engine[:key]) ||
|
|
641
|
-
real_config_value?(value: oauth[:bearer_token]) ||
|
|
642
|
-
real_config_value?(value: oauth[:refresh_token])
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
646
|
-
raw = opts[:raw]
|
|
647
|
-
return nil if raw.nil?
|
|
648
|
-
|
|
649
|
-
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
650
|
-
parsed.is_a?(Hash) ? parsed : nil
|
|
651
|
-
rescue JSON::ParserError, TypeError
|
|
652
|
-
nil
|
|
653
|
-
end
|
|
654
|
-
|
|
655
547
|
# Supported Method Parameters::
|
|
656
548
|
# response = PWN::AI::OpenAI.chat_with_tools(
|
|
657
549
|
# messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -1287,8 +1179,6 @@ module PWN
|
|
|
1287
1179
|
puts "USAGE:
|
|
1288
1180
|
models = #{self}.get_models
|
|
1289
1181
|
|
|
1290
|
-
usage = #{self}.get_plan_usage
|
|
1291
|
-
|
|
1292
1182
|
# One-time ChatGPT/Codex OAuth enrollment (device-code flow):
|
|
1293
1183
|
bearer = #{self}.obtain_oauth_bearer_token
|
|
1294
1184
|
# Subsequent runs silently refresh via ai.openai.oauth.refresh_token
|
data/lib/pwn/ai/open_web_ui.rb
CHANGED
|
@@ -413,16 +413,6 @@ module PWN
|
|
|
413
413
|
raise e
|
|
414
414
|
end
|
|
415
415
|
|
|
416
|
-
# Supported Method Parameters::
|
|
417
|
-
# usage = PWN::AI::OpenWebUI.get_plan_usage
|
|
418
|
-
#
|
|
419
|
-
# Open WebUI is a local gateway with no subscription / plan-usage
|
|
420
|
-
# endpoint. Always unavailable so the PS1 shows the infinity glyph.
|
|
421
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
422
|
-
_unused = opts
|
|
423
|
-
{ available: false, engine: :openwebui, unlimited: true }
|
|
424
|
-
end
|
|
425
|
-
|
|
426
416
|
# Coerce OpenAI-wire message history into Ollama-native shapes before
|
|
427
417
|
# POST /api/chat (or Open WebUI /ollama/api/chat):
|
|
428
418
|
# - function.arguments must be a Hash/Array object, not a JSON string
|
|
@@ -693,8 +683,6 @@ module PWN
|
|
|
693
683
|
puts "USAGE:
|
|
694
684
|
models = #{self}.get_models
|
|
695
685
|
|
|
696
|
-
usage = #{self}.get_plan_usage
|
|
697
|
-
|
|
698
686
|
response = #{self}.chat(
|
|
699
687
|
request: 'required - message to Open WebUI',
|
|
700
688
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:openwebui][:model])',
|
data/lib/pwn/ai.rb
CHANGED
|
@@ -14,92 +14,6 @@ module PWN
|
|
|
14
14
|
autoload :OpenWebUI, 'pwn/ai/open_web_ui'
|
|
15
15
|
autoload :RedTeam, 'pwn/ai/red_team'
|
|
16
16
|
|
|
17
|
-
PLAN_USAGE_CACHE_TTL = 60
|
|
18
|
-
PLAN_USAGE_INFINITY = "\u221E"
|
|
19
|
-
ENGINE_MODULES = {
|
|
20
|
-
openai: 'PWN::AI::OpenAI',
|
|
21
|
-
grok: 'PWN::AI::Grok',
|
|
22
|
-
ollama: 'PWN::AI::Ollama',
|
|
23
|
-
openwebui: 'PWN::AI::OpenWebUI',
|
|
24
|
-
anthropic: 'PWN::AI::Anthropic',
|
|
25
|
-
gemini: 'PWN::AI::Gemini'
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
|
-
# Supported Method Parameters::
|
|
29
|
-
# usage = PWN::AI.normalize_plan_usage(
|
|
30
|
-
# used: 'required - numeric used amount',
|
|
31
|
-
# limit: 'required - numeric limit / allowance',
|
|
32
|
-
# source: 'optional - endpoint / origin label',
|
|
33
|
-
# engine: 'optional - engine symbol'
|
|
34
|
-
# )
|
|
35
|
-
public_class_method def self.normalize_plan_usage(opts = {})
|
|
36
|
-
used = opts[:used]
|
|
37
|
-
limit = opts[:limit]
|
|
38
|
-
used_f = used&.to_f
|
|
39
|
-
limit_f = limit&.to_f
|
|
40
|
-
available = !used_f.nil? && !limit_f.nil? && limit_f.positive?
|
|
41
|
-
percent = available ? ((used_f / limit_f) * 100.0).round : nil
|
|
42
|
-
percent = 0 if percent && percent.negative?
|
|
43
|
-
percent = 100 if percent && percent > 100
|
|
44
|
-
{
|
|
45
|
-
available: available,
|
|
46
|
-
used: used_f,
|
|
47
|
-
limit: limit_f,
|
|
48
|
-
percent: percent,
|
|
49
|
-
source: opts[:source],
|
|
50
|
-
engine: opts[:engine]
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Supported Method Parameters::
|
|
55
|
-
# usage = PWN::AI.plan_usage(
|
|
56
|
-
# engine: 'optional - engine symbol (defaults to PWN::Env[:ai][:active])',
|
|
57
|
-
# ttl: 'optional - cache seconds (default 60)',
|
|
58
|
-
# force: 'optional - bypass cache (default false)'
|
|
59
|
-
# )
|
|
60
|
-
public_class_method def self.plan_usage(opts = {})
|
|
61
|
-
engine = (opts[:engine] || PWN::Env.dig(:ai, :active)).to_s.downcase.to_sym
|
|
62
|
-
ttl = (opts[:ttl] || PLAN_USAGE_CACHE_TTL).to_i
|
|
63
|
-
ttl = PLAN_USAGE_CACHE_TTL if ttl <= 0
|
|
64
|
-
cache = (@plan_usage_cache ||= {})
|
|
65
|
-
now = Time.now.to_i
|
|
66
|
-
hit = cache[engine]
|
|
67
|
-
return hit[:value] if !opts[:force] && hit.is_a?(Hash) && (now - hit[:at].to_i) < ttl
|
|
68
|
-
|
|
69
|
-
klass_name = ENGINE_MODULES[engine]
|
|
70
|
-
value = { available: false, engine: engine }
|
|
71
|
-
if klass_name
|
|
72
|
-
begin
|
|
73
|
-
klass = Object.const_get(klass_name)
|
|
74
|
-
value = klass.get_plan_usage if klass.respond_to?(:get_plan_usage)
|
|
75
|
-
value = { available: false, engine: engine } unless value.is_a?(Hash)
|
|
76
|
-
value[:engine] ||= engine
|
|
77
|
-
rescue StandardError
|
|
78
|
-
value = { available: false, engine: engine }
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
cache[engine] = { at: now, value: value }
|
|
82
|
-
value
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Supported Method Parameters::
|
|
86
|
-
# glyph = PWN::AI.plan_usage_glyph(
|
|
87
|
-
# engine: 'optional - engine symbol (defaults to PWN::Env[:ai][:active])',
|
|
88
|
-
# usage: 'optional - precomputed usage hash'
|
|
89
|
-
# )
|
|
90
|
-
#
|
|
91
|
-
# Returns "19%" when used/limit are known, otherwise the infinity glyph.
|
|
92
|
-
# Local engines (ollama / openwebui) always return infinity.
|
|
93
|
-
public_class_method def self.plan_usage_glyph(opts = {})
|
|
94
|
-
engine = (opts[:engine] || PWN::Env.dig(:ai, :active)).to_s.downcase.to_sym
|
|
95
|
-
return PLAN_USAGE_INFINITY if %i[ollama openwebui].include?(engine)
|
|
96
|
-
|
|
97
|
-
usage = opts[:usage] || plan_usage(engine: engine)
|
|
98
|
-
return PLAN_USAGE_INFINITY unless usage.is_a?(Hash) && usage[:available] && !usage[:percent].nil?
|
|
99
|
-
|
|
100
|
-
"#{usage[:percent].to_i}%"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
17
|
# Display a List of Every PWN::AI Module
|
|
104
18
|
|
|
105
19
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
data/lib/pwn/plugins/log.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
3
4
|
require 'json'
|
|
4
5
|
require 'logger'
|
|
5
6
|
require 'securerandom'
|
|
@@ -8,6 +9,35 @@ module PWN
|
|
|
8
9
|
module Plugins
|
|
9
10
|
# This plugin is used to instantiate a PWN logger with a custom message format
|
|
10
11
|
module Log
|
|
12
|
+
TRACE_SKIP_METHODS = %i[
|
|
13
|
+
authors help to_s inspect class object_id hash eql? == equal? !
|
|
14
|
+
method public_send send __send__ instance_eval class_eval
|
|
15
|
+
public_class_method private_class_method
|
|
16
|
+
].freeze
|
|
17
|
+
TRACE_SKIP_PREFIXES = %w[
|
|
18
|
+
PWN::Plugins::Log
|
|
19
|
+
PWN::Plugins::REPL
|
|
20
|
+
PWN::Banner
|
|
21
|
+
].freeze
|
|
22
|
+
DEFAULT_TRACE_PREFIXES = %w[
|
|
23
|
+
PWN::AI
|
|
24
|
+
PWN::Memory
|
|
25
|
+
PWN::Sessions
|
|
26
|
+
PWN::Config
|
|
27
|
+
PWN::Cron
|
|
28
|
+
PWN::Plugins
|
|
29
|
+
].freeze
|
|
30
|
+
SECRET_KEY_RX = /password|passwd|secret|token|api[_-]?key|authorization|bearer|cookie|session[_-]?id|private[_-]?key|decryptor|credential|ssh[_-]?key|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|vault|csrf/i
|
|
31
|
+
SECRET_VALUE_RX = %r{
|
|
32
|
+
-----BEGIN\ [A-Z ]*PRIVATE\ KEY----- |
|
|
33
|
+
Bearer\s+[A-Za-z0-9\-._~+/]+=* |
|
|
34
|
+
\beyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+ |
|
|
35
|
+
\b(?:sk|rk|pk|xai|xox[baprs]|ghp|gho|ghu|ghs|ghr|glpat|AKIA|ASIA|ya29)[-_][A-Za-z0-9\-_]{16,} |
|
|
36
|
+
\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*\S+
|
|
37
|
+
}ix
|
|
38
|
+
DEBUG_VALUE_MAX = 240
|
|
39
|
+
DEBUG_ARGS_MAX = 1_800
|
|
40
|
+
|
|
11
41
|
# Supported Method Parameters::
|
|
12
42
|
# PWN::Log.create(
|
|
13
43
|
# )
|
|
@@ -99,6 +129,271 @@ module PWN
|
|
|
99
129
|
raise e
|
|
100
130
|
end
|
|
101
131
|
|
|
132
|
+
public_class_method def self.debug_enabled?(opts = {})
|
|
133
|
+
return @debug_enabled == true if opts.is_a?(Hash)
|
|
134
|
+
|
|
135
|
+
@debug_enabled == true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
public_class_method def self.debug_log_path(opts = {})
|
|
139
|
+
return @debug_path if opts.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
@debug_path
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Open /tmp/pwn-ai-DEBUG-TIMESTAMP.log, tee the same lines to the TUI
|
|
145
|
+
# (opts[:tee]), and TracePoint PWN modules that process a pwn-ai request.
|
|
146
|
+
DEBUG_LOG_MAX = 1_024_000
|
|
147
|
+
|
|
148
|
+
public_class_method def self.start_debug(opts = {})
|
|
149
|
+
return @debug_path if debug_enabled? && opts.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
ts = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
152
|
+
path = opts[:path].to_s
|
|
153
|
+
path = "/tmp/pwn-ai-DEBUG-#{ts}.1.log" if path.empty?
|
|
154
|
+
stem, idx = debug_path_parts(path: path)
|
|
155
|
+
path = "#{stem}.#{idx}.log"
|
|
156
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
157
|
+
io = File.open(path, 'a')
|
|
158
|
+
io.sync = true
|
|
159
|
+
@debug_file = io
|
|
160
|
+
@debug_path = path
|
|
161
|
+
@debug_stem = stem
|
|
162
|
+
@debug_index = idx
|
|
163
|
+
@debug_tee = opts[:tee]
|
|
164
|
+
@debug_tui_quiet = false
|
|
165
|
+
@debug_enabled = true
|
|
166
|
+
start_trace!(prefixes: opts[:prefixes]) unless opts[:trace] == false
|
|
167
|
+
progress(msg: "debug session start path=#{path}", which_self: self)
|
|
168
|
+
path
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
public_class_method def self.stop_debug(opts = {})
|
|
172
|
+
reason = opts[:reason].to_s if opts.is_a?(Hash)
|
|
173
|
+
if debug_enabled?
|
|
174
|
+
tail = reason.to_s.empty? ? 'debug session stop' : "debug session stop reason=#{reason}"
|
|
175
|
+
progress(msg: tail, which_self: self)
|
|
176
|
+
end
|
|
177
|
+
stop_trace!
|
|
178
|
+
begin
|
|
179
|
+
@debug_file&.close unless @debug_file.nil? || @debug_file.closed?
|
|
180
|
+
rescue StandardError
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
path = @debug_path
|
|
184
|
+
@debug_file = nil
|
|
185
|
+
@debug_path = nil
|
|
186
|
+
@debug_stem = nil
|
|
187
|
+
@debug_index = nil
|
|
188
|
+
@debug_tee = nil
|
|
189
|
+
@debug_tui_quiet = false
|
|
190
|
+
@debug_enabled = false
|
|
191
|
+
path
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private_class_method def self.debug_path_parts(opts = {})
|
|
195
|
+
path = opts[:path].to_s
|
|
196
|
+
return [Regexp.last_match(1), Regexp.last_match(2).to_i] if path =~ /\A(.+)\.(\d+)\.log\z/
|
|
197
|
+
|
|
198
|
+
[path.sub(/\.log\z/, ''), 1]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private_class_method def self.roll_debug_log!(opts = {})
|
|
202
|
+
return if opts[:skip]
|
|
203
|
+
return if @debug_file.nil? || @debug_file.closed?
|
|
204
|
+
return if @debug_file.size < DEBUG_LOG_MAX
|
|
205
|
+
|
|
206
|
+
@debug_file.close
|
|
207
|
+
@debug_index = @debug_index.to_i + 1
|
|
208
|
+
path = "#{@debug_stem}.#{@debug_index}.log"
|
|
209
|
+
io = File.open(path, 'a')
|
|
210
|
+
io.sync = true
|
|
211
|
+
@debug_file = io
|
|
212
|
+
@debug_path = path
|
|
213
|
+
path
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
public_class_method def self.quiet_tui!(opts = {})
|
|
217
|
+
return if opts[:skip]
|
|
218
|
+
|
|
219
|
+
@debug_tui_quiet = true
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
public_class_method def self.loud_tui!(opts = {})
|
|
223
|
+
return if opts[:skip]
|
|
224
|
+
|
|
225
|
+
@debug_tui_quiet = false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# One progress line to the debug file and the TUI tee (same payload).
|
|
229
|
+
public_class_method def self.progress(opts = {})
|
|
230
|
+
return false unless debug_enabled?
|
|
231
|
+
return false if Thread.current[:pwn_log_progress]
|
|
232
|
+
|
|
233
|
+
Thread.current[:pwn_log_progress] = true
|
|
234
|
+
msg = sanitize_debug_text(text: opts[:msg].to_s)
|
|
235
|
+
which = opts[:which_self] || self
|
|
236
|
+
line = format_progress(msg: msg, which_self: which)
|
|
237
|
+
begin
|
|
238
|
+
@debug_file&.puts(line)
|
|
239
|
+
@debug_file&.flush
|
|
240
|
+
roll_debug_log!
|
|
241
|
+
rescue StandardError
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
tee = opts.key?(:tee) ? opts[:tee] : @debug_tee
|
|
245
|
+
if !@debug_tui_quiet && tee.respond_to?(:puts)
|
|
246
|
+
begin
|
|
247
|
+
tee.puts(color_progress(line: line))
|
|
248
|
+
rescue StandardError
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
true
|
|
253
|
+
ensure
|
|
254
|
+
Thread.current[:pwn_log_progress] = false
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private_class_method def self.format_progress(opts = {})
|
|
258
|
+
msg = opts[:msg].to_s.tr("\n", ' ')[0, 4_000]
|
|
259
|
+
which = opts[:which_self]
|
|
260
|
+
who = if which.is_a?(Module)
|
|
261
|
+
(which.name || which.to_s).to_s
|
|
262
|
+
else
|
|
263
|
+
which.to_s
|
|
264
|
+
end
|
|
265
|
+
ts = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L%z')
|
|
266
|
+
bits = ["[DEBUG #{ts}]"]
|
|
267
|
+
bits << who unless who.empty?
|
|
268
|
+
bits << msg unless msg.empty?
|
|
269
|
+
sanitize_debug_text(text: bits.join(' '))
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private_class_method def self.color_progress(opts = {})
|
|
273
|
+
line = opts[:line].to_s
|
|
274
|
+
"\001\e[35m\002#{line}\001\e[0m\002"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private_class_method def self.trace_module_name(opts = {})
|
|
278
|
+
tp = opts[:tp]
|
|
279
|
+
return '' unless tp
|
|
280
|
+
|
|
281
|
+
mod = tp.self.is_a?(Module) ? tp.self : tp.defined_class
|
|
282
|
+
name = if mod.respond_to?(:name) && mod.name
|
|
283
|
+
mod.name
|
|
284
|
+
else
|
|
285
|
+
mod.to_s
|
|
286
|
+
end
|
|
287
|
+
name.to_s.sub(/\A#<Class:(.+?)>\z/, '\1')
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
private_class_method def self.secret_key?(opts = {})
|
|
291
|
+
opts[:key].to_s.match?(SECRET_KEY_RX)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private_class_method def self.sanitize_debug_text(opts = {})
|
|
295
|
+
text = opts[:text].to_s
|
|
296
|
+
text = text.gsub(SECRET_VALUE_RX, '[REDACTED]')
|
|
297
|
+
text.gsub(/-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----/m, '[REDACTED]')
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
private_class_method def self.inspect_debug(opts = {})
|
|
301
|
+
val = opts[:value]
|
|
302
|
+
key = opts[:key]
|
|
303
|
+
return '[REDACTED]' if secret_key?(key: key)
|
|
304
|
+
return format_debug_hash(hash: val) if val.is_a?(Hash)
|
|
305
|
+
return '[REDACTED]' if val.is_a?(String) && val.match?(SECRET_VALUE_RX)
|
|
306
|
+
|
|
307
|
+
raw = sanitize_debug_text(text: val.inspect)
|
|
308
|
+
raw = "#{raw[0, DEBUG_VALUE_MAX]}…" if raw.length > DEBUG_VALUE_MAX
|
|
309
|
+
raw
|
|
310
|
+
rescue StandardError
|
|
311
|
+
"#<#{val.class}>"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private_class_method def self.format_debug_hash(opts = {})
|
|
315
|
+
hash = opts[:hash]
|
|
316
|
+
return '{}' unless hash.is_a?(Hash)
|
|
317
|
+
|
|
318
|
+
parts = hash.map do |k, v|
|
|
319
|
+
"#{k}: #{inspect_debug(value: v, key: k)}"
|
|
320
|
+
end
|
|
321
|
+
"{#{parts.join(', ')}}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
private_class_method def self.trace_arguments(opts = {})
|
|
325
|
+
tp = opts[:tp]
|
|
326
|
+
return '' unless tp
|
|
327
|
+
|
|
328
|
+
bind = tp.binding
|
|
329
|
+
parts = []
|
|
330
|
+
Array(tp.parameters).each do |kind, name|
|
|
331
|
+
next if name.nil?
|
|
332
|
+
next if %i[rest keyrest block].include?(kind)
|
|
333
|
+
next unless bind.local_variable_defined?(name)
|
|
334
|
+
|
|
335
|
+
val = bind.local_variable_get(name)
|
|
336
|
+
if name == :opts && val.is_a?(Hash)
|
|
337
|
+
rendered = format_debug_hash(hash: val)
|
|
338
|
+
return rendered == '{}' ? '' : rendered
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
parts << "#{name}=#{inspect_debug(value: val, key: name)}"
|
|
342
|
+
end
|
|
343
|
+
out = parts.join(', ')
|
|
344
|
+
out = "#{out[0, DEBUG_ARGS_MAX]}…" if out.length > DEBUG_ARGS_MAX
|
|
345
|
+
out
|
|
346
|
+
rescue StandardError
|
|
347
|
+
''
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
private_class_method def self.format_trace_call(opts = {})
|
|
351
|
+
tp = opts[:tp]
|
|
352
|
+
klass = trace_module_name(tp: tp)
|
|
353
|
+
meth = tp.method_id
|
|
354
|
+
args = trace_arguments(tp: tp)
|
|
355
|
+
args.to_s.empty? ? "#{klass}.#{meth}" : "#{klass}.#{meth}(#{args})"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
private_class_method def self.traceable?(opts = {})
|
|
359
|
+
tp = opts[:tp]
|
|
360
|
+
prefixes = opts[:prefixes] || DEFAULT_TRACE_PREFIXES
|
|
361
|
+
name = trace_module_name(tp: tp)
|
|
362
|
+
return false if name.empty?
|
|
363
|
+
return false if TRACE_SKIP_PREFIXES.any? { |p| name.start_with?(p) }
|
|
364
|
+
return false unless prefixes.any? { |p| name.start_with?(p) }
|
|
365
|
+
return false if TRACE_SKIP_METHODS.include?(tp.method_id)
|
|
366
|
+
|
|
367
|
+
true
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
public_class_method def self.start_trace!(opts = {})
|
|
371
|
+
return @debug_tp if @debug_tp && opts.is_a?(Hash)
|
|
372
|
+
|
|
373
|
+
prefixes = Array(opts[:prefixes] || DEFAULT_TRACE_PREFIXES).map(&:to_s)
|
|
374
|
+
prefixes = DEFAULT_TRACE_PREFIXES if prefixes.empty?
|
|
375
|
+
@debug_tp = TracePoint.new(:call) do |tp|
|
|
376
|
+
next unless debug_enabled?
|
|
377
|
+
next unless traceable?(tp: tp, prefixes: prefixes)
|
|
378
|
+
|
|
379
|
+
progress(
|
|
380
|
+
msg: format_trace_call(tp: tp),
|
|
381
|
+
which_self: ''
|
|
382
|
+
)
|
|
383
|
+
rescue StandardError
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
@debug_tp.enable
|
|
387
|
+
@debug_tp
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
public_class_method def self.stop_trace!(opts = {})
|
|
391
|
+
return if opts[:skip]
|
|
392
|
+
|
|
393
|
+
@debug_tp&.disable
|
|
394
|
+
@debug_tp = nil
|
|
395
|
+
end
|
|
396
|
+
|
|
102
397
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
103
398
|
|
|
104
399
|
public_class_method def self.authors
|
|
@@ -116,6 +411,13 @@ module PWN
|
|
|
116
411
|
msg: 'required - message to log',
|
|
117
412
|
which_self: 'required - pass in self object from module calling #{self}'
|
|
118
413
|
)
|
|
414
|
+
|
|
415
|
+
path = #{self}.start_debug(
|
|
416
|
+
tee: $stdout,
|
|
417
|
+
path: 'optional - defaults to /tmp/pwn-ai-DEBUG-TIMESTAMP.log'
|
|
418
|
+
)
|
|
419
|
+
#{self}.progress(msg: 'stage', which_self: self)
|
|
420
|
+
#{self}.stop_debug
|
|
119
421
|
"
|
|
120
422
|
end
|
|
121
423
|
end
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -375,11 +375,10 @@ module PWN
|
|
|
375
375
|
max_context = PWN::Env[:ai][engine][:max_prompt_length].to_i
|
|
376
376
|
current_context_length = "#{PWN::Plugins::REPL.compact_context_tokens(tokens: used_tokens)}:" \
|
|
377
377
|
"#{PWN::Plugins::REPL.compact_context_tokens(tokens: max_context)}"
|
|
378
|
-
plan_usage_glyph = PWN::AI.plan_usage_glyph(engine: engine)
|
|
379
378
|
|
|
380
379
|
pname = "pwn.ai:#{engine}"
|
|
381
|
-
pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}
|
|
382
|
-
pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}
|
|
380
|
+
pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}" if model
|
|
381
|
+
pname = "pwn.ai:#{engine}/#{model}/#{current_context_length}.SPEAK" if pi.config.pwn_ai_speak
|
|
383
382
|
pi.config.prompt_name = pname
|
|
384
383
|
|
|
385
384
|
name = "\001\e[1m\002\001\e[33m\002#{pi.config.prompt_name}\001\e[0m\002"
|
|
@@ -1045,11 +1044,23 @@ module PWN
|
|
|
1045
1044
|
end
|
|
1046
1045
|
|
|
1047
1046
|
Pry::Commands.create_command 'toggle-pwn-ai-debug' do
|
|
1048
|
-
description '
|
|
1047
|
+
description 'Stream pwn-ai module progress to the TUI and /tmp/pwn-ai-DEBUG-TIMESTAMP.log'
|
|
1049
1048
|
|
|
1050
1049
|
def process
|
|
1051
1050
|
pi = pry_instance
|
|
1052
|
-
|
|
1051
|
+
if pi.config.pwn_ai_debug
|
|
1052
|
+
path = PWN::Plugins::Log.stop_debug
|
|
1053
|
+
pi.config.pwn_ai_debug = false
|
|
1054
|
+
if path
|
|
1055
|
+
output.puts "pwn-ai debug OFF (was #{path})"
|
|
1056
|
+
else
|
|
1057
|
+
output.puts 'pwn-ai debug OFF'
|
|
1058
|
+
end
|
|
1059
|
+
else
|
|
1060
|
+
path = PWN::Plugins::Log.start_debug(tee: output)
|
|
1061
|
+
pi.config.pwn_ai_debug = true
|
|
1062
|
+
output.puts "pwn-ai debug ON → #{path}"
|
|
1063
|
+
end
|
|
1053
1064
|
end
|
|
1054
1065
|
end
|
|
1055
1066
|
|
|
@@ -1071,7 +1082,7 @@ module PWN
|
|
|
1071
1082
|
pi.config.pwn_asm = false if pi.config.pwn_asm
|
|
1072
1083
|
pi.config.pwn_ai = false if pi.config.pwn_ai
|
|
1073
1084
|
pi.config.pwn_ai_agent = false if pi.config.pwn_ai_agent
|
|
1074
|
-
pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
|
|
1085
|
+
# pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
|
|
1075
1086
|
pi.config.pwn_ai_speak = false if pi.config.pwn_ai_speak
|
|
1076
1087
|
pi.config.completer = Pry::InputCompleter
|
|
1077
1088
|
# pi.config.pwn_ai_original_input ||= Pry.config.input.clone
|
|
@@ -1215,10 +1226,19 @@ module PWN
|
|
|
1215
1226
|
request: orig_request,
|
|
1216
1227
|
session_id: sess_id,
|
|
1217
1228
|
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
1218
|
-
on_tool: on_tool
|
|
1229
|
+
on_tool: on_tool,
|
|
1230
|
+
debug: pi.config.pwn_ai_debug,
|
|
1231
|
+
debug_tee: $stdout
|
|
1219
1232
|
)
|
|
1233
|
+
$stdout.flush
|
|
1220
1234
|
puts "\n\001\e[32m\002#{final}\001\e[0m\002\n\n"
|
|
1221
|
-
|
|
1235
|
+
$stdout.flush
|
|
1236
|
+
if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
|
|
1237
|
+
PWN::Plugins::Log.progress(
|
|
1238
|
+
msg: "session=#{sess_id}",
|
|
1239
|
+
which_self: PWN::Sessions
|
|
1240
|
+
)
|
|
1241
|
+
end
|
|
1222
1242
|
request.replace('nil')
|
|
1223
1243
|
next
|
|
1224
1244
|
rescue StandardError => e
|
data/lib/pwn/version.rb
CHANGED