pwn 0.5.771 → 0.5.772

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a5ecce60b421a59e80f691710dd5ae25cd79d083b59207b25baccf8fe8e5b35
4
- data.tar.gz: 725ceb379d5436565b707e29d26e61e9cc10262b31e7cec537aec0e210d77fea
3
+ metadata.gz: c44656eba8412dd81f267cedd8f40d1954a9f51e528dddb1cc801084dff4d6b7
4
+ data.tar.gz: 2d3416bf7961deffcb1392bb2a18982bfacb944f9ca70724fb8e5b1d3b0e9eff
5
5
  SHA512:
6
- metadata.gz: b94c6d9ca4ebb2cf3835ed1f6951a15eae48e3484966e272861105a1c361964fd90235731e8c9ccfd10cfa03af736b42d69edb990b17aed37cd1c1504954ed11
7
- data.tar.gz: 39dd6eac404a8f55906c56f9dda74836bd9b934374eea9a8168d556b383ccfd69722b1a16debc893d20a0e58f37adfce54da11fef999a0666bf4f9a4f2258863
6
+ metadata.gz: d5c9dd294819d01c2c00878a96a911ef2f3964c112e5dcbc81bf46a4779ca476d433de0b5e9abdd2de25cfed8cf4eabb3581db6524ebbce06376b3924c73b667
7
+ data.tar.gz: 4f2e0a4f58d32a69a23a2f3226c1eb6a041ce45f1244a2d57f6b40b12d1376bd843e25df82487edb23e8859856ccd2e81e9f2b35fcf856948d82b670492f2a96
@@ -336,15 +336,22 @@ OpenAI options come from the model catalog's `supported_reasoning_levels` and
336
336
  while typing. When metadata is absent, explicit documented GPT-5, GPT-5.1/5.2/
337
337
  5.4/5.5 and GPT-6 Astra API contracts provide fallback options. Astra never offers
338
338
  `none`. Grok 3 Mini and Grok 4.5/4.6/4.7 use their documented effort levels.
339
- Unknown models without capability metadata and Anthropic, Gemini, Ollama and
340
- OpenWebUI do not offer an effort list: those adapters do not consume this effort
341
- setting (their thinking controls, where present, are different).
339
+ Anthropic prompts for documented effort models and sends `output_config.effort`
340
+ with adaptive thinking. Gemini 3 and Gemini 4 prompt for `thinkingLevel`; Gemini
341
+ 2.5 stays on its thinking budget and is not an effort menu. Ollama and Open WebUI
342
+ prompt only when `/api/show` (or the proxied `ollama/api/show`) publishes named
343
+ `thinking.values`. Boolean on/off thinking is not an effort list. Unknown models
344
+ without that metadata do not offer a list.
342
345
 
343
346
  Acceptance merges `ai.active`, the selected engine's `model` and its existing
344
347
  `reasoning_effort` field into the encrypted vault; other settings are retained.
