pwn 0.5.673 → 0.5.675

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +2 -2
  4. data/bin/pwn +2 -0
  5. data/bin/pwn_ai_red_team +7 -2
  6. data/bin/pwn_setup +25 -2
  7. data/documentation/Home.md +2 -2
  8. data/documentation/How-PWN-Works.md +1 -1
  9. data/documentation/WWW.md +17 -6
  10. data/documentation/What-is-PWN.md +1 -1
  11. data/documentation/diagrams/dot/overall-pwn-architecture.dot +1 -1
  12. data/documentation/diagrams/dot/web-application-testing.dot +1 -1
  13. data/documentation/diagrams/overall-pwn-architecture.svg +1 -1
  14. data/documentation/diagrams/web-application-testing.svg +1 -1
  15. data/lib/pwn/ai/agent/loop.rb +10 -0
  16. data/lib/pwn/ai/agent/reflect.rb +3 -1
  17. data/lib/pwn/ai/agent/reward.rb +27 -3
  18. data/lib/pwn/ai/agent/task_summarizer.rb +20 -3
  19. data/lib/pwn/ai/anthropic.rb +128 -4
  20. data/lib/pwn/ai/gemini.rb +15 -0
  21. data/lib/pwn/ai/grok.rb +185 -9
  22. data/lib/pwn/ai/ollama.rb +30 -5
  23. data/lib/pwn/ai/open_ai.rb +136 -9
  24. data/lib/pwn/ai/open_web_ui.rb +12 -0
  25. data/lib/pwn/ai/red_team/excessive_agency.rb +9 -7
  26. data/lib/pwn/ai/red_team/insecure_output_handling.rb +9 -7
  27. data/lib/pwn/ai/red_team/jailbreak.rb +9 -7
  28. data/lib/pwn/ai/red_team/overreliance.rb +9 -7
  29. data/lib/pwn/ai/red_team/payload_splitting.rb +9 -7
  30. data/lib/pwn/ai/red_team/prompt_injection.rb +9 -7
  31. data/lib/pwn/ai/red_team/sensitive_information_disclosure.rb +9 -7
  32. data/lib/pwn/ai/red_team/system_prompt_extraction.rb +9 -7
  33. data/lib/pwn/ai/red_team/test_case_engine.rb +171 -13
  34. data/lib/pwn/ai/red_team/token_smuggling.rb +9 -7
  35. data/lib/pwn/ai/red_team.rb +0 -1
  36. data/lib/pwn/ai.rb +86 -0
  37. data/lib/pwn/cron.rb +428 -15
  38. data/lib/pwn/plugins/repl.rb +3 -2
  39. data/lib/pwn/setup.rb +70 -0
  40. data/lib/pwn/version.rb +1 -1
  41. data/lib/pwn/www/github.rb +174 -0
  42. data/lib/pwn/www.rb +1 -0
  43. data/spec/documentation/installation_md_spec.rb +6 -3
  44. data/spec/lib/pwn/ai/agent/loop_spec.rb +7 -0
  45. data/spec/lib/pwn/ai/agent/reward_spec.rb +6 -0
  46. data/spec/lib/pwn/ai/anthropic_spec.rb +4 -0
  47. data/spec/lib/pwn/ai/gemini_spec.rb +4 -0
  48. data/spec/lib/pwn/ai/grok_spec.rb +14 -0
  49. data/spec/lib/pwn/ai/ollama_spec.rb +30 -0
  50. data/spec/lib/pwn/ai/open_ai_spec.rb +33 -0
  51. data/spec/lib/pwn/ai/open_web_ui_spec.rb +9 -0
  52. data/spec/lib/pwn/ai/red_team/excessive_agency_spec.rb +6 -0
  53. data/spec/lib/pwn/ai/red_team/insecure_output_handling_spec.rb +6 -0
  54. data/spec/lib/pwn/ai/red_team/jailbreak_spec.rb +6 -0
  55. data/spec/lib/pwn/ai/red_team/overreliance_spec.rb +6 -0
  56. data/spec/lib/pwn/ai/red_team/payload_splitting_spec.rb +6 -0
  57. data/spec/lib/pwn/ai/red_team/prompt_injection_spec.rb +6 -0
  58. data/spec/lib/pwn/ai/red_team/sensitive_information_disclosure_spec.rb +6 -0
  59. data/spec/lib/pwn/ai/red_team/system_prompt_extraction_spec.rb +6 -0
  60. data/spec/lib/pwn/ai/red_team/test_case_engine_spec.rb +99 -0
  61. data/spec/lib/pwn/ai/red_team/token_smuggling_spec.rb +6 -0
  62. data/spec/lib/pwn/ai_spec.rb +23 -0
  63. data/spec/lib/pwn/cron_spec.rb +143 -0
  64. data/spec/lib/pwn/plugins/repl_spec.rb +12 -0
  65. data/spec/lib/pwn/setup_spec.rb +43 -0
  66. data/spec/lib/pwn/www/github_spec.rb +15 -0
  67. data/spec/support/sandbox.rb +4 -0
  68. data/third_party/pwn_rdoc.jsonl +67 -14
  69. metadata +5 -5
  70. data/lib/pwn/ai/red_team/model_denial_of_service.rb +0 -89
  71. data/spec/lib/pwn/ai/red_team/model_denial_of_service_spec.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9640ee1c83ff54dc1e10246d519a448d648806e30b35b579284b23708b5b9924
