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 +4 -4
- data/Gemfile +2 -2
- data/documentation/Agent-Tool-Registry.md +14 -0
- data/lib/pwn/ai/agent/loop.rb +4 -3
- data/lib/pwn/ai/agent/registry.rb +34 -4
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/registry_spec.rb +50 -0
- data/spec/lib/pwn/ai/agent/tools/mcp_spec.rb +31 -0
- data/third_party/pwn_rdoc.jsonl +1 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a7b0a41aca20dc31343279c213c042d7362ff5c0ac59a89150a427111d5b11d
|
|
4
|
+
data.tar.gz: b4335ec85624abed2dbb25d8941ab1db79d73b07fb8b4eca64b90f56a96d15f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -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
|
|
2969
|
-
#
|
|
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
|
|
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 -
|
|
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
|
@@ -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')
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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: []
|