pwn 0.5.738 → 0.5.739

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: 1ca56931addf19f59b18ee2776ee84b0af0a75496bcf9bd9d8453b96b9b9eef2
4
- data.tar.gz: b9310696c7ae7098d37dc3c8e46b1cd2214ab652a49ef75df281b34c472a738c
3
+ metadata.gz: 5a7b0a41aca20dc31343279c213c042d7362ff5c0ac59a89150a427111d5b11d
4
+ data.tar.gz: b4335ec85624abed2dbb25d8941ab1db79d73b07fb8b4eca64b90f56a96d15f7
5
5
  SHA512:
6
- metadata.gz: d6ec57ded9a7213562cdaf4b4486d680387f8dea1ec1fbf6bfc47a2b4ed40e7c7782c40e3ab80a5164c308c5dae220442956b83cf2c0ccbddbbc2e398b1e6acf
7
- data.tar.gz: 7c447c3b9e42779e4ca86038836719dad5b051a872f4bbad2bc704b54596a69644c56a92a6d877681045f3c7000cddb0a51aaa49e79be9ab1afc5de40b7fa8a2
6
+ metadata.gz: 226c64efa942d21212d5766604c299908118f1be000093204ff1e70b31359d2931e009df8cdc0894696832240d597b687397975fad7437eea288a0254236b6d6
7
+ data.tar.gz: e2ed95fb25177d41d49cfe3efaef6cb162066eba28a7470d3256c88b74e6e8779b4daf2204836051305a8f7ec20d478fdd82526df046a13bd88eae4bbd68765c
data/Gemfile CHANGED
@@ -20,7 +20,7 @@ gem 'base32', '0.3.4'
20
20
  gem 'bitcoin-ruby', '0.0.20'
21
21
  gem 'brakeman', '8.0.6'
22
22
  gem 'bson', '5.2.0'
23
- gem 'bundler', '>=4.0.20'
23
+ gem 'bundler', '>=4.0.21'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
25
  gem 'bunny', '3.3.0'
26
26
  gem 'colorize', '1.1.0'
@@ -50,7 +50,7 @@ gem 'libusb', '0.8.0'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.1'
52
52
  gem 'mcp', '1.5.1'
53
- gem 'meshtastic', '0.0.180'
53
+ gem 'meshtastic', '0.0.183'
54
54
  gem 'metasm', '1.0.6'
55
55
  gem 'mongo', '2.26.0'
56
56
  gem 'msfrpc-client', '1.1.2'
@@ -50,6 +50,20 @@ protected operator preferences, open mistakes, or gold outcomes.
50
50
 
51
51
  ## Dynamic tool-set slimming (`ai.agent.tool_router`)
52
52
 
53
+ The normal `Loop.run` path uses `core_only: true`. The central registry also
54
+ includes `mcp` when the original request names MCP, a discovered backend, or
55
+ one of its declared tools. Backend names and tool names come from
56
+ `PWN::AI::MCP.backends`, not a ComboNation-specific router. Thus `combo.nation`,
57
+ `combo_nation`, and `PWN::AI::MCP::ComboNation` expose the same broker, even in
58
+ core-only mode or with a zero top-K budget. Unrelated core-only prompts remain
59
+ unchanged, and explicit toolset exclusions and availability checks still apply.
60
+
61
+ The advertised MCP schema includes the local backend catalog. Use `list_tools`
62
+ for the selected backend's live argument schemas, then `call_tool` with a JSON
63
+ `arguments` object. Sessions persist across calls and menu IDs such as `5.10`
64
+ must stay strings. Catalog discovery reads local metadata only: it does not
65
+ launch a server or enable hardware. Hardware still requires explicit opt-in.
66
+
53
67
  Shipping every schema on every turn overwhelms a small local model - the
54
68
  choice space is huge and it mis-routes (for example, picks an RF tool for a git
55
69
  question). When `ai.agent.tool_router: true` **and** `Loop.run` passes the
@@ -2965,8 +2965,9 @@ module PWN
2965
2965
  # TaskSummarizer.emit_plan! we re-rank using English tangible tasks
2966
2966
  # so generated tasks — not the bare request — drive which tools
2967
2967
  # the model may call.
2968
- # CORE_TOOLS is the default action space. Extra schemas are
2969
- # opt-in via enabled_toolsets + core_only: false.
2968
+ # CORE_TOOLS is the default action space, plus the MCP broker when
2969
+ # local client metadata matches the request. Other extra schemas
2970
+ # are opt-in via enabled_toolsets + core_only: false.
2970
2971
  core_only = opts.fetch(:core_only, true)