4
- data.tar.gz: 2cbd3b2296d32ad6d068d276c3badbfa599065c668bbec7ac58fa2baf84651d7
3
+ metadata.gz: 2ed5f171854df678e76d477ad52fee2dd42e1687afc6e950281d2b3e5fe75ab9
4
+ data.tar.gz: 35712ab0300a98de2dfc52430278088561b84fcd15e4dd70a3c95cd90ccf4645
5
5
  SHA512:
6
- metadata.gz: fa11ac68afcc6cde52bc207526a50cf53283ac59dd89340f25f303329562e7d34c7a02cbd286ca6fe34fffcaa307a9b0e3f9c03c03c89dca3788f9150f702f0a
7
- data.tar.gz: c9bd66494f36503ba864a7c97af7d7a77b2d9502a502ed04c897ce08e2e693982efe79a43abc4c12d95e1d4b4b97ea3a1ec6fba61b0711599e025bcc2b19618d
6
+ metadata.gz: 563ee1d396728ef91353cfc4e6d82da6b90662d03e55d1c027319203e47a8bd91d9c723a09cda3a6836dc9b1ed380bb3e0bb76d802dd23157ff7c92d52a3958b
7
+ data.tar.gz: 05e3c313c8262273f5799f24a9359b3cadc5448ccebfc5ae031a99a71865b548d24489208f4b7be6e0d4790485ebcc58d35a3ccdafbfddfecdb27eaddd531b9c
data/Gemfile CHANGED
@@ -49,7 +49,7 @@ gem 'jwt', '3.2.0'
49
49
  gem 'libusb', '0.8.0'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.1'
52
- gem 'mcp', '1.1.0'
52
+ gem 'mcp', '1.2.0'
53
53
  gem 'meshtastic', '0.0.173'
54
54
  gem 'metasm', '1.0.6'
55
55
  gem 'mongo', '2.25.0'
data/README.md CHANGED
@@ -34,7 +34,7 @@ Red teamers, pentesters, and vulnerability researchers get one place to script
34
34
  and automate instead of gluing together a pile of separate CLIs.
35
35
 
36
36
  **In numbers:** 66 `PWN::Plugins` · 48 `PWN::SAST` rules · 90 `PWN::AWS`
37
- service wrappers · 21 `PWN::WWW` site drivers · 53 `bin/pwn_*` CLI drivers ·
37
+ service wrappers · 22 `PWN::WWW` site drivers · 53 `bin/pwn_*` CLI drivers ·
38
38
  6 LLM engines · 13 agent toolsets · 85 LLM-callable tools.
39
39
 
40
40
  Full page: [What is PWN](documentation/What-is-PWN.md)