345
- Missing decryptor files leave changes session-only. No schema migration is needed.
346
- Capability references: [OpenAI model pages](https://developers.openai.com/api/docs/models),
347
- [xAI reasoning](https://docs.x.ai/developers/model-capabilities/text/reasoning).
348
+ Missing decryptor files leave changes session-only. Schema 10 backfills a missing
349
+ `reasoning_effort` key for Anthropic, Gemini, Ollama, and Open WebUI without
350
+ replacing an existing value. Capability references: [OpenAI model pages](https://developers.openai.com/api/docs/models),
351
+ [xAI reasoning](https://docs.x.ai/developers/model-capabilities/text/reasoning),
352
+ [Anthropic effort](https://platform.claude.com/docs/en/build-with-claude/effort),
353
+ [Gemini thinking](https://ai.google.dev/gemini-api/docs/generate-content/thinking),
354
+ [Ollama thinking](https://docs.ollama.com/capabilities/thinking).
348
355
 
349
356
  Settings and new requests are rejected while a request is running, rather than
350
357
  mutating its context concurrently. Network-capable local commands (`/mcp`,
@@ -567,6 +567,7 @@ module PWN
567
567
  # model: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',
568
568
  # temp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',
569
569
  # max_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',
570
+ # reasoning_effort: 'optional - output_config.effort when the model supports it',
570
571
  # timeout: 'optional - seconds (default 900)',
571
572
  # spinner: 'optional - display spinner (default false)'
572
573
  # )
@@ -613,6 +614,11 @@ module PWN
613
614
  end
614
615
  http_body[:tool_choice] = anth_tool_choice(choice: opts[:tool_choice], model: model) if opts[:tool_choice]
615
616
  end
617
+ effort = opts[:reasoning_effort].to_s
618
+ if !effort.empty? && effort != 'none' && reasoning_efforts(model: model)[:efforts].include?(effort)
619
+ http_body[:output_config] = { effort: effort }
620
+ http_body[:thinking] = { type: 'adaptive' } unless opts[:think] == false
621
+ end
616
622
 
617
623
  response = anthropic_rest_call(
618
624
  http_method: :post,
@@ -828,6 +834,41 @@ module PWN
828
834
  }
829
835
  end
830
836
 
837
+ private_class_method def self.reasoning_efforts(opts = {})
838
+ model = opts[:model].to_s.downcase
839
+ stamp = '(?:-\d{8})?(?:\z|-)'
840
+ return { efforts: [], default: nil } unless model.match?(/\Aclaude-/i)
841
+
842
+ patterns = [
843
+ /claude-opus-4-[5-8]#{stamp}/i,
844
+ /claude-sonnet-4-6#{stamp}/i,
845
+ /claude-(?:opus|sonnet)-5(?:-5)?#{stamp}/i,
846
+ /claude-haiku-5-5#{stamp}/i,
847
+ /claude-fable-5(?:-1)?#{stamp}/i,
848
+ /claude-mythos-(?:5(?:-1)?|preview)#{stamp}/i
849
+ ]
850
+ return { efforts: [], default: nil } unless patterns.any? { |pattern| model.match?(pattern) }
851
+
852
+ efforts = %w[low medium high]
853
+ newer = [
854
+ /claude-opus-4-[78]#{stamp}/i,
855
+ /claude-(?:opus|sonnet)-5(?:-5)?#{stamp}/i,
856
+ /claude-haiku-5-5#{stamp}/i,
857
+ /claude-fable-5(?:-1)?#{stamp}/i,
858
+ /claude-mythos-(?:5(?:-1)?|preview)#{stamp}/i
859
+ ]
860
+ efforts << 'xhigh' if newer.any? { |pattern| model.match?(pattern) }
861
+ claude5 = [
862
+ /claude-(?:opus|sonnet)-5(?:-5)?#{stamp}/i,
863
+ /claude-haiku-5-5#{stamp}/i,
864
+ /claude-fable-5(?:-1)?#{stamp}/i,
865
+ /claude-mythos-(?:5(?:-1)?|preview)#{stamp}/i
866
+ ]
867
+ efforts << 'max' if claude5.any? { |pattern| model.match?(pattern) }
868
+ default = model.match?(/claude-(?:opus-5-5|haiku-5-5)#{stamp}/i) ? 'medium' : 'high'
869
+ { efforts: efforts, default: default }
870
+ end
871
+
831
872
  private_class_method def self.anth_tool_choice(opts = {})
832
873
  choice = opts[:choice]
833
874
  mapped = case choice
@@ -1023,6 +1064,8 @@ module PWN
1023
1064
  model: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',
1024
1065
  temp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',
1025
1066
  max_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',
1067
+ reasoning_effort: 'optional - output_config.effort when the model supports it',
1068
+ think: 'optional - false omits adaptive thinking when an effort is set',
1026
1069
  timeout: 'optional - seconds (default 900)',
1027
1070
  spinner: 'optional - display spinner (default false)'
1028
1071
  )
data/lib/pwn/ai/gemini.rb CHANGED
@@ -213,7 +213,8 @@ module PWN
213
213
  maxOutputTokens: opts[:max_tokens] || 8192
214
214
  }
215
215
  }
216
- http_body[:generationConfig][:thinkingConfig] = { includeThoughts: true } unless opts[:think] == false
216
+ thinking = gemini_thinking_config(model: model, think: opts[:think], reasoning_effort: opts[:reasoning_effort])
217
+ http_body[:generationConfig][:thinkingConfig] = thinking if thinking
217
218
  if system_str && !system_str.empty?
218
219
  if defined?(PWN::AI::Agent::PromptCache) &&
219
220
  PWN::AI::Agent::PromptCache.enabled?(engine: :gemini)
@@ -369,6 +370,29 @@ module PWN
369
370
  }
370
371
  end
371
372
 
373
+ private_class_method def self.reasoning_efforts(opts = {})
374
+ model = opts[:model].to_s.downcase
375
+ return { efforts: [], default: nil } unless model.match?(/\Agemini-[34]/)
376
+ return { efforts: [], default: nil } if model.match?(/embed|imagen|veo|aqa|tts/)
377
+
378
+ if model.include?('flash')
379
+ { efforts: %w[minimal low medium high], default: model.include?('flash-lite') ? 'minimal' : 'medium' }
380
+ else
381
+ { efforts: %w[low medium high], default: 'medium' }
382
+ end
383
+ end
384
+
385
+ private_class_method def self.gemini_thinking_config(opts = {})
386
+ return nil if opts[:think] == false && opts[:reasoning_effort].to_s.empty?
387
+
388
+ config = {}
389
+ config[:includeThoughts] = true unless opts[:think] == false
390
+ effort = opts[:reasoning_effort].to_s
391
+ known = reasoning_efforts(model: opts[:model])[:efforts]
392
+ config[:thinkingLevel] = effort if known.include?(effort)
393
+ config.empty? ? nil : config
394
+ end
395
+
372
396
  private_class_method def self.gemini_tool_config(opts = {})
373
397
  choice = opts[:choice]
374
398
  case choice
@@ -509,7 +533,8 @@ module PWN
509
533
  max_tokens: 'optional - maxOutputTokens (defaults to 8192)',
510
534
  timeout: 'optional - seconds (default 900)',
511
535
  spinner: 'optional - display spinner (default false)',
512
- think: 'optional - true includes Gemini thought parts (default true; false disables)'
536
+ think: 'optional - true includes Gemini thought parts (default true; false disables)',
537
+ reasoning_effort: 'optional - thinkingLevel when the model supports named levels'
513
538
  )
514
539
 
515
540
  # Run chat and return its result
data/lib/pwn/ai/grok.rb CHANGED
@@ -153,7 +153,6 @@ module PWN
153
153
  # bearer lives in-session only.
154
154
  private_class_method def self.persist_oauth_to_vault(opts = {})
155
155
  oauth = opts[:oauth]
156
- return false if Thread.current[:pwn_request_runtime]
157
156
  return false unless oauth.is_a?(Hash)
158
157
  return false unless real_config_value?(value: oauth[:bearer_token])
159
158
 
@@ -230,9 +229,9 @@ module PWN
230
229
  # 2. User opens verification_uri_complete in a browser and approves.
231
230
  # 3. Poll POST /oauth2/token (grant_type=device_code) until access_token.
232
231
  #
233
- # On success the access_token + refresh_token are written back into the
234
- # passed opts/oauth Hash (so PWN::Env[:ai][:grok][:oauth] is live-cached)
235
- # and the operator is told exactly what to persist via pwn-vault.
232
+ # On success the access_token + refresh_token are written into the live
233
+ # request runtime (provider_config is a copy) and into the encrypted
234
+ # vault when the decryptor is present. Tokens are not printed.
236
235
  public_class_method def self.obtain_oauth_bearer_token(opts = {})
237
236
  client_id = real_config_value?(value: opts[:client_id]) ? opts[:client_id] : XAI_OAUTH_CLIENT_ID
238
237
  scope = real_config_value?(value: opts[:scope]) ? opts[:scope] : XAI_OAUTH_SCOPE
@@ -318,14 +317,14 @@ module PWN
318
317
  opts[:refresh_token] = refresh_token if refresh_token
319
318
  opts[:expires_at] = Time.now.to_i + data['expires_in'].to_i if data['expires_in']
320
319
 
320
+ # provider_config returns a Marshal copy. Mutating that copy does not
321
+ # survive the next hop, so a dead vault refresh token is reused and
322
+ # the device flow starts again.
323
+ sync_oauth_into_env(oauth: opts)
324
+ persisted = persist_oauth_to_vault(oauth: opts)
325
+
321
326
  puts "\n[*] SUCCESS: xAI Grok OAuth bearer obtained via device_code grant."
322
- puts ' Cached in-memory for this pwn / pwn-ai process.'
323
- puts ''
324
- puts ' TO MAKE THIS PERMANENT (recommended -- one-time), store via pwn-vault:'
325
- puts " ai.grok.oauth.refresh_token = #{refresh_token}" if refresh_token
326
- puts " ai.grok.oauth.bearer_token = #{access_token}"
327
- puts ' On future runs the refresh_token alone is enough -- PWN::AI::Grok will'
328
- puts ' silently exchange it for a fresh access_token (no browser, no prompt).'
327
+ puts(persisted ? ' Tokens saved to the encrypted vault; future sessions can refresh automatically.' : ' Tokens available in this session only; encrypted vault persistence was unavailable.')
329
328
  puts ''
330
329
 
331
330
  access_token
data/lib/pwn/ai/ollama.rb CHANGED
@@ -582,6 +582,42 @@ module PWN
582
582
  false
583
583
  end
584
584
 
585
+ private_class_method def self.thinking_controls(opts = {})
586
+ name = opts[:model].to_s.strip
587
+ return { efforts: [], default: nil } if name.empty?
588
+
589
+ names = [name]
590
+ names << "#{name}:latest" unless name.include?(':')
591
+ names.each do |candidate|
592
+ raw = ollama_rest_call(
593
+ http_method: :post, rest_call: 'api/show',
594
+ http_body: { model: candidate }, timeout: 8, quiet: true, non_interactive: true
595
+ )
596
+ parsed = raw.is_a?(Hash) ? raw : JSON.parse(raw.to_s)
597
+ thinking = parsed[:thinking] || parsed['thinking']
598
+ next unless thinking.is_a?(Hash)
599
+
600
+ values = Array(thinking[:values] || thinking['values'])
601
+ efforts = values.map(&:to_s).reject { |value| %w[true false].include?(value.downcase) }
602
+ next if efforts.empty?
603
+
604
+ default = (thinking[:default] || thinking['default']).to_s
605
+ default = nil unless efforts.include?(default)
606
+ return { efforts: efforts, default: default }
607
+ end
608
+ { efforts: [], default: nil }
609
+ rescue StandardError
610
+ { efforts: [], default: nil }
611
+ end
612
+
613
+ private_class_method def self.think_wire(opts = {})
614
+ effort = opts[:reasoning_effort].to_s
615
+ return false if opts[:think] == false || %w[none false].include?(effort)
616
+ return true if effort.empty? || effort == 'true'
617
+
618
+ effort
619
+ end
620
+
585
621
  # Supported Method Parameters::
586
622
  # response = PWN::AI::Ollama.chat_with_tools(
587
623
  # messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
@@ -634,7 +670,7 @@ module PWN
634
670
  http_body[:format] = fmt unless fmt.nil? || fmt.to_s.empty?
635
671
  end
636
672
  http_body[:tool_choice] = opts[:tool_choice] if opts[:tool_choice]
637
- http_body[:think] = true unless opts[:think] == false
673
+ http_body[:think] = think_wire(reasoning_effort: opts[:reasoning_effort], think: opts[:think])
638
674
 
639
675
  response = ollama_rest_call(
640
676
  http_method: :post,
@@ -800,7 +836,8 @@ module PWN
800
836
  temp: 'optional - temperature (defaults to PWN::Env[:ai][:ollama][:temp] || 1)',
801
837
  timeout: 'optional - seconds (default 900)',
802
838
  spinner: 'optional - display spinner (default false)',
803
- think: 'optional - true enables Ollama think (default true; false disables)'
839
+ think: 'optional - true enables Ollama think (default true; false disables)',
840
+ reasoning_effort: 'optional - think level from /api/show thinking.values'
804
841
  )
805
842
 
806
843
  # Run chat and return its result
@@ -614,6 +614,42 @@ module PWN
614
614
  false
615
615
  end
616
616
 
617
+ private_class_method def self.thinking_controls(opts = {})
618
+ name = opts[:model].to_s.strip
619
+ return { efforts: [], default: nil } if name.empty?
620
+
621
+ names = [name]
622
+ names << "#{name}:latest" unless name.include?(':')
623
+ names.each do |candidate|
624
+ raw = openwebui_rest_call(
625
+ http_method: :post, rest_call: 'ollama/api/show',
626
+ http_body: { model: candidate }, timeout: 8, quiet: true, non_interactive: true
627
+ )
628
+ parsed = raw.is_a?(Hash) ? raw : JSON.parse(raw.to_s)
629
+ thinking = parsed[:thinking] || parsed['thinking']
630
+ next unless thinking.is_a?(Hash)
631
+
632
+ values = Array(thinking[:values] || thinking['values'])
633
+ efforts = values.map(&:to_s).reject { |value| %w[true false].include?(value.downcase) }
634
+ next if efforts.empty?
635
+
636
+ default = (thinking[:default] || thinking['default']).to_s
637
+ default = nil unless efforts.include?(default)
638
+ return { efforts: efforts, default: default }
639
+ end
640
+ { efforts: [], default: nil }
641
+ rescue StandardError
642
+ { efforts: [], default: nil }
643
+ end
644
+
645
+ private_class_method def self.think_wire(opts = {})
646
+ effort = opts[:reasoning_effort].to_s
647
+ return false if opts[:think] == false || %w[none false].include?(effort)
648
+ return true if effort.empty? || effort == 'true'
649
+
650
+ effort
651
+ end
652
+
617
653
  # Supported Method Parameters::
618
654
  # response = PWN::AI::OpenWebUI.chat_with_tools(
619
655
  # messages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',
@@ -667,7 +703,7 @@ module PWN
667
703
  http_body[:format] = fmt unless fmt.nil? || fmt.to_s.empty?
668
704
  end
669
705
  http_body[:tool_choice] = opts[:tool_choice] if opts[:tool_choice]
670
- http_body[:think] = true unless opts[:think] == false
706
+ http_body[:think] = think_wire(reasoning_effort: opts[:reasoning_effort], think: opts[:think])
671
707
 
672
708
  response = openwebui_rest_call(
673
709
  http_method: :post,
@@ -830,7 +866,8 @@ module PWN
830
866
  temp: 'optional - temperature (defaults to PWN::Env[:ai][:openwebui][:temp] || 1)',
831
867
  timeout: 'optional - seconds (default 900)',
832
868
  spinner: 'optional - display spinner (default false)',
833
- think: 'optional - true enables backend think (default true; false disables)'
869
+ think: 'optional - true enables backend think (default true; false disables)',
870
+ reasoning_effort: 'optional - think level when the proxied model publishes named levels'
834
871
  )
835
872
 
836
873
  # Run chat and return its result
data/lib/pwn/config.rb CHANGED
@@ -80,6 +80,7 @@ module PWN
80
80
  base_uri: 'optional - Base URI for Ollama server (default http://127.0.0.1:11434)',
81
81
  key: 'optional - bearer token only if a reverse-proxy sits in front of Ollama (stock ollama needs none)',
82
82
  model: 'required - Ollama model tag to use',
83
+ reasoning_effort: 'optional - think level published by /api/show',
83
84
  embed_model: 'optional - embedding model for PWN::MemoryIndex (default nomic-embed-text)',
84
85
  system_role_content: 'You are an ethically hacking Ollama agent.',
85
86
  temp: 'optional - Ollama temperature',
@@ -102,6 +103,7 @@ module PWN
102
103
  base_uri: 'required - Base URI for Open WebUI - e.g. https://openwebui.local',
103
104
  key: 'required - Open WebUI API Key Under Settings >> Account >> JWT Token',
104
105
  model: 'required - model id/tag exposed by Open WebUI',
106
+ reasoning_effort: 'optional - think level when the proxied model publishes named levels',
105
107
  embed_model: 'optional - embedding model for PWN::MemoryIndex when ollama direct is unset (default nomic-embed-text)',
106
108
  system_role_content: 'You are an ethically hacking Open WebUI agent.',
107
109
  temp: 'optional - Open WebUI temperature',
@@ -118,6 +120,7 @@ module PWN
118
120
  base_uri: 'optional - Base URI for Anthropic - Use private base OR defaults to https://api.anthropic.com/v1',
119
121
  key: 'required - Anthropic API Key',
120
122
  model: 'optional - Anthropic model id to use (see provider docs for currently-supported ids)',
123
+ reasoning_effort: 'optional - output_config.effort when the model supports it',
121
124
  system_role_content: 'You are an ethically hacking Anthropic agent.',
122
125
  temp: 'optional - Anthropic temperature',
123
126
  max_tokens: 'optional - Max output tokens per response (default 8192). Raise if tool calls truncate.',
@@ -145,6 +148,7 @@ module PWN
145
148
  base_uri: 'optional - Base URI for Gemini - Use private base OR defaults to https://generativelanguage.googleapis.com/v1beta',
146
149
  key: 'required - Google Gemini API Key',
147
150
  model: 'optional - Gemini model id to use (see provider docs for currently-supported ids)',
151
+ reasoning_effort: 'optional - thinkingLevel when the model supports named levels',
148
152
  system_role_content: 'You are an ethically hacking Gemini agent.',
149
153
  temp: 'optional - Gemini temperature',
150
154
  think: true,
data/lib/pwn/migrate.rb CHANGED
@@ -49,7 +49,7 @@ module PWN
49
49
  # Bump this whenever the shape of any file under ~/.pwn changes in a
50
50
  # way that requires a one-time transform. Add the transform as an
51
51
  # entry in MIGRATIONS keyed by the NEW schema number.
52
- SCHEMA_VERSION = 9
52
+ SCHEMA_VERSION = 10
53
53
 
54
54
  OK = "\e[32mok\e[0m"
55
55
  BAD = "\e[31mFAIL\e[0m"
@@ -265,6 +265,10 @@ module PWN
265
265
  9 => lambda { |_root, io|
266
266
  io.puts ' · pwn.yaml plugins.meshtastic.channel short_name'
267
267
  PWN::Migrate.backfill_vault(io: io)
268
+ },
269
+ 10 => lambda { |_root, io|
270
+ io.puts ' · pwn.yaml ai reasoning_effort for anthropic, gemini, ollama, openwebui'
271
+ PWN::Migrate.backfill_vault(io: io)
268
272
  }
269
273
  }.freeze
270
274
 
@@ -758,6 +758,22 @@ module PWN
758
758
  efforts = %w[low medium high xhigh] if %w[grok-4.6 grok-4.7].include?(model)
759
759
  efforts = %w[low medium high] if model == 'grok-4.5'
760
760
  efforts = %w[low high] if %w[grok-3-mini grok-3-mini-fast].include?(model)
761
+ elsif engine == 'anthropic'
762
+ selection = PWN::AI::Anthropic.send(:reasoning_efforts, model: model)
763
+ efforts = selection[:efforts]
764
+ default = selection[:default] || default
765
+ elsif engine == 'gemini'
766
+ selection = PWN::AI::Gemini.send(:reasoning_efforts, model: model)
767
+ efforts = selection[:efforts]
768
+ default = selection[:default] || default
769
+ elsif engine == 'ollama'
770
+ selection = PWN::AI::Ollama.send(:thinking_controls, model: model)
771
+ efforts = selection[:efforts]
772
+ default = selection[:default] || default
773
+ elsif engine == 'openwebui'
774
+ selection = PWN::AI::OpenWebUI.send(:thinking_controls, model: model)
775
+ efforts = selection[:efforts]
776
+ default = selection[:default] || default
761
777
  end
762
778
  current = PWN::Env.dig(:ai, engine.to_sym, :reasoning_effort).to_s
763
779
  efforts.delete('none') if engine == 'openai' && model.start_with?('gpt-6-astra')
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.771'
4
+ VERSION = '0.5.772'
5
5
  end
@@ -320,16 +320,18 @@ RSpec.describe 'Swarm solve' do # rubocop:disable Metrics/BlockLength -- end-to-
320
320
  end
321
321
 
322
322
  it 'reaps a writer command when its role deadline expires' do
323
+ role_started = nil
323
324
  @hook = lambda do |handoff, _answer|
324
325
  next unless handoff[:phase] == 'build'
325
326
 
326
327
  @isolated = handoff[:workspace]
328
+ role_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
327
329
  PWN::AI::Agent::Dispatch.call(tool_call: { function: { name: 'shell', arguments: JSON.generate(command: 'sleep 4', timeout: 5) } })
328
330
  end
329
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
330
331
  out = PWN::AI::Agent::Swarm.solve(request: 'Write hello', workspace: @workspace, worktree: true, rounds: 1, role_timeout: 1)
331
332
  expect(out[:status]).to eq('incomplete')
332
- expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
333
+ # Worktree checkout is outside the role deadline. Time the writer, not setup.
334
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - role_started).to be < 3
333
335
  expect(File).not_to exist(File.join(@workspace, 'greeting.txt'))
334
336
  end
335
337
 
@@ -180,4 +180,39 @@ describe PWN::AI::Grok do
180
180
  src = File.read(described_class.method(:chat_with_tools).source_location.first)
181
181
  expect(src).to match(/HttpRetry\.report_event|Log\.progress/)
182
182
  end
183
+
184
+ it 'keeps a device grant in the request runtime and does not print the tokens' do
185
+ runtime = Class.new do
186
+ def initialize
187
+ @config = { grok: { oauth: { refresh_token: 'dead-refresh', bearer_token: 'expired-bearer' } } }
188
+ end
189
+
190
+ def provider_config(engine)
191
+ Marshal.load(Marshal.dump(@config[engine.to_sym] || {}))
192
+ end
193
+
194
+ def update_provider_oauth(engine, oauth)
195
+ @config[engine.to_sym][:oauth] = Marshal.load(Marshal.dump(oauth))
196
+ :updated
197
+ end
198
+ end.new
199
+ Thread.current[:pwn_request_runtime] = runtime
200
+ allow(described_class).to receive(:sleep)
201
+ allow(described_class).to receive(:persist_oauth_to_vault).and_return(false)
202
+ allow(RestClient).to receive(:post).and_return(
203
+ double(body: { device_code: 'dev', user_code: 'CODE', verification_uri: 'https://example.invalid/device', interval: 0, expires_in: 30 }.to_json),
204
+ double(body: { access_token: 'new-access', refresh_token: 'new-refresh', expires_in: 3600 }.to_json)
205
+ )
206
+ output = StringIO.new
207
+ original = $stdout
208
+ $stdout = output
209
+ copy = runtime.provider_config(:grok)[:oauth]
210
+ expect(described_class.obtain_oauth_bearer_token(copy)).to eq('new-access')
211
+ expect(runtime.provider_config(:grok)[:oauth]).to include(bearer_token: 'new-access', refresh_token: 'new-refresh')
212
+ expect(output.string).not_to include('new-access', 'new-refresh', 'dead-refresh')
213
+ expect(output.string).to include('session only')
214
+ ensure
215
+ $stdout = original if original
216
+ Thread.current[:pwn_request_runtime] = nil
217
+ end
183
218
  end
@@ -80,7 +80,7 @@ describe PWN::Migrate do
80
80
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 2))
81
81
  expect(described_class.needed?).to be(true)