2971
2972
  if nested && needs_host_work?(request: request) &&
2972
2973
  !(opts.key?(:enabled_toolsets) && Array(opts[:enabled_toolsets]).empty?)
@@ -3441,7 +3442,7 @@ module PWN
3441
3442
  force_tools: 'optional - force tools value consumed by #run',
3442
3443
  nested: 'optional - true for Swarm/child Loop.run (skip RN footer)',
3443
3444
  engine: 'optional - provider name; defaults to PWN::Env ai.active',
3444
- core_only: 'optional - restrict to CORE_TOOLS when true',
3445
+ core_only: 'optional - restrict to CORE_TOOLS plus relevant MCP discovery when true',
3445
3446
  trace: 'optional - enable TracePoint debug for this run',
3446
3447
  debug_tee: 'optional - IO to tee debug logs'
3447
3448
  )
@@ -122,6 +122,8 @@ module PWN
122
122
  enabled = enabled.map(&:to_s) if enabled
123
123
  pool = @entries.values.select { |e| (enabled.nil? || enabled.include?(e.toolset)) && safe_check(entry: e) }
124
124
  pool = pool.select { |e| available?(entry: e, trusted_context: opts[:trusted_context]) }
125
+ mcp = pool.any? { |entry| entry.name == 'mcp' } ? mcp_routing(query: opts[:relevance]) : {}
126
+ relevant = mcp[:relevant] ? ['mcp'] : []
125
127
 
126
128
  pref_fwd = {}
127
129
  pref_fwd[:order] = opts[:order] if opts.key?(:order)
@@ -131,16 +133,20 @@ module PWN
131
133
  pref_fwd[:trusted_context] = opts[:trusted_context] if opts.key?(:trusted_context)
132
134
 
133
135
  if opts[:core_only]
134
- pool = pool.select { |e| CORE_TOOLS.include?(e.name) }
136
+ pool = pool.select { |e| (CORE_TOOLS + relevant).include?(e.name) }
135
137
  elsif opts[:relevance] && router_enabled?
136
138
  keep = rank({ query: opts[:relevance], entries: pool }.merge(pref_fwd)).first(opts[:top_k] || 10).map(&:name)
137
139
  pinned = domain_pin_names(query: opts[:relevance])
138
- names = (CORE_TOOLS + keep + pinned).uniq
140
+ names = (CORE_TOOLS + keep + pinned + relevant).uniq
139
141
  pool = pool.select { |e| names.include?(e.name) }
140
142
  end
141
143
 
142
144
  pool = apply_preference({ items: pool }.merge(pref_fwd))
143
- pool.map { |e| { type: 'function', function: e.schema } }
145
+ pool.map do |entry|
146
+ schema = entry.schema
147
+ schema = schema.merge(description: "#{schema[:description]} #{mcp[:description]}") if entry.name == 'mcp'
148
+ { type: 'function', function: schema }
149
+ end
144
150
  end
145
151
 
146
152
  # Supported Method Parameters::
@@ -361,6 +367,30 @@ module PWN
361
367
  end
362
368
  end
363
369
 
370
+ # Read only local client metadata: schema discovery must never spawn a
371
+ # server, connect hardware, or inherit permission from a matching name.
372
+ private_class_method def self.mcp_routing(opts = {})
373
+ backends = PWN::AI::MCP.backends
374
+ normalize = lambda do |text|
375
+ text.to_s.gsub(/([a-z0-9])([A-Z])/, '\1 \2').downcase.gsub(/[^a-z0-9]+/, ' ').strip
376
+ end
377
+ query = " #{normalize.call(opts[:query])} "
378
+ terms = ['mcp', 'model context protocol'] + backends.flat_map do |backend|
379
+ [backend[:name], backend[:constant].to_s.split('::').last, *Array(backend[:tools])]
380
+ end
381
+ relevant = terms.any? do |term|
382
+ phrase = normalize.call(term)
383
+ !phrase.empty? && query.include?(" #{phrase} ")
384
+ end
385
+ catalog = backends.map do |backend|
386
+ "#{backend[:name]} (#{backend[:constant]}): #{Array(backend[:tools]).join(', ')}"
387
+ end.join('; ')
388
+ {
389
+ relevant: relevant,
390
+ description: "Local backend catalog: #{catalog}. For matching server tasks, use this broker and list_tools to discover live schemas before call_tool. Discovery does not enable hardware."
391
+ }
392
+ end
393
+
364
394
  private_class_method def self.safe_check(opts = {})
365
395
  entry = opts[:entry]
366
396
  entry.check.call