@@ -139,7 +139,7 @@ The complete wiki lives in this repo at **[`documentation/Home.md`](documentatio
139
139
  | [What is PWN](documentation/What-is-PWN.md) | [`pwn` REPL](documentation/pwn-REPL.md) | [AI / LLM Integration](documentation/AI-Integration.md) | [Plugins (66)](documentation/Plugins.md) |
140
140
  | [Why PWN](documentation/Why-PWN.md) | [`pwn-ai` Agent](documentation/pwn-ai-Agent.md) | [Agent Tool Registry](documentation/Agent-Tool-Registry.md) | [SAST (48)](documentation/SAST.md) |
141
141
  | [How PWN Works](documentation/How-PWN-Works.md) | [CLI Drivers (53)](documentation/CLI-Drivers.md) | [Memory · Skills · Learning](documentation/Skills-Memory-Learning.md) | [AWS (90)](documentation/AWS.md) |
142
- | [Installation](documentation/Installation.md) | [Build a Driver](documentation/Drivers.md) | [Mistakes (neg-feedback)](documentation/Mistakes.md) | [WWW (21)](documentation/WWW.md) |
142
+ | [Installation](documentation/Installation.md) | [Build a Driver](documentation/Drivers.md) | [Mistakes (neg-feedback)](documentation/Mistakes.md) | [WWW (22)](documentation/WWW.md) |
143
143
  | [General Usage](documentation/General-PWN-Usage.md) | | [Reinforcement Learning](documentation/Reinforcement-Learning.md) | [SDR / Radio](documentation/SDR.md) |
144
144
  | [Configuration](documentation/Configuration.md) | | [Extrospection](documentation/Extrospection.md) | [Hardware](documentation/Hardware.md) |
145
145
  | [`~/.pwn/` Persistence](documentation/Persistence.md) | | [Swarm (multi-agent)](documentation/Swarm.md) | [Reports](documentation/Reporting.md) |
data/bin/pwn CHANGED
@@ -51,9 +51,11 @@ begin
51
51
  # ------------------------------------------------------------------
52
52
  if opts[:setup] == true
53
53
  r = PWN::Setup.check
54
+ PWN::Setup.ensure_cron(restart: false)
54
55
  exit(r[:ok] ? 0 : 1)
55
56
  else
56
57
  PWN::Setup.deps(profile: opts[:setup])
58
+ PWN::Setup.ensure_cron(restart: false)
57
59
  end
58
60
  elsif opts[:ai]
59
61
  # ------------------------------------------------------------------
data/bin/pwn_ai_red_team CHANGED
@@ -14,6 +14,10 @@ PWN::Driver::Parser.new do |options|
14
14
  opts[:target_model] = m
15
15
  end
16
16
 
17
+ options.on('-cCOUNT', '--payload-count=COUNT', '<Optional - Number of LLM-generated payloads per test case (Default 10)>') do |c|
18
+ opts[:payload_count] = c
19
+ end
20
+
17
21
  options.on('-rROLE', '--system-role=ROLE', '<Optional - System Prompt Applied to the Target for Every Payload>') do |r|
18
22
  opts[:system_role_content] = r
19
23
  end
@@ -60,6 +64,7 @@ begin
60
64
  target_engine = opts[:target_engine]
61
65
  target_model = opts[:target_model]
62
66
  system_role_content = opts[:system_role_content]
67
+ payload_count = (opts[:payload_count] || 10).to_i
63
68
 
64
69
  max_threads = opts[:max_threads]
65
70
  max_threads ||= 5
@@ -80,7 +85,6 @@ begin
80
85
  ExcessiveAgency
81
86
  InsecureOutputHandling
82
87
  Jailbreak
83
- ModelDenialOfService
84
88
  Overreliance
85
89
  PayloadSplitting
86
90
  PromptInjection
@@ -115,7 +119,8 @@ begin
115
119
  ).scan(
116
120
  target_engine: target_engine,
117
121
  target_model: target_model,
118
- system_role_content: system_role_content
122
+ system_role_content: system_role_content,
123
+ payload_count: payload_count
119
124
  )
120
125
 
121
126
  rt_arr.each { |hash_line| mutex.synchronize { results_hash[:data].push(hash_line) } }
data/bin/pwn_setup CHANGED
@@ -59,6 +59,18 @@ pwn_driver.auto_opts_help = false
59
59
  pwn_driver.parse!
60
60
 
61
61
  begin
62
+ status = 0
63
+ # Seed default jobs + start/reuse the in-process scheduler so last_run
64
+ # actually advances. Do this BEFORE the doctor so `pwn setup` reports
65
+ # the worker as running. Skip list/terminal (no host scheduler needed)
66
+ # and dry-run (must not spawn).
67
+ unless opts[:list] || opts[:terminal] || opts[:dry_run]
68
+ PWN::Setup.ensure_cron(
69
+ restart: false,
70
+ dry_run: false
71
+ )
72
+ end
73
+
62
74
  if opts[:list]
63
75
  PWN::Setup.list_profiles
64
76
  elsif opts[:migrate]
@@ -66,7 +78,7 @@ begin
66
78
  fix: opts[:fix] || opts[:yes],
67
79
  dry_run: opts[:dry_run]
68
80
  )
69
- exit 1 unless r && r[:after] && r[:after][:incompatible].empty?
81
+ status = 1 unless r && r[:after] && r[:after][:incompatible].empty?
70
82
  elsif opts[:terminal]
