pwn 0.5.741 → 0.5.742

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: 3eb74de135994d78ce8af6b31729ef0622495f0cbe6ff72ddb5ecce5662a5aa7
4
- data.tar.gz: 3f2d855056ef2cce6424ac399393e38492970850baa36fde37419d5e798ca2f8
3
+ metadata.gz: aae26246e4b23381c5f727d27100f6a10d37d955f2c9a875ad63274d3397219b
4
+ data.tar.gz: 1a0abc7df30321bd4618a8db3d356fa72e9b6cc13f6055b5f71bf4fb1b4ec90a
5
5
  SHA512:
6
- metadata.gz: 995de4dfb6c55750ed90c2f8c282c4d6ca14e876f41ec207b71aecdc1d162ab69359a20d3d479115c4aea9b9eee7d04d9c3bd1678ff73d1e1c06dbefcbef7c46
7
- data.tar.gz: 30c2d6602caa5d1fef740508751d4457d69c7f51d861a6260e57f65251c5050d2425c21dad3e946181aa1776345f413e2a035ec3bcf411ae4eedce21441d61d4
6
+ metadata.gz: 5b680d2d2d3eb756642d68a6309a36a5d551100131b58323954bd402c73f554515304ecfb005a42e96ab15a385f16d717495cf527c2bf17c87434567401d0dd0
7
+ data.tar.gz: 7b51570ad19e664e45ca69925619fdbdda6272f985d8627f4c247815b469de19cb11bdfa942433480bbc38792127a8b39e45e56c08c8c73ffe517318ad004396
@@ -144,12 +144,31 @@ module PWN
144
144
 
145
145
  pool = apply_preference({ items: pool }.merge(pref_fwd))
146
146
  pool.map do |entry|
147
- schema = entry.schema
147
+ schema = entry.schema.merge(parameters: explicit_root_types(schema: entry.schema[:parameters]))
148
148
  schema = schema.merge(description: "#{schema[:description]} #{mcp[:description]}") if entry.name == 'mcp'
149
149
  { type: 'function', function: schema }
150
150
  end
151
151
  end
152
152
 
153
+ # Providers inspect union branches independently of their object parent.
154
+ # Inherit only along same-instance combinators, never into properties or
155
+ # array items (which may legitimately be scalar/object unions).
156
+ private_class_method def self.explicit_root_types(opts = {})
157
+ schema = opts[:schema]
158
+ return schema unless schema.is_a?(Hash)
159
+
160
+ result = schema.dup
161
+ type = schema[:type] || schema['type'] || opts[:inherited_type]
162
+ result[:type] = type if type && !schema.key?(:type) && !schema.key?('type')
163
+ %i[anyOf oneOf allOf].each do |combiner|
164
+ key = schema.key?(combiner) ? combiner : combiner.to_s
165
+ next unless schema[key].is_a?(Array)
166
+
167
+ result[key] = schema[key].map { |branch| explicit_root_types(schema: branch, inherited_type: type) }
168
+ end
169
+ result
170
+ end
171
+
153
172
  # Supported Method Parameters::
154
173
  # order = PWN::AI::Agent::Registry.preference_order(
155
174
  # order: 'optional - Array of tool names; explicit nil/[] disables',
data/lib/pwn/ai/grok.rb CHANGED
@@ -476,7 +476,7 @@ module PWN
476
476
  extra: '429 retries exhausted', error: e
477
477
  )
478
478
  end
479
- return "#{e.message}: #{e.response}"
479
+ raise
480
480
  end
481
481
  sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
482
482
  retry
@@ -497,8 +497,10 @@ module PWN
497
497
  nil
498
498
  end
499
499
  rescue RestClient::ExceptionWithResponse => e
500
- puts "ERROR: #{e.message}: #{e.response}" unless opts[:quiet]
501
- "#{e.message}: #{e.response}" if opts[:quiet]
500
+ # Keep the HTTP type/response for callers, and surface provider diagnostics
501
+ # instead of turning failed requests into nil or unparseable JSON text.
502
+ e.message = "Grok HTTP #{e.http_code} (#{http_method.to_s.upcase} #{rest_call}): #{e.response&.body}"
503
+ raise
502
504
  rescue StandardError => e