82
82
  result = described_class.run(fix: false, backup: false, io: io)
83
- expect(result[:applied_migrations]).to eq([3, 4, 5, 6, 7, 8, 9])
83
+ expect(result[:applied_migrations]).to eq([3, 4, 5, 6, 7, 8, 9, 10])
84
84
  expected = Marshal.load(Marshal.dump(original))
85
85
  expected['custom']['model'] = nil
86
86
  expect(YAML.safe_load_file(path)).to eq(expected)
@@ -189,7 +189,7 @@ describe PWN::Migrate do
189
189
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 3))
190
190
  expect(described_class.needed?).to be(true)
191
191
  result = described_class.run(fix: true, backup: false, io: io)
192
- expect(result[:applied_migrations]).to eq([4, 5, 6, 7, 8, 9])
192
+ expect(result[:applied_migrations]).to eq([4, 5, 6, 7, 8, 9, 10])
193
193
  creds = YAML.safe_load_file(dec, symbolize_names: true)
194
194
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
195
195
  expect(user[:ai][:active]).to eq('grok')
@@ -215,7 +215,7 @@ describe PWN::Migrate do
215
215
  PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
216
216
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 4))
217
217
  result = described_class.run(fix: true, backup: false, io: io)
218
- expect(result[:applied_migrations]).to eq([5, 6, 7, 8, 9])
218
+ expect(result[:applied_migrations]).to eq([5, 6, 7, 8, 9, 10])
219
219
  creds = YAML.safe_load_file(dec, symbolize_names: true)