@@ -449,7 +479,7 @@ module PWN
449
479
  preference: 'optional - alias of :order; same key-present rule',
450
480
  kind: 'optional - kind value consumed by #definitions',
451
481
  intent: 'optional - intent value consumed by #definitions',
452
- core_only: 'optional - core only value consumed by #definitions',
482
+ core_only: 'optional - restrict to CORE_TOOLS plus the MCP broker when client metadata matches relevance',
453
483
  trusted_context: 'optional - caller observations accepted by Policy.observed_context; defaults to live episode observations'
454
484
  )
455
485
 
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.738'
4
+ VERSION = '0.5.739'
5
5
  end
@@ -47,6 +47,56 @@ 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 'advertises the MCP broker for a named backend in the normal core-only prompt path' do
51
+ described_class.discover
52
+ request = 'Use combo.nation to inspect the menu catalog'
53
+ enabled = PWN::AI::Agent::Loop.send(:default_interactive_toolsets, request: request)
54
+ expect(PWN::AI::MCP::ComboNation).not_to receive(:connect)
55
+ definitions = described_class.definitions(core_only: true, relevance: request, enabled: enabled)
56
+ tool = definitions.find { |row| row.dig(:function, :name) == 'mcp' }
57
+ expect(tool).not_to be_nil
58
+ expect(tool.dig(:function, :description)).to include('combo_nation', 'PWN::AI::MCP::ComboNation', 'menu_catalog')
59
+ end
60
+
61
+ it 'routes MCP names and capabilities without relying on keyword rank or changing unrelated core prompts' do
62
+ described_class.discover
63
+ allow(described_class).to receive(:router_enabled?).and_return(true)
64
+ ['PWN::AI::MCP::ComboNation', 'combo_nation', 'ComboNation', 'combo.nation', 'menu_dial_calibration', 'MCP backends'].each do |query|
65
+ [true, false].each do |core_only|
66
+ names = described_class.definitions(core_only: core_only, relevance: query, top_k: 0).map { |row| row.dig(:function, :name) }
67
+ expect(names).to include('mcp')
68
+ end
69
+ end
70
+ names = described_class.definitions(core_only: true, relevance: 'inspect a local Ruby file').map { |row| row.dig(:function, :name) }
71
+ expect(names).to eq(described_class::CORE_TOOLS)
72
+ end
73
+
74
+ it 'discovers another MCP client from its metadata and respects explicit toolset exclusion' do
75
+ described_class.discover
76
+ client = Module.new
77
+ %i[connect disconnect list_tools call_tool].each { |method| client.define_singleton_method(method) { |_| {} } }
78
+ client.const_set(:TOOLS, %w[inspect_widget])
79
+ stub_const('PWN::AI::MCP::WidgetLab', client)
80
+ expect(client).not_to receive(:connect)
81
+ %w[widget_lab inspect_widget].each do |query|
82
+ tools = described_class.definitions(core_only: true, relevance: query)
83
+ mcp = tools.find { |row| row.dig(:function, :name) == 'mcp' }
84
+ expect(mcp.dig(:function, :description)).to include('widget_lab', 'inspect_widget')
85
+ expect(described_class.definitions(core_only: true, relevance: query, enabled: [])).to eq([])
86
+ names = described_class.definitions(core_only: true, relevance: query, enabled: ['pwn']).map { |row| row.dig(:function, :name) }
87
+ expect(names).not_to include('mcp')
88
+ end
89
+ expect(described_class.lookup(name: 'mcp').schema[:description]).not_to include('WidgetLab')
90
+ end
91
+
92
+ it 'does not let MCP relevance bypass a failed availability check' do
93
+ described_class.discover
94
+ entry = described_class.lookup(name: 'mcp')
95
+ allow(entry.check).to receive(:call).and_return(false)
96
+ tools = described_class.definitions(core_only: true, relevance: 'combo.nation')
97
+ expect(tools.map { |row| row.dig(:function, :name) }).not_to include('mcp')
98
+ end
99
+
50
100
  it 'applies observed shell and Ruby prerequisites to the existing core tools' do
51
101
  described_class.discover
52
102
  context = { environment: :local, capabilities: { shell: false, ruby: false } }
@@ -51,6 +51,37 @@ describe 'PWN::AI::Agent::Tools mcp' do
51
51
  expect(PWN::AI::Agent::Registry.lookup(name: 'mcp').toolset).to eq('mcp')
52
52
  end
53
53
 