503
505
  case e.message
504
506
  when '400 Bad Request', '404 Resource Not Found'
data/lib/pwn/ai/ollama.rb CHANGED
@@ -182,7 +182,7 @@ module PWN
182
182
  extra: '429 retries exhausted', error: e
183
183
  )
184
184
  end
185
- return "#{e.message}: #{e.response}"
185
+ raise
186
186
  end
187
187
  sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
188
188
  retry
@@ -207,7 +207,7 @@ module PWN
207
207
  extra: '429 retries exhausted', error: e
208
208
  )
209
209
  end
210
- return "#{e.message}: #{e.response}"
210
+ raise
211
211
  end
212
212
  sleep(PWN::AI::HttpRetry.retry_after_s(response: e.response, retry_count: retry_count) + rand(0.3..5.0))
213
213
  retry
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.741'
4
+ VERSION = '0.5.742'
5
5
  end
@@ -47,6 +47,33 @@ describe PWN::AI::Agent::Registry do
47
47
  expect(names.length).to eq(described_class::CORE_TOOLS.length)
48
48
  end
49
49
 
50
+ it 'exports explicitly object-typed root alternatives without changing registered schemas' do
51
+ described_class.discover
52
+ original = Marshal.dump(described_class.lookup(name: 'artifact_read').schema)
53
+ definitions = described_class.definitions(core_only: false, top_k: 0)
54
+ check = lambda do |schema|
55
+ %i[anyOf oneOf allOf].each do |key|
56
+ Array(schema[key]).each do |branch|
57
+ expect(branch[:type]).to eq('object')
58
+ check.call(branch)
59
+ end
60
+ end
61
+ end
62
+ definitions.each { |tool| check.call(tool[:function][:parameters]) }
63
+ tool = definitions.find { |entry| entry[:function][:name] == 'artifact_read' }
64
+ expect(tool[:function][:parameters][:anyOf].map { |branch| branch[:required] }).to eq([%w[handle], %w[path], %w[ref]])
65
+ expect(Marshal.dump(described_class.lookup(name: 'artifact_read').schema)).to eq(original)
66
+ end
67
+
68
+ it 'preserves property-level scalar unions and explicit branch types' do
69
+ schema = { 'type' => 'object', 'oneOf' => [{ 'required' => ['value'] }],
70
+ 'properties' => { 'value' => { 'anyOf' => [{ 'type' => 'string' }, { 'type' => 'object' }] } } }
71
+ normalized = described_class.send(:explicit_root_types, schema: schema)
72
+ expect(normalized['oneOf'].first[:type]).to eq('object')
73
+ expect(normalized['properties']).to eq(schema['properties'])
74
+ expect(schema['oneOf'].first).to eq('required' => ['value'])
75
+ end
76
+
50
77
  it 'advertises the MCP broker for a named backend in the normal core-only prompt path' do
51
78
  described_class.discover
52
79
  request = 'Use combo.nation to inspect the menu catalog'
@@ -18,6 +18,111 @@ describe PWN::AI::Grok do
18
18
  expect(src).to match(/openai_wire_messages/)
19
19
  end
20
20
 