220
220
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
221
221
  expect(user.dig(:ai, :agent, :skill_review)).to eq('off')
@@ -240,7 +240,7 @@ describe PWN::Migrate do
240
240
  File.write(yaml, YAML.dump(stale).gsub(/^(\s*):/, '\1'))
241
241
  PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
242
242
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 5))
243
- expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([6, 7, 8, 9])
243
+ expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([6, 7, 8, 9, 10])
244
244
  creds = YAML.safe_load_file(dec, symbolize_names: true)
245
245
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
246
246
  theme = user.dig(:ai, :tui, :theme)
@@ -287,7 +287,7 @@ describe PWN::Migrate do
287
287
  File.write(yaml, YAML.dump(stale).gsub(/^(\s*):/, '\1'))
288
288
  PWN::Plugins::Vault.create(file: yaml, decryptor_file: dec)
289
289
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 8))
290
- expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([9])
290
+ expect(described_class.run(fix: true, backup: false, io: io)[:applied_migrations]).to eq([9, 10])
291
291
  creds = YAML.safe_load_file(dec, symbolize_names: true)
292
292
  user = PWN::Plugins::Vault.dump(file: yaml, key: creds[:key], iv: creds[:iv])
293
293
  long_fast = user.dig(:plugins, :meshtastic, :channel, :LongFast)
@@ -97,13 +97,35 @@ describe PWN::Plugins::REPL do # rubocop:disable Metrics/BlockLength
97
97
  end
98
98
 
99
99
  it 'does not offer effort controls for unsupported models' do
100
+ allow(PWN::AI::Ollama).to receive(:thinking_controls).and_return({ efforts: [], default: nil })
101
+ allow(PWN::AI::OpenWebUI).to receive(:thinking_controls).and_return({ efforts: [], default: nil })
100
102
  expect(described_class.pwn_ai_reasoning_selection(engine: 'grok', model: 'grok-4')[:efforts]).to eq([])
101
103
  expect(described_class.pwn_ai_reasoning_selection(engine: 'openai', model: 'gpt-4o')[:efforts]).to eq([])
102
- %w[anthropic gemini ollama openwebui].each do |engine|
104
+ expect(described_class.pwn_ai_reasoning_selection(engine: 'anthropic', model: 'claude-3-haiku')[:efforts]).to eq([])
105
+ expect(described_class.pwn_ai_reasoning_selection(engine: 'gemini', model: 'gemini-2.5-pro')[:efforts]).to eq([])
106
+ %w[ollama openwebui].each do |engine|
103
107
  expect(described_class.pwn_ai_reasoning_selection(engine: engine, model: 'fixture')[:efforts]).to eq([])
104
108
  end
105
109
  end
106
110
 
