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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/documentation/Agent-Tool-Registry.md +1 -3
  4. data/documentation/Configuration.md +0 -2
  5. data/documentation/General-PWN-Usage.md +1 -1
  6. data/documentation/Home.md +2 -2
  7. data/documentation/How-PWN-Works.md +2 -2
  8. data/documentation/Installation.md +18 -2
  9. data/documentation/Persistence.md +1 -1
  10. data/documentation/Session-Workflow.md +8 -28
  11. data/documentation/Skills-Memory-Learning.md +20 -0
  12. data/documentation/Troubleshooting.md +1 -1
  13. data/documentation/Why-PWN.md +1 -1
  14. data/documentation/diagrams/dot/memory-skills-detailed.dot +1 -1
  15. data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
  16. data/documentation/diagrams/dot/persistence-filesystem.dot +1 -1
  17. data/documentation/diagrams/dot/sessions-cron-automation.dot +1 -1
  18. data/documentation/diagrams/memory-skills-detailed.svg +85 -84
  19. data/documentation/diagrams/overall-pwn-architecture.svg +15 -15
  20. data/documentation/diagrams/persistence-filesystem.svg +1 -1
  21. data/documentation/diagrams/sessions-cron-automation.svg +7 -6
  22. data/documentation/pwn-REPL.md +1 -1
  23. data/documentation/pwn-ai-Agent.md +4 -7
  24. data/etc/default_skills/bug-bounty-hunting/SKILL.md +70 -0
  25. data/etc/default_skills/deep-exploitation/SKILL.md +74 -0
  26. data/etc/default_skills/hardware-and-firmware-testing/SKILL.md +83 -0
  27. data/etc/default_skills/osint/SKILL.md +147 -0
  28. data/etc/default_skills/penetration-testing/SKILL.md +81 -0
  29. data/etc/default_skills/red-teaming/SKILL.md +70 -0
  30. data/etc/default_skills/reverse-engineering-binaries/SKILL.md +70 -0
  31. data/etc/default_skills/sast-code-scans/SKILL.md +66 -0
  32. data/etc/default_skills/social-engineering/SKILL.md +67 -0
  33. data/etc/default_skills/vulnerability-research-fundamentals/SKILL.md +74 -0
  34. data/etc/default_skills/web-application-penetration-testing/SKILL.md +68 -0
  35. data/lib/pwn/ai/agent/loop.rb +79 -41
  36. data/lib/pwn/ai/agent/mistakes.rb +6 -6
  37. data/lib/pwn/ai/agent/prompt_builder.rb +3 -4
  38. data/lib/pwn/ai/agent/tool_guard.rb +1 -46
  39. data/lib/pwn/ai/agent/tools/ruby_eval.rb +0 -10
  40. data/lib/pwn/ai/agent/tools/shell.rb +0 -6
  41. data/lib/pwn/ai/anthropic.rb +0 -118
  42. data/lib/pwn/ai/gemini.rb +0 -15
  43. data/lib/pwn/ai/grok.rb +0 -158
  44. data/lib/pwn/ai/ollama.rb +0 -12
  45. data/lib/pwn/ai/open_ai.rb +0 -110
  46. data/lib/pwn/ai/open_web_ui.rb +0 -12
  47. data/lib/pwn/ai.rb +0 -86
  48. data/lib/pwn/config.rb +62 -1
  49. data/lib/pwn/migrate.rb +2 -0
  50. data/lib/pwn/plugins/log.rb +302 -0
  51. data/lib/pwn/plugins/repl.rb +28 -8
  52. data/lib/pwn/version.rb +1 -1
  53. data/spec/integration/config_spec.rb +37 -0
  54. data/spec/lib/pwn/ai/agent/loop_spec.rb +52 -12
  55. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +7 -0
  56. data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +5 -8
  57. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -18
  58. data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
  59. data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
  60. data/spec/lib/pwn/ai/grok_spec.rb +2 -2
  61. data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
  62. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
  63. data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
  64. data/spec/lib/pwn/ai_spec.rb +4 -21
  65. data/spec/lib/pwn/plugins/log_spec.rb +139 -0
  66. data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +31 -0
  67. data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
  68. data/third_party/pwn_rdoc.jsonl +29 -24
  69. metadata +12 -2
  70. data/documentation/PWN_Contributors_and_Users.png +0 -0
@@ -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])',
@@ -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
@@ -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
- PWN::Config.load_skills(pwn_skills_path: pwn_skills_path) if defined?(pwn_skills_path)
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'