71
83
  PWN::Setup.terminal(
72
84
  yes: opts[:yes],
@@ -83,8 +95,19 @@ begin
83
95
  # `pwn setup --check` → same
84
96
  # `pwn setup --check --profile net` → gate ONLY on the :net profile
85
97
  r = PWN::Setup.check(profile: opts[:profile])
86
- exit 1 unless r[:ok]
98
+ status = 1 unless r[:ok]
99
+ end
100
+
101
+ # Seed default jobs + start/reuse the in-process scheduler so
102
+ # last_run actually advances. Skip list/terminal (no host scheduler
103
+ # needed) and dry-run (must not spawn).
104
+ unless opts[:list] || opts[:terminal] || opts[:dry_run]
105
+ PWN::Setup.ensure_cron(
106
+ restart: false,
107
+ dry_run: false
108
+ )
87
109
  end
110
+ exit status
88
111
  rescue Interrupt
89
112
  puts "\naborted."
90
113
  exit 130
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **PWN** (/pōn/) - an open-source offensive-security automation framework and
4
4
  > continuous-security-integration platform written in Ruby.
5
- > 66 plugins · 48 SAST rules · 90 AWS wrappers · 21 WWW drivers · 53 CLI
5
+ > 66 plugins · 48 SAST rules · 90 AWS wrappers · 22 WWW drivers · 53 CLI
6
6
  > drivers · 6 LLM engines · a self-improving multi-agent AI · one REPL.
7
7
 
8
8
  **Repo root:** `/opt/pwn` · **This wiki:** `/opt/pwn/documentation/` ·
@@ -57,7 +57,7 @@
57
57
  | &nbsp;&nbsp;↳ [Fuzzing](Fuzzing.md) | `Fuzz` · `Sock` · `Packet` |
58
58
  | &nbsp;&nbsp;↳ [Hardware](Hardware.md) | Serial · BusPirate · MSR206 · Android · BareSIP |
59
59
  | [SAST (48 rules)](SAST.md) | Static analysis + test-case engine |
60
- | [WWW (21 drivers)](WWW.md) | Browser automations for real sites |
60
+ | [WWW (22 drivers)](WWW.md) | Browser automations for real sites |
61
61
  | [AWS (90 services)](AWS.md) | Cloud-security enumeration |
62
62
  | [SDR](SDR.md) | GQRX · FlipperZero · RFIDler · SonMicro · **Decoder::* (20 protocols + Base/DSP)** · FFI-accelerated DSP |
63
63
  | [Blockchain](Blockchain.md) | BTC · ETH helpers |
@@ -45,7 +45,7 @@ call, and [Reinforcement Learning](Reinforcement-Learning.md) for how
45
45
 
46
46
  ## L3 - Capability namespaces (`lib/pwn/*`)
47
47
 
48
- `Plugins` (66) · `SAST` (48) · `WWW` (21) · `AWS` (90) · `SDR` · `Blockchain` ·
48
+ `Plugins` (66) · `SAST` (48) · `WWW` (22) · `AWS` (90) · `SDR` · `Blockchain` ·
49
49
  `Bounty` · `Reports` · `FFI` · `Banner` · **`Setup`** · **`Migrate`**. Each is
50
50
  a plain module of `public_class_method def self.x(opts = {})` methods -
51
51
  callable the same way from the REPL, from `pwn_eval`, or from a driver.
data/documentation/WWW.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `PWN::WWW` - Site-Specific Browser Automations
2
2
 
3
- 21 modules, each a scripted [`TransparentBrowser`](Transparent-Browser.md)
3
+ 22 modules, each a scripted [`TransparentBrowser`](Transparent-Browser.md)
4
4
  session against a real site: log in, navigate, scrape, submit.
5
5
  Source: `lib/pwn/www/*.rb`.
6
6
 
@@ -9,6 +9,7 @@ Source: `lib/pwn/www/*.rb`.
9
9
  | Category | Modules |
10
10
  |---|---|
11
11
  | **Bug bounty** | `HackerOne` · `BugCrowd` · `Synack` · `AppCobaltIO` |
12
+ | **Code hosts** | `GitHub` |
12
13
  | **Search / OSINT** | `Google` · `Bing` · `DuckDuckGo` · `Torch` · `WaybackMachine` · `Pastebin` · `Checkip` |
13
14
  | **Social** | `Twitter` · `Facebook` · `LinkedIn` · `Youtube` · `Pandora` |
14
15
  | **Finance / Work** | `CoinbasePro` · `Paypal` · `TradingView` · `Uber` · `Upwork` |
@@ -16,14 +17,24 @@ Source: `lib/pwn/www/*.rb`.
16
17
  ## Pattern
17
18
 
18
19
  ```ruby
19
- b = PWN::WWW::HackerOne.open(browser_type: :headless,
20
- proxy: 'http://127.0.0.1:8080')
21
- PWN::WWW::HackerOne.login(browser_obj: b, username: '...', mfa: '...')
22
- # ... scripted navigation ...
23
- PWN::WWW::HackerOne.logout(browser_obj: b)
20
+ b = PWN::WWW::GitHub.open(browser_type: :headless,
21
+ proxy: 'http://127.0.0.1:8080')
22
+ PWN::WWW::GitHub.login(
23
+ browser_obj: b,
24
+ username: '...',
25
+ password: '...',
26
+ mfa: '123456'
27
+ )
28
+ # scripted navigation
29
+ PWN::WWW::GitHub.logout(browser_obj: b)
24
30
  PWN::Plugins::TransparentBrowser.close(browser_obj: b)
25
31
  ```
26
32
 
33
+ `login` takes `username`, `password`, and an MFA token (`mfa:` or
34
+ `mfa_token:`). Pass the token string to stay non-interactive, or set
35
+ `mfa: true` to prompt. The browser stays on the post-auth GitHub session
36
+ when the method returns.
37
+
27
38
  Because traffic goes through TransparentBrowser, you can point `proxy:` at
28
39
  [BurpSuite](BurpSuite.md) and passively capture every request the automation
29
40
  makes.
@@ -15,7 +15,7 @@ with a **tool-calling AI agent** on top that can run the same methods.
15
15
  | `PWN::Plugins::*` | **66** | Wrappers for external and native tooling (Burp, Nmap, Metasploit, Shodan, browsers, serial, ...) |
16
16
  | `PWN::SAST::*` | **48** | Static-analysis rules across C/Java/Go/Python/Ruby/Scala/PHP/TS |
17
17
  | `PWN::AWS::*` | **90** | One module per AWS service for cloud enumeration |
18
- | `PWN::WWW::*` | **21** | Site-specific browser automations (HackerOne, BugCrowd, Google, LinkedIn, ...) |
18
+ | `PWN::WWW::*` | **22** | Site-specific browser automations (HackerOne, BugCrowd, GitHub, Google, LinkedIn, ...) |
19
19
  | `PWN::SDR::*` | **6** (+ **20** protocol decoders + Base/DSP) | GQRX, FlipperZero, RFIDler, SonMicro, band tables, `Decoder::{ADSB,POCSAG,RDS,LoRa,...}` |
20
20
  | `PWN::FFI::*` | **8** | Native DSP/RF backends: Volk · Liquid · FFTW · RTLSdr · HackRF · AdalmPluto · SoapySDR · Stdio |
21
21
  | `PWN::AI::*` | **6** engines | OpenAI, Anthropic, Grok (OAuth device-flow), Gemini, Ollama, Open WebUI |
@@ -49,7 +49,7 @@ digraph "PWN_Overall_Architecture" {
49
49
  style=rounded; color="#047857"; bgcolor="#022c22"; penwidth=2;
50
50
  Plugins [label="Plugins\n66 modules", fillcolor="#6ee7b7"];
51
51
  SAST [label="SAST\n48 rules", fillcolor="#6ee7b7"];
52
- WWW [label="WWW\n21 site drivers", fillcolor="#6ee7b7"];
52
+ WWW [label="WWW\n22 site drivers", fillcolor="#6ee7b7"];
53
53
  AWS [label="AWS\n90 services", fillcolor="#6ee7b7"];
54
54
  SDR [label="SDR\nGQRX · Flipper · RFID\nDecoder::* (20+Base/DSP)", fillcolor="#6ee7b7"];
55
55
  Chain [label="Blockchain\nBTC · ETH", fillcolor="#6ee7b7"];
@@ -17,7 +17,7 @@ digraph "PWN_Web" {
17
17
  color="#047857"; bgcolor="#022c22";
18
18
  TB [label="TransparentBrowser\nheadless · watir · devtools", fillcolor="#6ee7b7"];
19
19
  Spider [label="Plugins::Spider\nURI buster", fillcolor="#6ee7b7"];
20
- WWW [label="PWN::WWW::*\n21 site drivers", fillcolor="#6ee7b7"];
20
+ WWW [label="PWN::WWW::*\n22 site drivers", fillcolor="#6ee7b7"];
21
21
  }
22
22
  subgraph cluster_proxy {
23
23
  label="Intercept + Scan"; fontcolor="#fde68a"; style=rounded;
@@ -219,7 +219,7 @@
219
219
  <title>WWW</title>
220
220
  <path fill="#6ee7b7" stroke="#334155" stroke-width="1.4" d="M495.38,-198.88C495.38,-198.88 436.62,-198.88 436.62,-198.88 430.62,-198.88 424.62,-192.88 424.62,-186.88 424.62,-186.88 424.62,-174.88 424.62,-174.88 424.62,-168.88 430.62,-162.88 436.62,-162.88 436.62,-162.88 495.38,-162.88 495.38,-162.88 501.38,-162.88 507.38,-168.88 507.38,-174.88 507.38,-174.88 507.38,-186.88 507.38,-186.88 507.38,-192.88 501.38,-198.88 495.38,-198.88"/>
221
221
  <text xml:space="preserve" text-anchor="middle" x="466" y="-183.93" font-family="sans-Serif" font-size="11.00" fill="#0f172a">WWW</text>
222
- <text xml:space="preserve" text-anchor="middle" x="466" y="-170.43" font-family="sans-Serif" font-size="11.00" fill="#0f172a">21 site drivers</text>
222
+ <text xml:space="preserve" text-anchor="middle" x="466" y="-170.43" font-family="sans-Serif" font-size="11.00" fill="#0f172a">22 site drivers</text>
223
223
  </g>
224
224
  <!-- Registry&#45;&gt;WWW -->
225
225
  <g id="edge12" class="edge">
@@ -64,7 +64,7 @@
64
64
  <title>WWW</title>
65
65
  <path fill="#6ee7b7" stroke="#334155" stroke-width="1.3" d="M256.25,-52C256.25,-52 196,-52 196,-52 190,-52 184,-46 184,-40 184,-40 184,-28 184,-28 184,-22 190,-16 196,-16 196,-16 256.25,-16 256.25,-16 262.25,-16 268.25,-22 268.25,-28 268.25,-28 268.25,-40 268.25,-40 268.25,-46 262.25,-52 256.25,-52"/>
66
66
  <text xml:space="preserve" text-anchor="middle" x="226.12" y="-36.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">PWN::WWW::*</text>
67
- <text xml:space="preserve" text-anchor="middle" x="226.12" y="-24.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">21 site drivers</text>
67
+ <text xml:space="preserve" text-anchor="middle" x="226.12" y="-24.5" font-family="sans-Serif" font-size="10.00" fill="#0f172a">22 site drivers</text>
68
68
  </g>
69
69
  <!-- Target&#45;&gt;WWW -->
70
70
  <g id="edge3" class="edge">
@@ -1063,6 +1063,16 @@ module PWN
1063
1063
  # 3.2 — local models cannot afford auto_introspect (judge+prm+critic+
1064
1064
  # sentinel+extro) on every success. Default :failure_only when local.
1065
1065
  private_class_method def self.should_auto_introspect?(opts = {})
1066
+ kind = (opts[:kind] || Thread.current[:pwn_request_kind]).to_s.to_sym
1067
+ intent = (opts[:intent] || Thread.current[:pwn_request_intent]).to_s.to_sym
1068
+ fails = opts[:turn_fails].is_a?(Hash) ? opts[:turn_fails].values.sum : 0
1069
+ # Cheap answers already returned user-visible text. The post-answer
1070
+ # critic + 12s ORM printed ERROR: Timed out reading data from server
1071
+ # after greetings / takes / questions.
1072
+ return false if %i[greeting howto recall].include?(intent)
1073
+ return false if kind == :statement
1074
+ return false if kind == :question && fails.zero?
1075
+
1066
1076
  return true unless opts[:local]
1067
1077
 
1068
1078
  policy = agent_flag(key: :local_introspect, default: :failure_only).to_s.to_sym
@@ -97,7 +97,8 @@ module PWN
97
97
  system_role_content: system_role_content,
98
98
  spinner: spinner,
99
99
  timeout: timeout,
100
- temp: temp
100
+ temp: temp,
101
+ quiet: opts[:quiet]
101
102
  )
102
103
  end
103
104
  ensure
@@ -132,6 +133,7 @@ module PWN
132
133
  chat_opts[:timeout] = opts[:timeout] unless opts[:timeout].nil?
133
134
  chat_opts[:temp] = opts[:temp] unless opts[:temp].nil?
134
135
  chat_opts[:model] = opts[:model] unless opts[:model].to_s.empty?
136
+ chat_opts[:quiet] = opts[:quiet] unless opts[:quiet].nil?
135
137
  mod.chat(chat_opts)
136
138
  end
137
139
 
@@ -1157,11 +1157,20 @@ module PWN
1157
1157
  spinner: false,
1158
1158
  model: judge_model,
1159
1159
  timeout: cheap_orm_timeout,
1160
- temp: CHEAP_ORM_TEMP
1160
+ temp: CHEAP_ORM_TEMP,
1161
+ quiet: true
1161
1162
  )
1162
1163
  text = extract_chat_text(resp: raw)
1163
1164
  return text unless text.strip.empty?
1164
- rescue StandardError
1165
+
1166
+ # Quiet ReadTimeout now returns nil instead of raising. That
1167
+ # empty response already spent the cheap-ORM budget — do not
1168
+ # immediately fire engine_chat_cheap (second 12s stall).
1169
+ return nil
1170
+ rescue StandardError => e
1171
+ # A 12s ReadTimeout already spent the cheap-ORM budget.
1172
+ return nil if timeout_error?(err: e)
1173
+
1165
1174
  nil
1166
1175
  end
1167
1176
  end
@@ -1191,7 +1200,8 @@ module PWN
1191
1200
  system_role_content: opts[:system_role_content],
1192
1201
  spinner: false,
1193
1202
  timeout: cheap_orm_timeout,
1194
- temp: CHEAP_ORM_TEMP
1203
+ temp: CHEAP_ORM_TEMP,
1204
+ quiet: true
1195
1205
  }