111
+ it 'offers named effort levels only for models that publish them' do
112
+ anthropic = described_class.pwn_ai_reasoning_selection(engine: 'anthropic', model: 'claude-sonnet-4-6')
113
+ expect(anthropic[:efforts]).to eq(%w[low medium high])
114
+ expect(anthropic[:default]).to eq('high')
115
+ newer = described_class.pwn_ai_reasoning_selection(engine: 'anthropic', model: 'claude-opus-5-5')
116
+ expect(newer[:efforts]).to eq(%w[low medium high xhigh max])
117
+ expect(newer[:default]).to eq('medium')
118
+ gemini = described_class.pwn_ai_reasoning_selection(engine: 'gemini', model: 'gemini-4-argon')
119
+ expect(gemini[:efforts]).to eq(%w[low medium high])
120
+ expect(gemini[:default]).to eq('medium')
121
+ flash = described_class.pwn_ai_reasoning_selection(engine: 'gemini', model: 'gemini-3-flash')
122
+ expect(flash[:efforts]).to eq(%w[minimal low medium high])
123
+ allow(PWN::AI::Ollama).to receive(:thinking_controls).and_return({ efforts: %w[low medium high], default: 'medium' })
124
+ ollama = described_class.pwn_ai_reasoning_selection(engine: 'ollama', model: 'gpt-oss')
125
+ expect(ollama[:efforts]).to eq(%w[low medium high])
126
+ expect(ollama[:default]).to eq('medium')
127
+ end
128
+
107
129
  it 'cancels without changing any configuration or persisting' do
108
130
  PWN::Env[:ai] = { active: 'ollama', ollama: { model: 'old' }, grok: { model: 'old', reasoning_effort: 'high' } }
109
131
  before = Marshal.dump(PWN::Env[:ai])
@@ -112,9 +134,10 @@ describe PWN::Plugins::REPL do # rubocop:disable Metrics/BlockLength
112
134
  expect(Marshal.dump(PWN::Env[:ai])).to eq(before)
113
135
  end
114
136
 
115
- it 'skips effort prompts and catalogs for engines without effort support' do
137
+ it 'skips effort prompts when the selected model publishes no named levels' do
116
138
  PWN::Env[:ai] = { active: 'ollama', ollama: { model: 'old', think: false } }
117
139
  allow(described_class).to receive(:persist_ai_selection).and_return(false)
140
+ allow(PWN::AI::Ollama).to receive(:thinking_controls).and_return({ efforts: [], default: nil })
118
141
  expect(PWN::AI::OpenAI).not_to receive(:get_models)
119
142
  prompt = double('prompt')
120
143
  expect(prompt).not_to receive(:call)
@@ -24,7 +24,7 @@ RSpec.describe 'Bundled session cleanup' do
24
24
  original = PWN::Cron.create(name: 'session_cleanup', schedule: '12 9 * * *', ruby: 'custom guard', enabled: false)
25
25
  File.write(File.join(@tmp, '.schema'), JSON.generate(schema: 7))
26
26
  result = PWN::Migrate.run(fix: false, backup: false, io: StringIO.new)
27
- expect(result[:applied_migrations]).to eq([8, 9])
27
+ expect(result[:applied_migrations]).to eq([8, 9, 10])
28
28
  expect(PWN::Cron.list.fetch(original[:id])).to eq(original)
29
29
  expect(PWN::Cron.list.values.count { |job| job[:name] == 'session_cleanup' }).to eq(1)
30
30
  expect(File).to exist(File.join(PWN::Cron.cron_dir, 'session_cleanup.rb'))
@@ -15,7 +15,7 @@ module HarnessCLIFaults
15
15
  path = File.join(root, 'provider.json')
16
16
  File.write(path, JSON.generate(fixture))
17
17
  source = options[:source_root] || HarnessEvaluation::ROOT
18
- env = { 'HOME' => File.join(root, 'home'), 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
18
+ env = { 'HOME' => File.join(root, 'home'), 'PATH' => HarnessEvaluation.isolated_path, 'LANG' => 'C.UTF-8',
19
19
  'GEM_HOME' => options[:gem_home] || Gem.dir, 'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR),
20
20
  'PWN_HARNESS_FIXTURE' => path, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
21
21
  'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json'), 'PWN_HARNESS_RUNTIME' => File.join(root, 'runtime'),
@@ -25,7 +25,12 @@ module HarnessCLIFaults
25
25
  command += ['-r', File.join(source, 'spec/support/harness/fault_preload.rb'),
26
26
  options[:executable] || File.join(source, 'bin/pwn-ai'), '--ai', prompt]
27
27
  ready_path = fixture[:await_cap_ready] ? env.fetch('PWN_HARNESS_CAP_READY') : nil
28
- report = HarnessEvaluation.capture(env, command, root, fixture.fetch(:wall_seconds, 35), ready_path: ready_path)
28
+ startup = options[:executable] ? 180 : 45
29
+ report = if ready_path
30
+ HarnessEvaluation.capture(env, command, root, fixture.fetch(:wall_seconds, 35), ready_path: ready_path, startup_seconds: startup)
31
+ else
32
+ HarnessEvaluation.agent_capture(env, command, root, fixture.fetch(:wall_seconds, 35), options)
33
+ end
29
34
  phase = fixture[:resume] ? 'resumed' : 'initial'
30
35
  File.write(File.join(root, "#{phase}-process.json"), JSON.pretty_generate(report.merge(command: command)))
31
36
  report
@@ -13,6 +13,13 @@ module HarnessEvaluation
13
13
  ROOT = File.expand_path('../../..', __dir__)
14
14
  MANIFEST = File.join(ROOT, 'spec/fixtures/harness/manifest.json')
15
15
 
16
+ # Scripted shell tasks call `ruby`. A stripped PATH must still find the suite
17
+ # interpreter: /usr/bin/ruby may be older or lack json, which fails only the
18
+ # held-out JSON task while File-only tasks pass.
19
+ def self.isolated_path
20
+ [File.dirname(RbConfig.ruby), '/usr/bin', '/bin'].uniq.join(File::PATH_SEPARATOR)
21
+ end
22
+
16
23
  def self.validate_parity!(left, right)
17
24
  fields = %w[provider model revision endpoint reasoning temperature max_iterations max_tokens wall_seconds tool_capabilities network]
18
25
  fields.each do |key|
@@ -99,6 +106,14 @@ module HarnessEvaluation
99
106
  latency_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - started }
100
107
  end
101
108
 
109
+ # Gem activation is not part of the scripted work cap. Packaged boots get a
110
+ # separate finite startup bound; a missing ready file is a capped boot, not a parser crash.
111
+ def self.agent_capture(env, command, cwd, seconds, options = {})
112
+ ready = File.join(cwd, 'harness-ready')
113
+ startup = options[:executable] ? 180 : 45
114
+ capture(env.merge('PWN_HARNESS_READY' => ready), command, cwd, seconds, ready_path: ready, startup_seconds: startup)
115
+ end
116
+
102
117
  def self.run(options)
103
118
  manifest_path = options[:source_root] ? File.join(options[:source_root], 'spec/fixtures/harness/manifest.json') : MANIFEST
104
119
  manifest_bytes = File.binread(manifest_path)
@@ -121,7 +136,7 @@ module HarnessEvaluation
121
136
  File.write(fixture, JSON.generate(responses: task.fetch('responses')))
122
137
  events = File.join(root, 'events.jsonl')
123
138
  fingerprint = File.join(root, 'fingerprint.json')
