pwn 0.5.684 → 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/README.md +6 -0
- data/documentation/Agent-Tool-Registry.md +1 -3
- data/documentation/Configuration.md +0 -2
- data/documentation/General-PWN-Usage.md +1 -1
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +2 -2
- data/documentation/Installation.md +18 -2
- data/documentation/Persistence.md +1 -1
- data/documentation/Session-Workflow.md +8 -28
- data/documentation/Skills-Memory-Learning.md +20 -0
- data/documentation/Troubleshooting.md +1 -1
- data/documentation/Why-PWN.md +1 -1
- data/documentation/diagrams/dot/memory-skills-detailed.dot +1 -1
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
- data/documentation/diagrams/dot/persistence-filesystem.dot +1 -1
- data/documentation/diagrams/dot/sessions-cron-automation.dot +1 -1
- data/documentation/diagrams/memory-skills-detailed.svg +85 -84
- data/documentation/diagrams/overall-pwn-architecture.svg +15 -15
- data/documentation/diagrams/persistence-filesystem.svg +1 -1
- data/documentation/diagrams/sessions-cron-automation.svg +7 -6
- data/documentation/pwn-REPL.md +1 -1
- data/documentation/pwn-ai-Agent.md +4 -7
- data/etc/default_skills/bug-bounty-hunting/SKILL.md +70 -0
- data/etc/default_skills/deep-exploitation/SKILL.md +74 -0
- data/etc/default_skills/hardware-and-firmware-testing/SKILL.md +83 -0
- data/etc/default_skills/osint/SKILL.md +147 -0
- data/etc/default_skills/penetration-testing/SKILL.md +81 -0
- data/etc/default_skills/red-teaming/SKILL.md +70 -0
- data/etc/default_skills/reverse-engineering-binaries/SKILL.md +70 -0
- data/etc/default_skills/sast-code-scans/SKILL.md +66 -0
- data/etc/default_skills/social-engineering/SKILL.md +67 -0
- data/etc/default_skills/vulnerability-research-fundamentals/SKILL.md +74 -0
- data/etc/default_skills/web-application-penetration-testing/SKILL.md +68 -0
- data/lib/pwn/ai/agent/loop.rb +79 -41
- data/lib/pwn/ai/agent/mistakes.rb +6 -6
- data/lib/pwn/ai/agent/prompt_builder.rb +3 -4
- data/lib/pwn/ai/agent/tool_guard.rb +1 -46
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +0 -10
- data/lib/pwn/ai/agent/tools/shell.rb +0 -6
- 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/config.rb +62 -1
- data/lib/pwn/migrate.rb +2 -0
- 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/integration/config_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +52 -12
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +7 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +5 -8
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -18
- 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 +29 -24
- metadata +12 -2
- data/documentation/PWN_Contributors_and_Users.png +0 -0
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -490,122 +490,6 @@ module PWN
|
|
|
490
490
|
raise e
|
|
491
491
|
end
|
|
492
492
|
|
|
493
|
-
# Supported Method Parameters::
|
|
494
|
-
# usage = PWN::AI::Anthropic.get_plan_usage(
|
|
495
|
-
# timeout: 'optional - seconds (default 8)'
|
|
496
|
-
# )
|
|
497
|
-
#
|
|
498
|
-
# Admin Usage API (organizations/usage_report/messages) yields tokens
|
|
499
|
-
# used; organizations/rate_limits yields the configured ceiling so the
|
|
500
|
-
# PS1 can show a percent. Individual accounts without an Admin key
|
|
501
|
-
# return {available:false}. Never prompts for credentials.
|
|
502
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
503
|
-
timeout = opts[:timeout] || 8
|
|
504
|
-
return { available: false, engine: :anthropic } unless plan_usage_credentials?
|
|
505
|
-
|
|
506
|
-
now = Time.now.utc
|
|
507
|
-
start_of_month = Time.utc(now.year, now.month, 1)
|
|
508
|
-
report = parse_plan_usage_json(
|
|
509
|
-
raw: anthropic_rest_call(
|
|
510
|
-
rest_call: 'organizations/usage_report/messages',
|
|
511
|
-
params: {
|
|
512
|
-
starting_at: start_of_month.strftime('%Y-%m-%dT00:00:00Z'),
|
|
513
|
-
ending_at: now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
514
|
-
bucket_width: '1d'
|
|
515
|
-
},
|
|
516
|
-
timeout: timeout,
|
|
517
|
-
spinner: false,
|
|
518
|
-
non_interactive: true
|
|
519
|
-
)
|
|
520
|
-
)
|
|
521
|
-
used_tokens = sum_usage_tokens(report: report)
|
|
522
|
-
|
|
523
|
-
limits = parse_plan_usage_json(
|
|
524
|
-
raw: anthropic_rest_call(
|
|
525
|
-
rest_call: 'organizations/rate_limits',
|
|
526
|
-
timeout: timeout,
|
|
527
|
-
spinner: false,
|
|
528
|
-
non_interactive: true
|
|
529
|
-
)
|
|
530
|
-
)
|
|
531
|
-
limit_tokens = first_rate_limit(limits: limits)
|
|
532
|
-
|
|
533
|
-
normalized = PWN::AI.normalize_plan_usage(
|
|
534
|
-
used: used_tokens,
|
|
535
|
-
limit: limit_tokens,
|
|
536
|
-
source: 'organizations/usage_report',
|
|
537
|
-
engine: :anthropic
|
|
538
|
-
)
|
|
539
|
-
return normalized if normalized[:available]
|
|
540
|
-
|
|
541
|
-
{ available: false, engine: :anthropic }
|
|
542
|
-
rescue StandardError
|
|
543
|
-
{ available: false, engine: :anthropic }
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
private_class_method def self.plan_usage_credentials?
|
|
547
|
-
engine = PWN::Env.dig(:ai, :anthropic) || {}
|
|
548
|
-
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
549
|
-
real_config_value?(value: engine[:key]) ||
|
|
550
|
-
real_config_value?(value: oauth[:bearer_token]) ||
|
|
551
|
-
real_config_value?(value: oauth[:refresh_token])
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
555
|
-
raw = opts[:raw]
|
|
556
|
-
return nil if raw.nil?
|
|
557
|
-
|
|
558
|
-
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
559
|
-
parsed.is_a?(Hash) ? parsed : nil
|
|
560
|
-
rescue JSON::ParserError, TypeError
|
|
561
|
-
nil
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
private_class_method def self.sum_usage_tokens(opts = {})
|
|
565
|
-
report = opts[:report]
|
|
566
|
-
return nil unless report.is_a?(Hash)
|
|
567
|
-
|
|
568
|
-
buckets = report[:data] || report[:usage] || []
|
|
569
|
-
total = 0
|
|
570
|
-
Array(buckets).each do |bucket|
|
|
571
|
-
next unless bucket.is_a?(Hash)
|
|
572
|
-
|
|
573
|
-
results = bucket[:results] || bucket[:usage] || [bucket]
|
|
574
|
-
Array(results).each do |row|
|
|
575
|
-
next unless row.is_a?(Hash)
|
|
576
|
-
|
|
577
|
-
%i[
|
|
578
|
-
uncached_input_tokens
|
|
579
|
-
input_tokens
|
|
580
|
-
cache_read_input_tokens
|
|
581
|
-
cache_creation_input_tokens
|
|
582
|
-
output_tokens
|
|
583
|
-
].each { |k| total += row[k].to_i }
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
total.positive? ? total : nil
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
private_class_method def self.first_rate_limit(opts = {})
|
|
590
|
-
limits = opts[:limits]
|
|
591
|
-
return nil unless limits.is_a?(Hash)
|
|
592
|
-
|
|
593
|
-
Array(limits[:data]).each do |group|
|
|
594
|
-
next unless group.is_a?(Hash)
|
|
595
|
-
|
|
596
|
-
Array(group[:limits]).each do |lim|
|
|
597
|
-
next unless lim.is_a?(Hash)
|
|
598
|
-
|
|
599
|
-
type = lim[:type].to_s
|
|
600
|
-
next unless type.match?(/tokens_per|input_tokens|output_tokens/)
|
|
601
|
-
|
|
602
|
-
v = lim[:value].to_f
|
|
603
|
-
return v if v.positive?
|
|
604
|
-
end
|
|
605
|
-
end
|
|
606
|
-
nil
|
|
607
|
-
end
|
|
608
|
-
|
|
609
493
|
# ----------------------------------------------------------------------
|
|
610
494
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
611
495
|
#
|
|
@@ -960,8 +844,6 @@ module PWN
|
|
|
960
844
|
puts "USAGE:
|
|
961
845
|
models = #{self}.get_models
|
|
962
846
|
|
|
963
|
-
usage = #{self}.get_plan_usage
|
|
964
|
-
|
|
965
847
|
# One-time Claude Pro/Max OAuth enrollment (PKCE paste flow):
|
|
966
848
|
bearer = #{self}.obtain_oauth_bearer_token
|
|
967
849
|
# Subsequent runs silently refresh via ai.anthropic.oauth.refresh_token
|
data/lib/pwn/ai/gemini.rb
CHANGED
|
@@ -114,19 +114,6 @@ module PWN
|
|
|
114
114
|
raise e
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
# Supported Method Parameters::
|
|
118
|
-
# usage = PWN::AI::Gemini.get_plan_usage(
|
|
119
|
-
# timeout: 'optional - seconds (default 8)'
|
|
120
|
-
# )
|
|
121
|
-
#
|
|
122
|
-
# Gemini Generative Language has no subscription-usage endpoint that
|
|
123
|
-
# returns used-vs-limit for an API key. Cached-miss / unavailable so
|
|
124
|
-
# the PS1 shows ∞ unless the operator later injects used/limit.
|
|
125
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
126
|
-
_timeout = opts[:timeout] || 8
|
|
127
|
-
{ available: false, engine: :gemini }
|
|
128
|
-
end
|
|
129
|
-
|
|
130
117
|
# ----------------------------------------------------------------------
|
|
131
118
|
# Native tool-calling adapter for PWN::AI::Agent::Loop.
|
|
132
119
|
#
|
|
@@ -449,8 +436,6 @@ module PWN
|
|
|
449
436
|
puts "USAGE:
|
|
450
437
|
models = #{self}.get_models
|
|
451
438
|
|
|
452
|
-
usage = #{self}.get_plan_usage
|
|
453
|
-
|
|
454
439
|
response = #{self}.chat(
|
|
455
440
|
request: 'required - message to Gemini',
|
|
456
441
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:gemini][:model])',
|
data/lib/pwn/ai/grok.rb
CHANGED
|
@@ -505,162 +505,6 @@ module PWN
|
|
|
505
505
|
raise e
|
|
506
506
|
end
|
|
507
507
|
|
|
508
|
-
# Supported Method Parameters::
|
|
509
|
-
# usage = PWN::AI::Grok.get_plan_usage(
|
|
510
|
-
# timeout: 'optional - seconds (default 8)'
|
|
511
|
-
# )
|
|
512
|
-
#
|
|
513
|
-
# Subscription / team usage for the PS1 percent suffix.
|
|
514
|
-
# GET /v1/api-key yields team_id; then billing preview /
|
|
515
|
-
# spending-limits / prepaid balance compute used vs limit.
|
|
516
|
-
# GET /v1/me is also tried (OAuth personal-team payload).
|
|
517
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
518
|
-
timeout = opts[:timeout] || 8
|
|
519
|
-
return { available: false, engine: :grok } unless plan_usage_credentials?
|
|
520
|
-
|
|
521
|
-
me = parse_plan_usage_json(
|
|
522
|
-
raw: grok_rest_call(rest_call: 'me', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
|
|
523
|
-
)
|
|
524
|
-
if me.is_a?(Hash)
|
|
525
|
-
used = first_numeric(src: me, keys: %i[used usage spend spent used_usd usage_usd])
|
|
526
|
-
limit = first_numeric(src: me, keys: %i[limit quota allowance hard_limit spending_limit])
|
|
527
|
-
normalized = PWN::AI.normalize_plan_usage(
|
|
528
|
-
used: used,
|
|
529
|
-
limit: limit,
|
|
530
|
-
source: 'me',
|
|
531
|
-
engine: :grok
|
|
532
|
-
)
|
|
533
|
-
return normalized if normalized[:available]
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
key_info = parse_plan_usage_json(
|
|
537
|
-
raw: grok_rest_call(rest_call: 'api-key', timeout: timeout, spinner: false, quiet: true, non_interactive: true)
|
|
538
|
-
)
|
|
539
|
-
team_id = key_info.is_a?(Hash) ? key_info[:team_id] : nil
|
|
540
|
-
team_id ||= me.is_a?(Hash) ? (me[:team_id] || me.dig(:team, :id)) : nil
|
|
541
|
-
|
|
542
|
-
if team_id.to_s.strip != ''
|
|
543
|
-
preview = parse_plan_usage_json(
|
|
544
|
-
raw: grok_rest_call(
|
|
545
|
-
rest_call: "billing/teams/#{team_id}/postpaid/invoice/preview",
|
|
546
|
-
timeout: timeout,
|
|
547
|
-
spinner: false,
|
|
548
|
-
quiet: true,
|
|
549
|
-
non_interactive: true
|
|
550
|
-
)
|
|
551
|
-
)
|
|
552
|
-
limits = parse_plan_usage_json(
|
|
553
|
-
raw: grok_rest_call(
|
|
554
|
-
rest_call: "billing/teams/#{team_id}/postpaid/spending-limits",
|
|
555
|
-
timeout: timeout,
|
|
556
|
-
spinner: false,
|
|
557
|
-
quiet: true,
|
|
558
|
-
non_interactive: true
|
|
559
|
-
)
|
|
560
|
-
)
|
|
561
|
-
balance = parse_plan_usage_json(
|
|
562
|
-
raw: grok_rest_call(
|
|
563
|
-
rest_call: "billing/teams/#{team_id}/prepaid/balance",
|
|
564
|
-
timeout: timeout,
|
|
565
|
-
spinner: false,
|
|
566
|
-
quiet: true,
|
|
567
|
-
non_interactive: true
|
|
568
|
-
)
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
used = nil
|
|
572
|
-
limit = nil
|
|
573
|
-
if preview.is_a?(Hash)
|
|
574
|
-
core = preview[:coreInvoice] || preview[:core_invoice] || {}
|
|
575
|
-
used_cents = cents_val(src: core, key: :prepaidCreditsUsed) ||
|
|
576
|
-
cents_val(src: core, key: :prepaid_credits_used) ||
|
|
577
|
-
core[:amountAfterVat] ||
|
|
578
|
-
core[:amount_after_vat]
|
|
579
|
-
prepaid_cents = cents_val(src: core, key: :prepaidCredits) ||
|
|
580
|
-
cents_val(src: core, key: :prepaid_credits)
|
|
581
|
-
spend_limit_cents = preview[:effectiveSpendingLimit] || preview[:effective_spending_limit]
|
|
582
|
-
used = to_f_or_nil(value: used_cents)
|
|
583
|
-
limit = to_f_or_nil(value: spend_limit_cents)
|
|
584
|
-
limit = to_f_or_nil(value: prepaid_cents).abs if (limit.nil? || limit <= 0) && prepaid_cents
|
|
585
|
-
end
|
|
586
|
-
used = to_f_or_nil(value: cents_val(src: balance, key: :total)).to_f.abs if (used.nil? || used <= 0) && balance.is_a?(Hash)
|
|
587
|
-
if (limit.nil? || limit <= 0) && limits.is_a?(Hash)
|
|
588
|
-
sl = limits[:spendingLimits] || limits[:spending_limits] || {}
|
|
589
|
-
limit = to_f_or_nil(value: cents_val(src: sl, key: :effectiveSl)) ||
|
|
590
|
-
to_f_or_nil(value: cents_val(src: sl, key: :effectiveHardSl)) ||
|
|
591
|
-
to_f_or_nil(value: cents_val(src: sl, key: :softSl))
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
normalized = PWN::AI.normalize_plan_usage(
|
|
595
|
-
used: used,
|
|
596
|
-
limit: limit,
|
|
597
|
-
source: 'billing/teams',
|
|
598
|
-
engine: :grok
|
|
599
|
-
)
|
|
600
|
-
return normalized if normalized[:available]
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
{ available: false, engine: :grok }
|
|
604
|
-
rescue StandardError
|
|
605
|
-
{ available: false, engine: :grok }
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
private_class_method def self.plan_usage_credentials?
|
|
609
|
-
engine = PWN::Env.dig(:ai, :grok) || {}
|
|
610
|
-
oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
|
|
611
|
-
real_config_value?(value: engine[:key]) ||
|
|
612
|
-
real_config_value?(value: oauth[:bearer_token]) ||
|
|
613
|
-
real_config_value?(value: oauth[:refresh_token])
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
private_class_method def self.parse_plan_usage_json(opts = {})
|
|
617
|
-
raw = opts[:raw]
|
|
618
|
-
return nil if raw.nil?
|
|
619
|
-
|
|
620
|
-
parsed = JSON.parse(raw.to_s, symbolize_names: true)
|
|
621
|
-
parsed.is_a?(Hash) ? parsed : nil
|
|
622
|
-
rescue JSON::ParserError, TypeError
|
|
623
|
-
nil
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
private_class_method def self.cents_val(opts = {})
|
|
627
|
-
src = opts[:src]
|
|
628
|
-
key = opts[:key]
|
|
629
|
-
return nil unless src.is_a?(Hash)
|
|
630
|
-
|
|
631
|
-
v = src[key]
|
|
632
|
-
return v[:val] if v.is_a?(Hash) && v.key?(:val)
|
|
633
|
-
return v['val'] if v.is_a?(Hash) && v.key?('val')
|
|
634
|
-
|
|
635
|
-
v
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
private_class_method def self.to_f_or_nil(opts = {})
|
|
639
|
-
val = opts.is_a?(Hash) && opts.key?(:value) ? opts[:value] : opts
|
|
640
|
-
return nil if val.nil?
|
|
641
|
-
return val.to_f if val.respond_to?(:to_f)
|
|
642
|
-
|
|
643
|
-
nil
|
|
644
|
-
rescue StandardError
|
|
645
|
-
nil
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
private_class_method def self.first_numeric(opts = {})
|
|
649
|
-
src = opts[:src]
|
|
650
|
-
keys = Array(opts[:keys])
|
|
651
|
-
return nil unless src.is_a?(Hash)
|
|
652
|
-
|
|
653
|
-
keys.each do |k|
|
|
654
|
-
v = src[k]
|
|
655
|
-
next if v.nil?
|
|
656
|
-
next if v.is_a?(Hash)
|
|
657
|
-
|
|
658
|
-
f = v.to_f
|
|
659
|
-
return f if f.positive? || v.to_s.strip.match?(/\A-?\d/)
|
|
660
|
-
end
|
|
661
|
-
nil
|
|
662
|
-
end
|
|
663
|
-
|
|
664
508
|
# Supported Method Parameters::
|
|
665
509
|
# response = PWN::AI::Grok.chat_with_tools(
|
|
666
510
|
# messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
|
|
@@ -845,8 +689,6 @@ module PWN
|
|
|
845
689
|
puts "USAGE:
|
|
846
690
|
models = #{self}.get_models
|
|
847
691
|
|
|
848
|
-
usage = #{self}.get_plan_usage
|
|
849
|
-
|
|
850
692
|
response = #{self}.chat(
|
|
851
693
|
request: 'required - message to Grok',
|
|
852
694
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:grok][:model])',
|
data/lib/pwn/ai/ollama.rb
CHANGED
|
@@ -370,16 +370,6 @@ module PWN
|
|
|
370
370
|
raise e
|
|
371
371
|
end
|
|
372
372
|
|
|
373
|
-
# Supported Method Parameters::
|
|
374
|
-
# usage = PWN::AI::Ollama.get_plan_usage
|
|
375
|
-
#
|
|
376
|
-
# Local Ollama has no subscription / plan-usage endpoint. Always
|
|
377
|
-
# unavailable so the PS1 shows the infinity glyph.
|
|
378
|
-
public_class_method def self.get_plan_usage(opts = {})
|
|
379
|
-
_unused = opts
|
|
380
|
-
{ available: false, engine: :ollama, unlimited: true }
|
|
381
|
-
end
|
|
382
|
-
|
|
383
373
|
# Coerce OpenAI-wire message history into Ollama-native shapes before
|
|
384
374
|
# POST /api/chat (or Open WebUI /ollama/api/chat):
|
|
385
375
|
# - function.arguments must be a Hash/Array object, not a JSON string
|
|
@@ -652,8 +642,6 @@ module PWN
|
|
|
652
642
|
puts "USAGE:
|
|
653
643
|
models = #{self}.get_models
|
|
654
644
|
|
|
655
|
-
usage = #{self}.get_plan_usage
|
|
656
|
-
|
|
657
645
|
response = #{self}.chat(
|
|
658
646
|
request: 'required - message to Ollama',
|
|
659
647
|
model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:ollama][:model])',
|
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/config.rb
CHANGED
|
@@ -309,6 +309,7 @@ module PWN
|
|
|
309
309
|
|
|
310
310
|
Pry.config.refresh_pwn_env = false if defined?(Pry)
|
|
311
311
|
env[:pwn_skills_path] = pwn_skills_path
|
|
312
|
+
PWN::Config.install_default_skills(pwn_skills_path: pwn_skills_path)
|
|
312
313
|
PWN::Config.load_skills(pwn_skills_path: pwn_skills_path)
|
|
313
314
|
|
|
314
315
|
# pwn-ai agent: memory/sessions/cron paths
|
|
@@ -559,7 +560,10 @@ module PWN
|
|
|
559
560
|
|
|
560
561
|
# Make pwn-ai aware of the skills folder in pwn_env parent (before freeze)
|
|
561
562
|
env[:pwn_skills_path] = pwn_skills_path if defined?(pwn_skills_path)
|
|
562
|
-
|
|
563
|
+
if defined?(pwn_skills_path)
|
|
564
|
+
PWN::Config.install_default_skills(pwn_skills_path: pwn_skills_path)
|
|
565
|
+
PWN::Config.load_skills(pwn_skills_path: pwn_skills_path)
|
|
566
|
+
end
|
|
563
567
|
|
|
564
568
|
# pwn-ai agent: memory, sessions, cron paths (before freeze)
|
|
565
569
|
env[:pwn_memory_path] = PWN::Memory::MEMORY_FILE if defined?(PWN::Memory)
|
|
@@ -802,6 +806,60 @@ module PWN
|
|
|
802
806
|
{ migrated: migrated.length, details: migrated }
|
|
803
807
|
end
|
|
804
808
|
|
|
809
|
+
DEFAULT_SKILLS_DIR = File.expand_path('../../etc/default_skills', __dir__)
|
|
810
|
+
DEFAULT_SKILL_NAMES = %w[
|
|
811
|
+
vulnerability-research-fundamentals
|
|
812
|
+
deep-exploitation
|
|
813
|
+
bug-bounty-hunting
|
|
814
|
+
sast-code-scans
|
|
815
|
+
reverse-engineering-binaries
|
|
816
|
+
penetration-testing
|
|
817
|
+
web-application-penetration-testing
|
|
818
|
+
red-teaming
|
|
819
|
+
hardware-and-firmware-testing
|
|
820
|
+
social-engineering
|
|
821
|
+
osint
|
|
822
|
+
].freeze
|
|
823
|
+
|
|
824
|
+
public_class_method def self.default_skill_names(opts = {})
|
|
825
|
+
return DEFAULT_SKILL_NAMES.dup if opts.is_a?(Hash)
|
|
826
|
+
|
|
827
|
+
DEFAULT_SKILL_NAMES.dup
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
public_class_method def self.default_skills_dir(opts = {})
|
|
831
|
+
return DEFAULT_SKILLS_DIR if opts.is_a?(Hash)
|
|
832
|
+
|
|
833
|
+
DEFAULT_SKILLS_DIR
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# Seed bundled skills into ~/.pwn/skills (or pwn_skills_path:).
|
|
837
|
+
# Missing names are copied from etc/default_skills. Existing SKILL.md
|
|
838
|
+
# files are left alone so operator edits survive upgrades.
|
|
839
|
+
public_class_method def self.install_default_skills(opts = {})
|
|
840
|
+
root = opts[:pwn_skills_path] || pwn_skills_path
|
|
841
|
+
src_root = opts[:source] || default_skills_dir
|
|
842
|
+
return [] if root.to_s.empty? || !Dir.exist?(src_root.to_s)
|
|
843
|
+
|
|
844
|
+
FileUtils.mkdir_p(root)
|
|
845
|
+
seeded = []
|
|
846
|
+
default_skill_names.each do |name|
|
|
847
|
+
dest = File.join(root, name, SKILL_ENTRY)
|
|
848
|
+
next if File.file?(dest)
|
|
849
|
+
|
|
850
|
+
src = File.join(src_root, name, SKILL_ENTRY)
|
|
851
|
+
next unless File.file?(src)
|
|
852
|
+
|
|
853
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
854
|
+
FileUtils.cp(src, dest)
|
|
855
|
+
seeded << { name: name, path: dest }
|
|
856
|
+
end
|
|
857
|
+
seeded
|
|
858
|
+
rescue StandardError => e
|
|
859
|
+
warn "[PWN::Config] install_default_skills failed: #{e.class}: #{e.message}"
|
|
860
|
+
[]
|
|
861
|
+
end
|
|
862
|
+
|
|
805
863
|
# Supported Method Parameters::
|
|
806
864
|
# skills = PWN::Config.load_skills(
|
|
807
865
|
# pwn_skills_path: 'optional - Path to skills folder. Defaults to ~/.pwn/skills'
|
|
@@ -955,6 +1013,9 @@ module PWN
|
|
|
955
1013
|
|
|
956
1014
|
#{self}.migrate_legacy_skills(pwn_skills_path: 'optional', delete_legacy: true)
|
|
957
1015
|
|
|
1016
|
+
#{self}.install_default_skills(pwn_skills_path: 'optional')
|
|
1017
|
+
#{self}.default_skill_names
|
|
1018
|
+
|
|
958
1019
|
#{self}.refresh_env(
|
|
959
1020
|
pwn_env_path: 'optional - Path to pwn.yaml file. Defaults to ~/.pwn/pwn.yaml',
|
|
960
1021
|
pwn_dec_path: 'optional - Path to pwn.yaml.decryptor file. Defaults to ~/.pwn/pwn.yaml.decryptor'
|