1196
1206
  model = judge_model
1197
1207
  chat_opts[:model] = model unless model.to_s.empty?
@@ -1200,6 +1210,20 @@ module PWN
1200
1210
  nil
1201
1211
  end
1202
1212
 
1213
+ private_class_method def self.timeout_error?(opts = {})
1214
+ err = opts[:err]
1215
+ return false if err.nil?
1216
+
1217
+ klass = err.class
1218
+ name = klass.name.to_s
1219
+ return true if name.include?('Timeout') || name.include?('ReadTimeout') || name.include?('OpenTimeout')
1220
+ return true if err.message.to_s.match?(/timed out reading|read timeout|open timeout/i)
1221
+
1222
+ false
1223
+ rescue StandardError
1224
+ false
1225
+ end
1226
+
1203
1227
  private_class_method def self.cheap_orm_timeout
1204
1228
  n = agent_flag(key: :reward_llm_timeout, default: CHEAP_ORM_TIMEOUT).to_i
1205
1229
  n = CHEAP_ORM_TIMEOUT if n < 2
@@ -566,7 +566,9 @@ module PWN
566
566
  request: user,
567
567
  system_role_content: system,
568
568
  suppress_pii_warning: true,
569
- spinner: false
569
+ spinner: false,
570
+ timeout: sidecar_timeout,
571
+ quiet: true
570
572
  )