21
+ describe '.chat_with_tools HTTP failures' do
22
+ let(:messages) { [{ role: 'user', content: 'hello' }] }
23
+ let(:body) { '{"error":"Invalid schema: tools[0].function.parameters requires properties"}' }
24
+ let(:response) { instance_double(RestClient::Response, code: 400, body: body, to_s: body) }
25
+ let(:error) { RestClient::BadRequest.new(response) }
26
+
27
+ before do
28
+ stub_const('PWN::Env', { ai: { grok: { key: 'test-key', model: 'test-model' } } })
29
+ allow(PWN::Plugins::TransparentBrowser).to receive(:open).with(browser_type: :rest).and_return(browser: RestClient)
30
+ allow(PWN::AI::Agent::ToolGuard).to receive(:protect_http!)
31
+ allow(PWN::AI::Agent::PromptCache).to receive(:enabled?).and_return(false)
32
+ allow(PWN::Plugins::TTYSpinner).to receive(:stop)
33
+ allow(RestClient::Request).to receive(:execute).and_raise(error)
34
+ end
35
+
36
+ it 'serializes explicit artifact_read branch types from the real registry' do
37
+ tools = PWN::AI::Agent::Registry.definitions(core_only: true)
38
+ allow(RestClient::Request).to receive(:execute).and_return(
39
+ '{"choices":[{"message":{"role":"assistant","content":"hello"}}]}'
40
+ )
41
+
42
+ described_class.chat_with_tools(messages: messages, tools: tools)
43
+
44
+ expect(RestClient::Request).to have_received(:execute) do |request|
45
+ payload = JSON.parse(request[:payload])
46
+ artifact_read = payload.fetch('tools').find { |tool| tool.dig('function', 'name') == 'artifact_read' }
47
+ expect(artifact_read).not_to be_nil
48
+ branches = artifact_read.fetch('function').fetch('parameters').fetch('anyOf')
49
+ expect(branches).not_to be_empty
50
+ expect(branches).to all(include('type' => 'object'))
51
+ end
52
+ end
53
+
54
+ context 'when rate limits exhaust the retry budget' do
55
+ let(:body) { '{"error":"rate limit exceeded"}' }
56
+ let(:response) { instance_double(RestClient::Response, code: 429, body: body, to_s: body, headers: { retry_after: '2' }) }
57
+ let(:error) { RestClient::TooManyRequests.new(response) }
58
+
59
+ it 'raises the HTTP error after five attempts and four Retry-After waits' do
60
+ allow(described_class).to receive(:rand).with(0.3..5.0).and_return(0.5)
61
+ allow(described_class).to receive(:sleep)
62
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
63
+
64
+ expect do
65
+ described_class.chat_with_tools(messages: messages)
66
+ end.to raise_error(RestClient::TooManyRequests) { |raised|
67
+ expect(raised.message).to include('HTTP 429', body)
68
+ }
69
+ expect(RestClient::Request).to have_received(:execute).exactly(5).times
70
+ expect(described_class).to have_received(:sleep).with(2.5).exactly(4).times
71
+ end
72
+ end
73
+
74
+ context 'when the request times out' do
75
+ let(:error) { RestClient::Exceptions::ReadTimeout.new }
76
+
77
+ it 'preserves the default five attempts and nil on exhaustion' do
78
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
79
+ expect(described_class.chat_with_tools(messages: messages)).to be_nil
80
+ expect(RestClient::Request).to have_received(:execute).with(hash_including(timeout: 180)).exactly(5).times
81
+ end
82
+
83
+ it 'keeps quiet sidecars single-shot and silent' do
84
+ expect(PWN::AI::HttpRetry).not_to receive(:report_event)
85
+ expect(described_class.chat_with_tools(messages: messages, quiet: true)).to be_nil
86
+ expect(RestClient::Request).to have_received(:execute).once
87
+ end
88
+
89
+ it 'keeps short timeouts single-shot' do
90
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
91
+ expect(described_class.chat_with_tools(messages: messages, timeout: 10)).to be_nil
92
+ expect(RestClient::Request).to have_received(:execute).with(hash_including(timeout: 10)).once
93
+ end
94
+
95
+ it 'returns the successful response after a transient timeout' do
96
+ calls = 0
97
+ allow(RestClient::Request).to receive(:execute) do
98
+ calls += 1
99
+ raise error if calls == 1
100
+
101
+ '{"choices":[{"message":{"role":"assistant","content":"hello"}}]}'
102
+ end
103
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
104
+ result = described_class.chat_with_tools(messages: messages)
105
+ expect(result.dig(:assistant_message, :content)).to eq('hello')
106
+ expect(RestClient::Request).to have_received(:execute).twice
107
+ end
108
+ end
109
+
110
+ [false, true].each do |quiet|
111
+ it "propagates actionable schema errors without printing (quiet=#{quiet})" do
112
+ expect do
113
+ expect do
114
+ described_class.chat_with_tools(messages: messages, quiet: quiet)
115
+ end.to raise_error(RestClient::BadRequest) { |raised|
116
+ expect(raised.response).to equal(response)
117
+ expect(raised.message).to include('Grok', 'HTTP 400', 'chat/completions', body)
118
+ }
119
+ end.not_to output.to_stdout
120
+ expect(RestClient::Request).to have_received(:execute).once
121
+ expect(PWN::Plugins::TTYSpinner).to have_received(:stop)
122
+ end
123
+ end
124
+ end
125
+
21
126
  it 'does not expose get_plan_usage' do