54
+ it 'dispatches the broker advertised for a combo.nation prompt and preserves menu IDs across calls' do
55
+ registry = PWN::AI::Agent::Registry
56
+ tools = registry.definitions(core_only: true, relevance: 'Inspect combo.nation menus')
57
+ schema = tools.find { |tool| tool.dig(:function, :name) == 'mcp' }
58
+ expect(schema).not_to be_nil
59
+ fake = start_fake_server
60
+ # Only replace process spawning; exercise the real protocol client, broker,
61
+ # schema validation and Dispatch with JSON arguments as returned by a model.
62
+ allow(PWN::AI::MCP::ComboNation).to receive(:connect).and_wrap_original do |method, opts|
63
+ expect(opts[:allow_hardware]).not_to eq(true)
64
+ method.call(opts.merge(reader: fake[:reader], writer: fake[:writer], timeout: 2))
65
+ end
66
+ calls = [
67
+ { action: 'list_tools', backend: 'combo_nation' },
68
+ { action: 'call_tool', backend: 'combo_nation', name: 'menu_guess', arguments: { option: '5.10' } }
69
+ ]
70
+ results = calls.map do |args|
71
+ JSON.parse(PWN::AI::Agent::Dispatch.call(
72
+ scope_policy: { enabled: false },
73
+ tool_call: { id: 'mcp-test', type: 'function', function: { name: schema.dig(:function, :name), arguments: JSON.generate(args) } }
74
+ ))
75
+ end
76
+ expect(results.first.dig('result', 'tools').map { |tool| tool['name'] }).to include('menu_catalog')
77
+ expect(results.last.dig('result', 'parsed', 'arguments')).to eq('option' => '5.10')
78
+ expect(results.last.dig('result', 'allow_hardware')).to eq(false)
79
+ expect(PWN::AI::MCP::ComboNation).to have_received(:connect).once
80
+ ensure
81
+ PWN::AI::MCP.reset!
82
+ fake[:thread].join(2) if fake
83
+ end
84
+
54
85
  it 'lists MCP backends and drives combo.nation tools through Dispatch-shaped args' do
55
86
  entry = PWN::AI::Agent::Registry.lookup(name: 'mcp')
56
87
  listed = entry.handler.call(action: 'backends')
@@ -671,6 +671,7 @@
671
671
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.eager_load! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.eager_load!`: "}]}
672
672
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.help`: "}]}
673
673
  {"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"}]}
674
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.mcp_routing Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.mcp_routing`: "}]}
674
675
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.metrics_rates Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.metrics_rates`: "}]}
675
676
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.preference_order Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.preference_order`: Supported Method Parameters\n\norder = PWN::AI::Agent::Registry.preference_order(\n\norder: 'optional - Array of tool names; explicit nil/[] disables',\npreference: 'optional - alias of :order. Explicit key disables Env/DEFAULT fallback'\n\n)\n"}]}
676
677
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Registry.rank Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Registry.rank`: Supported Method Parameters\n\nranked = PWN::AI::Agent::Registry.rank(\n\nquery: 'required - user request text',\nentries: 'optional - Entry pool to rank (default .all)',\norder: 'optional - preference list forwarded to preference_order'\n\n)\n\nC1 advantage-weighted router: score = α·keyword_sim + β·(tool rolling success_rate − global rate) + γ·UCB1(tool). Untried tools get an exploration bonus; tools that outperform the fleet get an exploitation bonus. Thompson sampling is available via Metrics.thompson for stochastic routing.\n"}]}
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.738
4
+ version: 0.5.739
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -141,14 +141,14 @@ dependencies:
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: 4.0.20
144
+ version: 4.0.21
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: 4.0.20
151
+ version: 4.0.21
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: bundler-audit
154
154
  requirement: !ruby/object:Gem::Requirement
@@ -519,14 +519,14 @@ dependencies:
519
519
  requirements:
520
520
  - - '='
521
521
  - !ruby/object:Gem::Version
522
- version: 0.0.180
522
+ version: 0.0.183
523
523
  type: :development
524
524
  prerelease: false
525
525
  version_requirements: !ruby/object:Gem::Requirement
526
526
  requirements:
527
527
  - - '='
528
528
  - !ruby/object:Gem::Version
529
- version: 0.0.180
529
+ version: 0.0.183
530
530
  - !ruby/object:Gem::Dependency
531
531
  name: metasm
532
532
  requirement: !ruby/object:Gem::Requirement
@@ -7164,7 +7164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
7164
7164
  - !ruby/object:Gem::Version
7165
7165
  version: '0'
7166
7166
  requirements: []
7167
- rubygems_version: 4.0.20
7167
+ rubygems_version: 4.0.21
7168
7168
  specification_version: 4
7169
7169
  summary: Automated Security Testing for CI/CD Pipelines & Beyond
7170
7170
  test_files: []