571
573
  text = reflect_text(resp: resp)
572
574
  return text unless text.to_s.strip.empty?
@@ -846,7 +848,9 @@ module PWN
846
848
  request: user,
847
849
  system_role_content: system,
848
850
  suppress_pii_warning: true,
849
- spinner: false
851
+ spinner: false,
852
+ timeout: sidecar_timeout,
853
+ quiet: true
850
854
  )
851
855
  text = reflect_text(resp: resp)
852
856
  return text unless text.to_s.strip.empty?
@@ -896,7 +900,9 @@ module PWN
896
900
  r = mod.chat(
897
901
  request: opts[:request],
898
902
  system_role_content: opts[:system_role_content],
899
- spinner: false
903
+ spinner: false,
904
+ timeout: sidecar_timeout,
905
+ quiet: true
900
906
  )
901
907
  reflect_text(resp: r)
902
908
  rescue StandardError => e
@@ -911,6 +917,17 @@ module PWN
911
917
  :ollama
912
918
  end
913
919
 
920
+ # Kind / plan classification is a sidecar hop. Bound it so a hung
921
+ # model cannot stall the user's real answer, and stay quiet on timeout.
922
+ private_class_method def self.sidecar_timeout
923
+ n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
924
+ n = 20 if n < 5
925
+ n = 60 if n > 60
926
+ n
927
+ rescue StandardError
928
+ 20
929
+ end
930
+
914
931
  # Parse JSON array, fenced JSON, or numbered/bulleted plain text.