22
127
  expect(described_class).not_to respond_to(:get_plan_usage)
23
128
  end
@@ -3,6 +3,23 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::AI::Ollama do
6
+ [false, true].each do |quiet|
7
+ it "raises an actionable exhausted 429 instead of parsing error text (quiet=#{quiet})" do
8
+ stub_const('PWN::Env', { ai: { ollama: { model: 'fixture', base_uri: 'http://example.test' } } })
9
+ allow(PWN::Plugins::TransparentBrowser).to receive(:open).and_return(browser: RestClient)
10
+ response = instance_double(RestClient::Response, code: 429, body: 'rate limit exceeded', to_s: 'rate limit exceeded', headers: { retry_after: '2' })
11
+ allow(RestClient::Request).to receive(:execute).and_raise(RestClient::TooManyRequests.new(response))
12
+ allow(described_class).to receive(:sleep)
13
+ allow(described_class).to receive(:rand).and_return(0.5)
14
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
15
+ expect do
16
+ described_class.chat_with_tools(messages: [{ role: 'user', content: 'hello' }], quiet: quiet)
17
+ end.to raise_error(RuntimeError, /Ollama.*429.*rate limit exceeded/)
18
+ expect(RestClient::Request).to have_received(:execute).exactly(5).times
19
+ expect(described_class).to have_received(:sleep).with(2.5).exactly(4).times
20
+ end
21
+ end
22
+
6
23
  it 'should display information for authors' do
7
24
  authors_response = PWN::AI::Ollama
8
25
  expect(authors_response).to respond_to :authors
@@ -3,6 +3,23 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::AI::OpenWebUI do
6
+ [false, true].each do |quiet|
7
+ it "raises an actionable exhausted 429 instead of parsing error text (quiet=#{quiet})" do
8
+ stub_const('PWN::Env', { ai: { openwebui: { model: 'fixture', base_uri: 'http://example.test', key: 'fixture-key' } } })
9
+ allow(PWN::Plugins::TransparentBrowser).to receive(:open).and_return(browser: RestClient)
10
+ response = instance_double(RestClient::Response, code: 429, body: 'rate limit exceeded', to_s: 'rate limit exceeded', headers: { retry_after: '2' })
11
+ allow(RestClient::Request).to receive(:execute).and_raise(RestClient::TooManyRequests.new(response))
12
+ allow(described_class).to receive(:sleep)
13
+ allow(described_class).to receive(:rand).and_return(0.5)
14
+ allow(PWN::AI::HttpRetry).to receive(:report_event)
15
+ expect do
16
+ described_class.chat_with_tools(messages: [{ role: 'user', content: 'hello' }], quiet: quiet)
17
+ end.to raise_error(RuntimeError, /Open WebUI.*429.*rate limit exceeded/)
18
+ expect(RestClient::Request).to have_received(:execute).exactly(5).times
19
+ expect(described_class).to have_received(:sleep).with(2.5).exactly(4).times
20
+ end
21
+ end
22
+
6
23
  it 'should display information for authors' do
7
24
  authors_response = PWN::AI::OpenWebUI
8
25
  expect(authors_response).to respond_to :authors
@@ -685,6 +685,7 @@
685
685
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.discover Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.discover`: Supported Method Parameters\n\nnames = PWN::AI::Agent::Registry.discover(\n\nforce: 'optional - re-require tool files even if already discovered (default false)'\n\n)\n"}]}
686
686
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.domain_pin_names Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.domain_pin_names`: "}]}
687
687
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.eager_load! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.eager_load!`: "}]}
688
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.explicit_root_types Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.explicit_root_types`: "}]}
688
689
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.help`: "}]}
689
690
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.lookup Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.lookup`: Supported Method Parameters\n\nentry = PWN::AI::Agent::Registry.lookup(\n\nname: 'required - registered tool name'\n\n)\n"}]}
690
691
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.mcp_routing Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.mcp_routing`: "}]}
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.741
4
+ version: 0.5.742
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.