124
- env = { 'HOME' => home, 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
139
+ env = { 'HOME' => home, 'PATH' => isolated_path, 'LANG' => 'C.UTF-8',
125
140
  'GEM_HOME' => options[:gem_home] || Gem.dir,
126
141
  'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR),
127
142
  'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => events,
@@ -131,7 +146,7 @@ module HarnessEvaluation
131
146
  command = [ruby]
132
147
  command += ['-I', File.join(ROOT, 'lib')] unless options[:executable]
133
148
  command += ['-r', preload, executable, '--ai', task.fetch('prompt')]
134
- result = capture(env, command, root, manifest.fetch('budget').fetch('wall_seconds'))
149
+ result = HarnessEvaluation.agent_capture(env, command, root, manifest.fetch('budget').fetch('wall_seconds'), options)
135
150
  grade = grade(task, root, result)
136
151
  File.write(File.join(root, 'stdout.txt'), result.delete(:stdout))
137
152
  File.write(File.join(root, 'stderr.txt'), result.delete(:stderr))
@@ -144,7 +159,7 @@ module HarnessEvaluation
144
159
  artifact_root: root, command: command }.merge(result).merge(grade)
145
160
  end
146
161
  fault_root = Dir.mktmpdir('runtime-faults-', output_root)
147
- fault_env = { 'HOME' => fault_root, 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
162
+ fault_env = { 'HOME' => fault_root, 'PATH' => isolated_path, 'LANG' => 'C.UTF-8',
148
163
  'GEM_HOME' => options[:gem_home] || Gem.dir,
149
164
  'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR) }
150
165
  fault_source = options[:source_root] || ROOT
@@ -50,14 +50,14 @@ module HarnessExperiments
50
50
  agent: { policy: arm == 'single-policy', max_iters: options.fetch(:max_iterations, 12) } }
51
51
  File.write(File.join(root, 'provider.json'), JSON.generate(fixture))
52
52
  source = options[:source_root] || HarnessEvaluation::ROOT
53
- env = { 'HOME' => home, 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
53
+ env = { 'HOME' => home, 'PATH' => HarnessEvaluation.isolated_path, 'LANG' => 'C.UTF-8',
54
54
  'GEM_HOME' => options[:gem_home] || Gem.dir, 'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR),
55
55
  'PWN_HARNESS_FIXTURE' => File.join(root, 'provider.json'), 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
56
56
  'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json'), 'PWN_HARNESS_OBSERVATIONS' => File.join(root, 'observations.json') }
57
57
  command = [RbConfig.ruby]
58
58
  command += ['-I', File.join(source, 'lib')] unless options[:executable]
59
59
  command += ['-r', File.join(source, 'spec/support/harness/experiment_preload.rb'), options[:executable] || File.join(source, 'bin/pwn-ai'), '--ai', task.fetch('prompt')]
60
- result = HarnessEvaluation.capture(env, command, root, options.fetch(:wall_seconds, 35))
60
+ result = HarnessEvaluation.agent_capture(env, command, root, options.fetch(:wall_seconds, 35), options)
61
61
  grade = HarnessEvaluation.grade({ 'checks' => gold.fetch(task.fetch('id')), 'success_marker' => 'FIXTURE_COMPLETE' }, root, result)
62
62
  File.write(File.join(root, 'process.json'), JSON.pretty_generate(result.merge(command: command)))
63
63
  trace_path = File.join(root, 'events.jsonl')
@@ -105,7 +105,7 @@ module HarnessExperiments
105
105
 
106
106
  receipt = File.join(output, "#{task.fetch('id')}-receipt.json")
107
107
  File.write(receipt, JSON.generate(row))
108
- env = { 'HOME' => row[:home], 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
108
+ env = { 'HOME' => row[:home], 'PATH' => HarnessEvaluation.isolated_path, 'LANG' => 'C.UTF-8',
109
109
  'GEM_HOME' => options[:gem_home] || Gem.dir, 'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR) }
110
110
  recorded = HarnessEvaluation.capture(env, [RbConfig.ruby, '-I', File.join(source, 'lib'), File.join(source, 'spec/support/harness/record_training.rb'), receipt], output, 20)
111
111
  File.write(File.join(output, "#{task.fetch('id')}-learning.json"), JSON.pretty_generate(recorded))
@@ -75,6 +75,8 @@ provider.define_singleton_method(:chat_with_tools) do |opts = {}|
75
75
  { choices: [{ message: row }] }
76
76
  end
77
77
  provider.define_singleton_method(:chat) { |*_args| raise 'unexpected text-only provider path' }
78
+ ready = ENV['PWN_HARNESS_READY'].to_s
79
+ File.write(ready, 'ready') unless ready.empty?
78
80
  at_exit do
79
81
  File.write(ENV.fetch('PWN_HARNESS_FINGERPRINT'), JSON.pretty_generate(PWN::RuntimeFingerprint.report))
80
82
  end
@@ -8,14 +8,14 @@ module HarnessSessionAcceptance
8
8
  source = options[:source_root] || HarnessEvaluation::ROOT
9
9
  FileUtils.mkdir_p(File.join(root, 'home'))
10
10
  File.write(File.join(root, 'provider.json'), JSON.generate(responses: steps))
11
- env = { 'HOME' => File.join(root, 'home'), 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
11
+ env = { 'HOME' => File.join(root, 'home'), 'PATH' => HarnessEvaluation.isolated_path, 'LANG' => 'C.UTF-8',
12
12
  'GEM_HOME' => options[:gem_home] || Gem.dir, 'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR),
13
13
  'PWN_HARNESS_FIXTURE' => File.join(root, 'provider.json'), 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
14
14
  'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json') }
15
15
  command = [RbConfig.ruby]
16
16
  command += ['-I', File.join(source, 'lib')] unless options[:executable]
17
17
  command += ['-r', File.join(source, 'spec/support/harness/fake_provider.rb'), options[:executable] || File.join(source, 'bin/pwn-ai')] + args
18
- HarnessEvaluation.capture(env, command, root, 35).merge(command: command)
18
+ HarnessEvaluation.agent_capture(env, command, root, 35, options).merge(command: command)
19
19
  end
20
20
 
21
21
  def self.run(options)
@@ -29,7 +29,13 @@ module HarnessSessionAcceptance
29
29
  File.write(contract_path, JSON.generate(contract))
30
30
  initial = invoke(root, [HarnessExperiments.tool('shell', { command: "printf 'wrong\\n' > result.txt", timeout: 10 }, 'wrong')] + Array.new(8) { final },
31
31
  ['--ai', prompt, '--verification-contract', contract_path], options)
32
- old_path = Dir.glob(File.join(root, 'home/.pwn/sessions/requests/**/*.json')).fetch(0)
32
+ old_path = Dir.glob(File.join(root, 'home/.pwn/sessions/requests/**/*.json')).first
33
+ unless old_path
34
+ File.write(File.join(root, 'process.json'), JSON.pretty_generate(initial: initial))
35
+ next { id: name, outcome: 'FAIL', initial_exit_status: initial[:exit_status], exit_status: initial[:exit_status],
36
+ identity_preserved: false, artifact_root: root, task_outcome: nil, missing_checkpoint: true,
37
+ ready_observed: initial[:ready_observed], resource_cap: initial[:resource_cap] }
38
+ end
33
39
  old_bytes = File.binread(old_path)
34
40
  old = JSON.parse(old_bytes)
35
41
  sid = old.fetch('session_id')
@@ -29,14 +29,14 @@ module HarnessSwarmAcceptance
29
29
  steps += Array.new(16) { final }
30
30
  fixture = File.join(root, 'provider.json')
31
31
  File.write(fixture, JSON.generate(responses: steps, toolsets: %w[shell swarm], agent: { max_iters: 6 }))
32
- env = { 'HOME' => home, 'PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
32
+ env = { 'HOME' => home, 'PATH' => HarnessEvaluation.isolated_path, 'LANG' => 'C.UTF-8',
33
33
  'GEM_HOME' => options[:gem_home] || Gem.dir, 'GEM_PATH' => options[:gem_path] || Gem.path.join(File::PATH_SEPARATOR),
34
34
  'PWN_HARNESS_FIXTURE' => fixture, 'PWN_HARNESS_EVENTS' => File.join(root, 'events.jsonl'),
35
35
  'PWN_HARNESS_FINGERPRINT' => File.join(root, 'fingerprint.json') }
36
36
  command = [RbConfig.ruby]
37
37
  command += ['-I', File.join(source, 'lib')] unless options[:executable]
38
38
  command += ['-r', File.join(source, 'spec/support/harness/fake_provider.rb'), options[:executable] || File.join(source, 'bin/pwn-ai'), '--ai', prompt]
39
- result = HarnessEvaluation.capture(env, command, root, 35)
39
+ result = HarnessEvaluation.agent_capture(env, command, root, 35, options)
40
40
  File.write(File.join(root, 'process.json'), JSON.pretty_generate(result.merge(command: command)))
41
41
  checkpoints = Dir.glob(File.join(home, '.pwn/sessions/requests/**/*.json')).map { |path| JSON.parse(File.read(path)) }
42
42
  parent = checkpoints.find { |row| Array(row['enabled_toolsets']).include?('swarm') }
@@ -1158,7 +1158,7 @@
1158
1158
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.anthropic_schema_notes Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.anthropic_schema_notes`: "}]}
1159
1159
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.authors Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1160
1160
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.chat Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.chat`: Supported Method Parameters\n\nresponse = PWN::AI::Anthropic.chat(\n\nrequest: 'required - message to Anthropic',\nmodel: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:anthropic][:model])',\ntemp: 'optional - creative response float (defaults to PWN::Env[:ai][:anthropic][:temp])',\nsystem_role_content: 'optional - context to set up the model behavior for conversation (Default: PWN::Env[:ai][:anthropic][:system_role_content])',\nresponse_history: 'optional - pass response back in to have a conversation',\nspeak_answer: 'optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
1161
- {"messages":[{"role":"user","content":"PWN::AI::Anthropic.chat_with_tools Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.chat_with_tools`: Supported Method Parameters\n\nresponse = PWN::AI::Anthropic.chat_with_tools(\n\nmessages: 'required - OpenAI-format messages array (system/user/assistant/tool)',\ntools: 'optional - OpenAI tools array [{type:\"function\", function:{...}}]',\ntool_choice: 'optional - \"auto\" | \"none\" | \"required\" | {type:\"function\", function:{name:..}}',\nmodel: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',\ntemp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',\nmax_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',\ntimeout: 'optional - seconds (default 900)',\nspinner: 'optional - display spinner (default false)'\n\n)\n"}]}
1161
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.chat_with_tools Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.chat_with_tools`: Supported Method Parameters\n\nresponse = PWN::AI::Anthropic.chat_with_tools(\n\nmessages: 'required - OpenAI-format messages array (system/user/assistant/tool)',\ntools: 'optional - OpenAI tools array [{type:\"function\", function:{...}}]',\ntool_choice: 'optional - \"auto\" | \"none\" | \"required\" | {type:\"function\", function:{name:..}}',\nmodel: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',\ntemp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',\nmax_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',\nreasoning_effort: 'optional - output_config.effort when the model supports it',\ntimeout: 'optional - seconds (default 900)',\nspinner: 'optional - display spinner (default false)'\n\n)\n"}]}
1162
1162
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.forced_tool_choice? Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.forced_tool_choice?`: "}]}
1163
1163
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.get_model Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.get_model`: Supported Method Parameters\n\nmodel = PWN::AI::Anthropic.get_model(name: ‘required - model id’)\n\nThe Models API lists id and capabilities. Pricing, when Anthropic includes it on that record, is passed through unchanged. A missing price field is not filled from a public price table.\n"}]}
1164
1164
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.get_models Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.get_models`: Supported Method Parameters\n\nmodels = PWN::AI::Anthropic.get_models\n"}]}
@@ -1171,6 +1171,7 @@
1171
1171
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.persist_oauth_to_vault Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.persist_oauth_to_vault`: "}]}
1172
1172
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.pkce_pair Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.pkce_pair`: "}]}
1173
1173
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.real_config_value?`: "}]}
1174
+ {"messages":[{"role":"user","content":"PWN::AI::Anthropic.reasoning_efforts Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.reasoning_efforts`: "}]}
1174
1175
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.refresh_oauth_bearer_token Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.refresh_oauth_bearer_token`: Supported Method Parameters\n\naccess_token = PWN::AI::Anthropic.refresh_oauth_bearer_token(\n\nrefresh_token: 'required - Anthropic OAuth refresh_token',\nclient_id: 'optional - defaults to Claude Code public client',\ntoken_uri: 'optional - defaults to platform.claude.com token endpoint'\n\n) On success, writes :bearer_token (and a rotated :refresh_token if returned) back into the passed opts/oauth Hash so the live PWN::Env stays warm for the rest of the process. Also mirrors those values onto PWN::Env[:anthropic] when that Hash is available, then attempts to re-encrypt the updated tokens into ~/.pwn/pwn.yaml when the matching decryptor (key + iv) is present.\n"}]}
1175
1176
  {"messages":[{"role":"user","content":"PWN::AI::Anthropic.sync_oauth_into_env Usage"},{"role":"assistant","content":"`PWN::AI::Anthropic.sync_oauth_into_env`: "}]}
1176
1177
  {"messages":[{"role":"user","content":"PWN::AI::CLI.authors Usage"},{"role":"assistant","content":"`PWN::AI::CLI.authors`: "}]}
@@ -1205,11 +1206,13 @@
1205
1206
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.chat_with_tools Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.chat_with_tools`: Supported Method Parameters\n\nresponse = PWN::AI::Gemini.chat_with_tools(\n\nmessages: 'required - OpenAI-format messages array (system/user/assistant/tool)',\ntools: 'optional - OpenAI tools array [{type:\"function\", function:{...}}]',\ntool_choice: 'optional - \"auto\" | \"none\" | \"required\" | {type:\"function\", function:{name:..}}',\nmodel: 'optional - overrides PWN::Env[:ai][:gemini][:model]',\ntemp: 'optional - temperature (defaults to PWN::Env[:ai][:gemini][:temp] || 1)',\nmax_tokens: 'optional - maxOutputTokens (defaults to 8192)',\ntimeout: 'optional - seconds (default 900)',\nspinner: 'optional - display spinner (default false)'\n\n)\n"}]}
1206
1207
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.gemini_resp_to_oa Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.gemini_resp_to_oa`: "}]}
1207
1208
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.gemini_rest_call Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.gemini_rest_call`: Supported Method Parameters\n\ngemini_rest_call(\n\nhttp_method: 'optional HTTP method (defaults to GET)',\nrest_call: 'required rest call to make per the schema',\nparams: 'optional params passed in the URI or HTTP Headers',\nhttp_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
1209
+ {"messages":[{"role":"user","content":"PWN::AI::Gemini.gemini_thinking_config Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.gemini_thinking_config`: "}]}
1208
1210
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.gemini_tool_config Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.gemini_tool_config`: "}]}
1209
1211
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.get_model Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.get_model`: Supported Method Parameters\n\nmodel = PWN::AI::Gemini.get_model(name: ‘required - model id or models/<id>’)\n\nGemini names models as models/<id> and usually publishes token limits, not USD rates. A pricing object on the record is kept; none is invented.\n"}]}
1210
1212
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.get_models Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.get_models`: Supported Method Parameters\n\nmodels = PWN::AI::Gemini.get_models\n"}]}
1211
1213
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.help Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.help`: "}]}
1212
1214
  {"messages":[{"role":"user","content":"PWN::AI::Gemini.oa_messages_to_gemini Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.oa_messages_to_gemini`: "}]}
1215
+ {"messages":[{"role":"user","content":"PWN::AI::Gemini.reasoning_efforts Usage"},{"role":"assistant","content":"`PWN::AI::Gemini.reasoning_efforts`: "}]}
1213
1216
  {"messages":[{"role":"user","content":"PWN::AI::Grok.authors Usage"},{"role":"assistant","content":"`PWN::AI::Grok.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1214
1217
  {"messages":[{"role":"user","content":"PWN::AI::Grok.chat Usage"},{"role":"assistant","content":"`PWN::AI::Grok.chat`: Supported Method Parameters\n\nresponse = PWN::AI::Grok.chat(\n\nrequest: 'required - message to Grok'\nmodel: 'optional - model to use for text generation (defaults to PWN::Env[:ai][:grok][:model])',\ntemp: 'optional - creative response float (deafults to PWN::Env[:ai][:grok][:temp])',\nsystem_role_content: 'optional - context to set up the model behavior for conversation (Default: PWN::Env[:ai][:grok][:system_role_content])',\nresponse_history: 'optional - pass response back in to have a conversation',\nspeak_answer: 'optional speak answer using PWN::Plugins::Voice.text_to_speech (Default: nil)',\ntimeout: 'optional timeout in seconds (defaults to 900)',\nspinner: 'optional - display spinner (defaults to false)'\n\n)\n"}]}
1215
1218
  {"messages":[{"role":"user","content":"PWN::AI::Grok.chat_with_tools Usage"},{"role":"assistant","content":"`PWN::AI::Grok.chat_with_tools`: Supported Method Parameters\n\nresponse = PWN::AI::Grok.chat_with_tools(\n\nmessages: 'required - full OpenAI-format messages array (system/user/assistant/tool)',\ntools: 'optional - OpenAI tools array [{type:\"function\", function:{...}}]',\ntool_choice: 'optional - \"auto\" | \"none\" | \"required\" | {type:\"function\", function:{name:..}}',\nmodel: 'optional - overrides PWN::Env[:ai][:grok][:model]',\ntemp: 'optional - temperature (defaults to PWN::Env[:ai][:grok][:temp] || 1)',\ntimeout: 'optional - seconds (default 180)',\nspinner: 'optional - display spinner (default false)'\n\n)\n\nReturns the raw chat/completions response Hash with :choices intact (including :message) — used by PWN::AI::Agent::Loop. xAI’s API is OpenAI-compatible for tool calling, so the request and response shapes are identical to PWN::AI::OpenAI.chat_with_tools.\n"}]}
@@ -1219,7 +1222,7 @@
1219
1222
  {"messages":[{"role":"user","content":"PWN::AI::Grok.help Usage"},{"role":"assistant","content":"`PWN::AI::Grok.help`: "}]}
1220
1223
  {"messages":[{"role":"user","content":"PWN::AI::Grok.jwt_exp Usage"},{"role":"assistant","content":"`PWN::AI::Grok.jwt_exp`: "}]}
1221
1224
  {"messages":[{"role":"user","content":"PWN::AI::Grok.oauth_token_expiring? Usage"},{"role":"assistant","content":"`PWN::AI::Grok.oauth_token_expiring?`: "}]}
1222
- {"messages":[{"role":"user","content":"PWN::AI::Grok.obtain_oauth_bearer_token Usage"},{"role":"assistant","content":"`PWN::AI::Grok.obtain_oauth_bearer_token`: Supported Method Parameters\n\nbearer = PWN::AI::Grok.obtain_oauth_bearer_token(\n\nclient_id: 'optional - xAI OAuth Client ID (defaults to public Grok-CLI client)',\nscope: 'optional - space-delimited scopes (defaults to XAI_OAUTH_SCOPE)',\ntimeout: 'optional - seconds to wait for user consent (default 300)'\n\n)\n\nRuns the RFC 8628 OAuth 2.0 Device Authorization Grant against auth.x.ai using xAI’s public Grok-CLI client (no client_secret). This is the same identity path ‘hermes auth add xai-oauth` uses – a SuperGrok / X Premium+ subscription on the account is what grants api:access at consent time.\n\n1. POST /oauth2/device/code -> device_code, user_code, verification_uri\n2. User opens verification_uri_complete in a browser and approves.\n3. Poll POST /oauth2/token (grant_type=device_code) until access_token.\n\nOn success the access_token + refresh_token are written back into the passed opts/oauth Hash (so PWN::Env[:grok] is live-cached) and the operator is told exactly what to persist via pwn-vault.\n"}]}
1225
+ {"messages":[{"role":"user","content":"PWN::AI::Grok.obtain_oauth_bearer_token Usage"},{"role":"assistant","content":"`PWN::AI::Grok.obtain_oauth_bearer_token`: Supported Method Parameters\n\nbearer = PWN::AI::Grok.obtain_oauth_bearer_token(\n\nclient_id: 'optional - xAI OAuth Client ID (defaults to public Grok-CLI client)',\nscope: 'optional - space-delimited scopes (defaults to XAI_OAUTH_SCOPE)',\ntimeout: 'optional - seconds to wait for user consent (default 300)'\n\n)\n\nRuns the RFC 8628 OAuth 2.0 Device Authorization Grant against auth.x.ai using xAI’s public Grok-CLI client (no client_secret). This is the same identity path ‘hermes auth add xai-oauth` uses – a SuperGrok / X Premium+ subscription on the account is what grants api:access at consent time.\n\n1. POST /oauth2/device/code -> device_code, user_code, verification_uri\n2. User opens verification_uri_complete in a browser and approves.\n3. Poll POST /oauth2/token (grant_type=device_code) until access_token.\n\nOn success the access_token + refresh_token are written into the live request runtime (provider_config is a copy) and into the encrypted vault when the decryptor is present. Tokens are not printed.\n"}]}
1223
1226
  {"messages":[{"role":"user","content":"PWN::AI::Grok.persist_oauth_to_vault Usage"},{"role":"assistant","content":"`PWN::AI::Grok.persist_oauth_to_vault`: "}]}
1224
1227
  {"messages":[{"role":"user","content":"PWN::AI::Grok.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::Grok.real_config_value?`: "}]}
1225
1228
  {"messages":[{"role":"user","content":"PWN::AI::Grok.refresh_oauth_bearer_token Usage"},{"role":"assistant","content":"`PWN::AI::Grok.refresh_oauth_bearer_token`: Supported Method Parameters\n\naccess_token = PWN::AI::Grok.refresh_oauth_bearer_token(\n\nrefresh_token: 'required - xAI OAuth refresh_token',\nclient_id: 'optional - defaults to public Grok-CLI client',\ntoken_uri: 'optional - defaults to https://auth.x.ai/oauth2/token'\n\n)\n\nExchanges a refresh_token for a fresh access_token at auth.x.ai. On success, writes :bearer_token (and a rotated :refresh_token if returned) back into the passed opts/oauth Hash so the live PWN::Env stays warm for the rest of the process. Also mirrors those values onto PWN::Env[:grok] when that Hash is available, then attempts to re-encrypt the updated tokens into ~/.pwn/pwn.yaml when the matching decryptor (key + iv) is present.\n"}]}
@@ -1305,6 +1308,8 @@
1305
1308
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.real_config_value?`: "}]}
1306
1309
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.strict_system_template?`: "}]}
1307
1310
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.template_rejects_later_system?`: "}]}
1311
+ {"messages":[{"role":"user","content":"PWN::AI::Ollama.think_wire Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.think_wire`: "}]}
1312
+ {"messages":[{"role":"user","content":"PWN::AI::Ollama.thinking_controls Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.thinking_controls`: "}]}
1308
1313
  {"messages":[{"role":"user","content":"PWN::AI::Ollama.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.visible_from_thinking`: "}]}
1309
1314
  {"messages":[{"role":"user","content":"PWN::AI::OpenAI.api_endpoint Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.api_endpoint`: "}]}
1310
1315
  {"messages":[{"role":"user","content":"PWN::AI::OpenAI.authors Usage"},{"role":"assistant","content":"`PWN::AI::OpenAI.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
@@ -1359,6 +1364,8 @@
1359
1364
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.real_config_value? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.real_config_value?`: "}]}
1360
1365
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.strict_system_template? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.strict_system_template?`: "}]}
1361
1366
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.template_rejects_later_system? Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.template_rejects_later_system?`: "}]}
1367
+ {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.think_wire Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.think_wire`: "}]}
1368
+ {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.thinking_controls Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.thinking_controls`: "}]}
1362
1369
  {"messages":[{"role":"user","content":"PWN::AI::OpenWebUI.visible_from_thinking Usage"},{"role":"assistant","content":"`PWN::AI::OpenWebUI.visible_from_thinking`: "}]}
1363
1370
  {"messages":[{"role":"user","content":"PWN::AI::RedTeam.authors Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1364
1371
  {"messages":[{"role":"user","content":"PWN::AI::RedTeam.help Usage"},{"role":"assistant","content":"`PWN::AI::RedTeam.help`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.771
4
+ version: 0.5.772
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.