915
932
  #
916
933
  # Supported Method Parameters::
@@ -362,7 +362,7 @@ module PWN
362
362
  real_config_value?(value: oauth[:bearer_token]) ||
363
363
  real_config_value?(value: oauth[:refresh_token])
364
364
 
365
- if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key]))
365
+ if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key])) && !opts[:non_interactive]
366
366
  token = obtain_oauth_bearer_token(oauth)
367
367
  using_oauth = true
368
368
  end
@@ -372,9 +372,15 @@ module PWN
372
372
  using_oauth = false
373
373
  end
374
374
 
375
- token ||= PWN::Plugins::AuthenticationHelper.mask_password(
376
- prompt: 'Anthropic API Key (or run PWN::AI::Anthropic.obtain_oauth_bearer_token for Claude Pro/Max OAuth)'
377
- )
375
+ if token.nil?
376
+ return nil if opts[:non_interactive]
377
+
378
+ token = PWN::Plugins::AuthenticationHelper.mask_password(
379
+ prompt: 'Anthropic API Key (or run PWN::AI::Anthropic.obtain_oauth_bearer_token for Claude Pro/Max OAuth)'
380
+ )
381
+ end
382
+
383
+ return nil if token.nil?
378
384
 
379
385
  http_method = if opts[:http_method].nil?
380
386
  :get
@@ -488,6 +494,122 @@ module PWN
488
494
  raise e
489
495
  end
490
496
 
497
+ # Supported Method Parameters::
498
+ # usage = PWN::AI::Anthropic.get_plan_usage(
499
+ # timeout: 'optional - seconds (default 8)'
500
+ # )
501
+ #
502
+ # Admin Usage API (organizations/usage_report/messages) yields tokens
503
+ # used; organizations/rate_limits yields the configured ceiling so the
504
+ # PS1 can show a percent. Individual accounts without an Admin key
505
+ # return {available:false}. Never prompts for credentials.
506
+ public_class_method def self.get_plan_usage(opts = {})
507
+ timeout = opts[:timeout] || 8
508
+ return { available: false, engine: :anthropic } unless plan_usage_credentials?
509
+
510
+ now = Time.now.utc
511
+ start_of_month = Time.utc(now.year, now.month, 1)
512
+ report = parse_plan_usage_json(
513
+ raw: anthropic_rest_call(
514
+ rest_call: 'organizations/usage_report/messages',
515
+ params: {
516
+ starting_at: start_of_month.strftime('%Y-%m-%dT00:00:00Z'),
517
+ ending_at: now.strftime('%Y-%m-%dT%H:%M:%SZ'),
518
+ bucket_width: '1d'
519
+ },
520
+ timeout: timeout,
521
+ spinner: false,
522
+ non_interactive: true
523
+ )
524
+ )
525
+ used_tokens = sum_usage_tokens(report: report)
526
+
527
+ limits = parse_plan_usage_json(
528
+ raw: anthropic_rest_call(
529
+ rest_call: 'organizations/rate_limits',
530
+ timeout: timeout,
531
+ spinner: false,
532
+ non_interactive: true
533
+ )
534
+ )
535
+ limit_tokens = first_rate_limit(limits: limits)
536
+
537
+ normalized = PWN::AI.normalize_plan_usage(
538
+ used: used_tokens,
539
+ limit: limit_tokens,
540
+ source: 'organizations/usage_report',
541
+ engine: :anthropic
542
+ )
543
+ return normalized if normalized[:available]
544
+
545
+ { available: false, engine: :anthropic }
546
+ rescue StandardError
547
+ { available: false, engine: :anthropic }
548
+ end
549
+
550
+ private_class_method def self.plan_usage_credentials?
551
+ engine = PWN::Env.dig(:ai, :anthropic) || {}
552
+ oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : {}
553
+ real_config_value?(value: engine[:key]) ||
554
+ real_config_value?(value: oauth[:bearer_token]) ||
555
+ real_config_value?(value: oauth[:refresh_token])
556
+ end
557
+
558
+ private_class_method def self.parse_plan_usage_json(opts = {})
559
+ raw = opts[:raw]
560
+ return nil if raw.nil?
561
+
562
+ parsed = JSON.parse(raw.to_s, symbolize_names: true)
563
+ parsed.is_a?(Hash) ? parsed : nil
564
+ rescue JSON::ParserError, TypeError
565
+ nil
566
+ end
567
+
568
+ private_class_method def self.sum_usage_tokens(opts = {})
569
+ report = opts[:report]
570
+ return nil unless report.is_a?(Hash)
571
+
572
+ buckets = report[:data] || report[:usage] || []
573
+ total = 0
574
+ Array(buckets).each do |bucket|
575
+ next unless bucket.is_a?(Hash)
576
+
577
+ results = bucket[:results] || bucket[:usage] || [bucket]
578
+ Array(results).each do |row|
579
+ next unless row.is_a?(Hash)
580
+
581
+ %i[
582
+ uncached_input_tokens
583
+ input_tokens
584
+ cache_read_input_tokens
585
+ cache_creation_input_tokens
586
+ output_tokens
587
+ ].each { |k| total += row[k].to_i }
588
+ end
589
+ end
590
+ total.positive? ? total : nil
591
+ end
592
+
593
+ private_class_method def self.first_rate_limit(opts = {})
594
+ limits = opts[:limits]
595
+ return nil unless limits.is_a?(Hash)
596
+
597
+ Array(limits[:data]).each do |group|
598
+ next unless group.is_a?(Hash)
599
+
600
+ Array(group[:limits]).each do |lim|
601
+ next unless lim.is_a?(Hash)
602
+
603
+ type = lim[:type].to_s
604
+ next unless type.match?(/tokens_per|input_tokens|output_tokens/)
605
+
606
+ v = lim[:value].to_f
607
+ return v if v.positive?
608
+ end
609
+ end
610
+ nil
611
+ end
612
+
491
613
  # ----------------------------------------------------------------------
492
614
  # Native tool-calling adapter for PWN::AI::Agent::Loop.
493
615
  #
@@ -832,6 +954,8 @@ module PWN
832
954
  puts "USAGE:
833
955
  models = #{self}.get_models
834
956
 
957
+ usage = #{self}.get_plan_usage
958
+
835
959
  # One-time Claude Pro/Max OAuth enrollment (PKCE paste flow):
836
960
  bearer = #{self}.obtain_oauth_bearer_token
837
961
  # Subsequent runs silently refresh via ai.anthropic.oauth.refresh_token
data/lib/pwn/ai/gemini.rb CHANGED
@@ -118,6 +118,19 @@ module PWN
118
118
  raise e
119
119
  end
120
120
 
121
+ # Supported Method Parameters::
122
+ # usage = PWN::AI::Gemini.get_plan_usage(
123
+ # timeout: 'optional - seconds (default 8)'
124
+ # )
125
+ #
126
+ # Gemini Generative Language has no subscription-usage endpoint that
127
+ # returns used-vs-limit for an API key. Cached-miss / unavailable so
128
+ # the PS1 shows ∞ unless the operator later injects used/limit.
129
+ public_class_method def self.get_plan_usage(opts = {})
130
+ _timeout = opts[:timeout] || 8
131
+ { available: false, engine: :gemini }
132
+ end
133
+
121
134
  # ----------------------------------------------------------------------
122
135
  # Native tool-calling adapter for PWN::AI::Agent::Loop.
123
136
  #
@@ -432,6 +445,8 @@ module PWN
432
445
  puts "USAGE:
433
446
  models = #{self}.get_models
434
447
 
448
+ usage = #{self}.get_plan_usage
449
+
435
450
  response = #{self}.chat(
436
451
  request: 'required - message to Gemini',
437
452
  model: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:gemini][:model])',