pwn 0.5.674 → 0.5.676
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 +1 -1
- data/bin/pwn +2 -0
- data/bin/pwn_setup +28 -2
- data/lib/pwn/ai/agent/learning.rb +5 -0
- data/lib/pwn/ai/agent/loop.rb +13 -0
- data/lib/pwn/ai/agent/policy.rb +15 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +4 -1
- data/lib/pwn/ai/agent/prompt_cache.rb +253 -0
- data/lib/pwn/ai/agent/reflect.rb +3 -1
- data/lib/pwn/ai/agent/reward.rb +27 -3
- data/lib/pwn/ai/agent/task_summarizer.rb +20 -3
- data/lib/pwn/ai/agent/turn_finalizer.rb +231 -0
- data/lib/pwn/ai/agent.rb +2 -0
- data/lib/pwn/ai/anthropic.rb +139 -5
- data/lib/pwn/ai/gemini.rb +24 -1
- data/lib/pwn/ai/grok.rb +202 -9
- data/lib/pwn/ai/ollama.rb +30 -5
- data/lib/pwn/ai/open_ai.rb +152 -10
- data/lib/pwn/ai/open_web_ui.rb +12 -0
- data/lib/pwn/ai.rb +86 -0
- data/lib/pwn/config.rb +7 -0
- data/lib/pwn/cron.rb +1297 -64
- data/lib/pwn/plugins/repl.rb +3 -2
- data/lib/pwn/setup.rb +98 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/documentation/installation_md_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +13 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +22 -0
- data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +13 -0
- data/spec/lib/pwn/ai/agent/prompt_cache_spec.rb +118 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +106 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +12 -0
- data/spec/lib/pwn/ai/gemini_spec.rb +10 -0
- data/spec/lib/pwn/ai/grok_spec.rb +21 -0
- data/spec/lib/pwn/ai/ollama_spec.rb +30 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +40 -0
- data/spec/lib/pwn/ai/open_web_ui_spec.rb +9 -0
- data/spec/lib/pwn/ai_spec.rb +23 -0
- data/spec/lib/pwn/cron_spec.rb +289 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +12 -0
- data/spec/lib/pwn/setup_spec.rb +74 -0
- data/spec/support/sandbox.rb +4 -0
- data/third_party/pwn_rdoc.jsonl +140 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65d8aa03f93adc593efbfb8b342885f8ceb815a19a6a8ef8e72c3b0a493a32a5
|
|
4
|
+
data.tar.gz: eee3d957a5975a105240040cf6bd2f0a44a05d35cbac3240ec252f913cc38c5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03f6a8960538e278b4041981a64ad27c4163e3748a11866c559c81da75152644f62b61658f695369f0281832e03d0810f012f47387b3ff4c456ad2df964b365c
|
|
7
|
+
data.tar.gz: 9b3eec90f64577c1167ae556d713f07f82f1c76d43255a14b7a1b69eaec5cfa2bdd9d99343d0a338aef40d1aa9e6fbd2c4c89d72e9e5d7b35b6a69ab504cdc07
|
data/Gemfile
CHANGED
data/bin/pwn
CHANGED
|
@@ -51,9 +51,11 @@ begin
|
|
|
51
51
|
# ------------------------------------------------------------------
|
|
52
52
|
if opts[:setup] == true
|
|
53
53
|
r = PWN::Setup.check
|
|
54
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
54
55
|
exit(r[:ok] ? 0 : 1)
|
|
55
56
|
else
|
|
56
57
|
PWN::Setup.deps(profile: opts[:setup])
|
|
58
|
+
PWN::Setup.ensure_cron(restart: false)
|
|
57
59
|
end
|
|
58
60
|
elsif opts[:ai]
|
|
59
61
|
# ------------------------------------------------------------------
|
data/bin/pwn_setup
CHANGED
|
@@ -54,11 +54,27 @@ pwn_driver = PWN::Driver::Parser.new do |options|
|
|
|
54
54
|
'<Optional - Print the commands that would run, do not execute>') do |o|
|
|
55
55
|
opts[:dry_run] = o
|
|
56
56
|
end
|
|
57
|
+
|
|
58
|
+
options.on('--[no-]cron',
|
|
59
|
+
'<Optional - Enable (default) or disable OS-native persistence of pwn-ai cron jobs>') do |o|
|
|
60
|
+
opts[:cron] = o
|
|
61
|
+
end
|
|
57
62
|
end
|
|
58
63
|
pwn_driver.auto_opts_help = false
|
|
59
64
|
pwn_driver.parse!
|
|
60
65
|
|
|
61
66
|
begin
|
|
67
|
+
status = 0
|
|
68
|
+
# Seed default jobs + start/reuse the in-process scheduler so last_run
|
|
69
|
+
# actually advances. Do this BEFORE the doctor so `pwn setup` reports
|
|
70
|
+
# the worker as running. Skip list/terminal (no host scheduler needed)
|
|
71
|
+
# and dry-run (must not spawn).
|
|
72
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
73
|
+
cron_opts = { restart: false, dry_run: false }
|
|
74
|
+
cron_opts[:enabled] = opts[:cron] unless opts[:cron].nil?
|
|
75
|
+
PWN::Setup.ensure_cron(**cron_opts)
|
|
76
|
+
end
|
|
77
|
+
|
|
62
78
|
if opts[:list]
|
|
63
79
|
PWN::Setup.list_profiles
|
|
64
80
|
elsif opts[:migrate]
|
|
@@ -66,7 +82,7 @@ begin
|
|
|
66
82
|
fix: opts[:fix] || opts[:yes],
|
|
67
83
|
dry_run: opts[:dry_run]
|
|
68
84
|
)
|
|
69
|
-
|
|
85
|
+
status = 1 unless r && r[:after] && r[:after][:incompatible].empty?
|
|
70
86
|
elsif opts[:terminal]
|
|
71
87
|
PWN::Setup.terminal(
|
|
72
88
|
yes: opts[:yes],
|
|
@@ -83,8 +99,18 @@ begin
|
|
|
83
99
|
# `pwn setup --check` → same
|
|
84
100
|
# `pwn setup --check --profile net` → gate ONLY on the :net profile
|
|
85
101
|
r = PWN::Setup.check(profile: opts[:profile])
|
|
86
|
-
|
|
102
|
+
status = 1 unless r[:ok]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Seed default jobs + start/reuse the in-process scheduler so
|
|
106
|
+
# last_run actually advances. Skip list/terminal (no host scheduler
|
|
107
|
+
# needed) and dry-run (must not spawn).
|
|
108
|
+
unless opts[:list] || opts[:terminal] || opts[:dry_run]
|
|
109
|
+
cron_opts = { restart: false, dry_run: false }
|
|
110
|
+
cron_opts[:enabled] = opts[:cron] unless opts[:cron].nil?
|
|
111
|
+
PWN::Setup.ensure_cron(**cron_opts)
|
|
87
112
|
end
|
|
113
|
+
exit status
|
|
88
114
|
rescue Interrupt
|
|
89
115
|
puts "\naborted."
|
|
90
116
|
exit 130
|
|
@@ -460,6 +460,11 @@ module PWN
|
|
|
460
460
|
return unless session_id
|
|
461
461
|
return unless auto_introspect_enabled?
|
|
462
462
|
|
|
463
|
+
# Hermes split: Loop.run is on the stack → defer critic/judge/PRM/HER
|
|
464
|
+
# onto a daemon thread so the user already has the reply. Specs,
|
|
465
|
+
# cron, and tools pass through (inline:true or no user-path depth).
|
|
466
|
+
return TurnFinalizer.defer(opts.merge(inline: true)) if defined?(TurnFinalizer) && !opts[:inline] && TurnFinalizer.should_defer?
|
|
467
|
+
|
|
463
468
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
464
469
|
stages_run = []
|
|
465
470
|
stages_skipped = []
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1063,6 +1063,16 @@ module PWN
|
|
|
1063
1063
|
# 3.2 — local models cannot afford auto_introspect (judge+prm+critic+
|
|
1064
1064
|
# sentinel+extro) on every success. Default :failure_only when local.
|
|
1065
1065
|
private_class_method def self.should_auto_introspect?(opts = {})
|
|
1066
|
+
kind = (opts[:kind] || Thread.current[:pwn_request_kind]).to_s.to_sym
|
|
1067
|
+
intent = (opts[:intent] || Thread.current[:pwn_request_intent]).to_s.to_sym
|
|
1068
|
+
fails = opts[:turn_fails].is_a?(Hash) ? opts[:turn_fails].values.sum : 0
|
|
1069
|
+
# Cheap answers already returned user-visible text. The post-answer
|
|
1070
|
+
# critic + 12s ORM printed ERROR: Timed out reading data from server
|
|
1071
|
+
# after greetings / takes / questions.
|
|
1072
|
+
return false if %i[greeting howto recall].include?(intent)
|
|
1073
|
+
return false if kind == :statement
|
|
1074
|
+
return false if kind == :question && fails.zero?
|
|
1075
|
+
|
|
1066
1076
|
return true unless opts[:local]
|
|
1067
1077
|
|
|
1068
1078
|
policy = agent_flag(key: :local_introspect, default: :failure_only).to_s.to_sym
|
|
@@ -1865,6 +1875,7 @@ module PWN
|
|
|
1865
1875
|
request = opts[:request].to_s
|
|
1866
1876
|
session_id = opts[:session_id]
|
|
1867
1877
|
on_tool = opts[:on_tool]
|
|
1878
|
+
TurnFinalizer.enter_user_path! if defined?(TurnFinalizer)
|
|
1868
1879
|
# Live coalesced "what am I doing" lines for the TUI (not a model tool).
|
|
1869
1880
|
ts_state = (TaskSummarizer.fresh(request: request) if defined?(TaskSummarizer) && TaskSummarizer.enabled? && Thread.current[:pwn_reflect_depth].to_i.zero?)
|
|
1870
1881
|
engine = active_engine
|
|
@@ -2297,6 +2308,8 @@ module PWN
|
|
|
2297
2308
|
maybe_finish_policy(session_id: session_id, proxy_ok: false, ts_state: ts_state)
|
|
2298
2309
|
task_summary_flush!(state: ts_state, on_tool: on_tool)
|
|
2299
2310
|
final_msg
|
|
2311
|
+
ensure
|
|
2312
|
+
TurnFinalizer.leave_user_path! if defined?(TurnFinalizer)
|
|
2300
2313
|
end
|
|
2301
2314
|
|
|
2302
2315
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
data/lib/pwn/ai/agent/policy.rb
CHANGED
|
@@ -434,6 +434,21 @@ module PWN
|
|
|
434
434
|
Thread.current[EP_KEY]
|
|
435
435
|
end
|
|
436
436
|
|
|
437
|
+
# Hermes split: snapshot + clear the live episode so Loop.maybe_finish_policy
|
|
438
|
+
# is a no-op on the user-visible path while TurnFinalizer re-attaches it
|
|
439
|
+
# on the background review thread.
|
|
440
|
+
|
|
441
|
+
public_class_method def self.detach_episode!
|
|
442
|
+
ep = Thread.current[EP_KEY]
|
|
443
|
+
Thread.current[EP_KEY] = nil
|
|
444
|
+
ep
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
public_class_method def self.attach_episode!(opts = {})
|
|
448
|
+
Thread.current[EP_KEY] = opts[:episode]
|
|
449
|
+
opts[:episode]
|
|
450
|
+
end
|
|
451
|
+
|
|
437
452
|
# ----------------------------------------------------------------
|
|
438
453
|
# Persistence / eval
|
|
439
454
|
# ----------------------------------------------------------------
|
|
@@ -41,6 +41,9 @@ module PWN
|
|
|
41
41
|
|
|
42
42
|
# Heredoc (not a "..." literal): an unescaped "..." inside a
|
|
43
43
|
# double-quoted string is parsed as Range (begin..."...end).
|
|
44
|
+
# Skills sit in the STATIC prefix (Hermes index) so prompt-cache
|
|
45
|
+
# breakpoints can pin the persona + SKILLS list; MEMORY/LEARNING
|
|
46
|
+
# remain in the dynamic tail.
|
|
44
47
|
<<~PROMPT
|
|
45
48
|
#{base}
|
|
46
49
|
|
|
@@ -51,7 +54,7 @@ module PWN
|
|
|
51
54
|
pwn : #{pwn_version}
|
|
52
55
|
session_id : #{session_id || '(none)'}
|
|
53
56
|
|
|
54
|
-
#{memory_block(limit: b[:memory], request: request)}#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{
|
|
57
|
+
#{skills_block}#{memory_block(limit: b[:memory], request: request)}#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}#{metrics_block(limit: b[:metrics], engine: engine)}#{policy_block if b[:policy].to_i.positive?}#{extrospection_block if b[:extro]}TOOL USE
|
|
55
58
|
Use the provided function tools to act on the host via NATIVE
|
|
56
59
|
tool_calls / function calling — never print tool invocations as
|
|
57
60
|
plain text (e.g. do NOT write shell(command="...") as your answer).
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module AI
|
|
7
|
+
module Agent
|
|
8
|
+
# Hermes-style prompt-cache breakpoints. The static system prefix +
|
|
9
|
+
# SKILLS index stay cacheable; dynamic MEMORY / LEARNING / MISTAKES /
|
|
10
|
+
# METRICS / EXTROSPECTION stay uncached so a turn-local write is not
|
|
11
|
+
# baked into the prefix.
|
|
12
|
+
#
|
|
13
|
+
# Anthropic: system is an array of text blocks; the last static block
|
|
14
|
+
# carries cache_control: { type: 'ephemeral' }.
|
|
15
|
+
# OpenAI Chat Completions: two system/developer messages (static then
|
|
16
|
+
# dynamic) plus prompt_cache_key for cache routing.
|
|
17
|
+
# xAI Grok Chat Completions: the same message split plus the
|
|
18
|
+
# x-grok-conv-id request header (sticky server routing).
|
|
19
|
+
# Gemini generateContent: systemInstruction.parts = [static, dynamic]
|
|
20
|
+
# so implicit prefix cache can hit the stable head.
|
|
21
|
+
# Ollama: no-op.
|
|
22
|
+
module PromptCache
|
|
23
|
+
STATIC_HEAD = %w[ENVIRONMENT].freeze
|
|
24
|
+
DYNAMIC_HEADS = [
|
|
25
|
+
'MEMORY',
|
|
26
|
+
'RECENT TURNS',
|
|
27
|
+
'LEARNING',
|
|
28
|
+
'OPERATOR INBOX',
|
|
29
|
+
'KNOWN MISTAKES',
|
|
30
|
+
'KNOWN FIXES',
|
|
31
|
+
'TOOL EFFECTIVENESS',
|
|
32
|
+
'POLICY',
|
|
33
|
+
'EXTROSPECTION',
|
|
34
|
+
'TOOL USE'
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# Supported Method Parameters::
|
|
38
|
+
# ok = PWN::AI::Agent::PromptCache.enabled?(engine: :anthropic)
|
|
39
|
+
|
|
40
|
+
public_class_method def self.enabled?(opts = {})
|
|
41
|
+
flag = agent_flag(key: :prompt_cache, default: true)
|
|
42
|
+
return false if flag == false || flag.to_s.match?(/\A(0|false|no|off)\z/i)
|
|
43
|
+
|
|
44
|
+
engine = (opts[:engine] || active_engine).to_s.downcase.to_sym
|
|
45
|
+
supports.include?(engine)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Supported Method Parameters::
|
|
51
|
+
# parts = PWN::AI::Agent::PromptCache.split_system(text: system_prompt)
|
|
52
|
+
#
|
|
53
|
+
# → { static: String, dynamic: String, skills: String }
|
|
54
|
+
|
|
55
|
+
public_class_method def self.split_system(opts = {})
|
|
56
|
+
text = opts[:text].to_s
|
|
57
|
+
return { static: '', dynamic: '', skills: '' } if text.empty?
|
|
58
|
+
|
|
59
|
+
dyn_idx = first_dynamic_index(text: text)
|
|
60
|
+
if dyn_idx.nil?
|
|
61
|
+
skills = extract_skills(text: text)
|
|
62
|
+
return { static: text, dynamic: '', skills: skills }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
static = text[0...dyn_idx].rstrip
|
|
66
|
+
dynamic = text[dyn_idx..].to_s
|
|
67
|
+
{ static: static, dynamic: dynamic, skills: extract_skills(text: static) }
|
|
68
|
+
rescue StandardError
|
|
69
|
+
{ static: opts[:text].to_s, dynamic: '', skills: '' }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Supported Method Parameters::
|
|
73
|
+
# blocks = PWN::AI::Agent::PromptCache.anthropic_system_blocks(text: system_prompt)
|
|
74
|
+
#
|
|
75
|
+
# → [ {type:'text', text: static, cache_control:{type:'ephemeral'}},
|
|
76
|
+
# {type:'text', text: dynamic} ] (empty blocks omitted)
|
|
77
|
+
|
|
78
|
+
public_class_method def self.anthropic_system_blocks(opts = {})
|
|
79
|
+
parts = split_system(text: opts[:text])
|
|
80
|
+
blocks = []
|
|
81
|
+
unless parts[:static].to_s.strip.empty?
|
|
82
|
+
blocks << {
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: parts[:static],
|
|
85
|
+
cache_control: { type: 'ephemeral' }
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
blocks << { type: 'text', text: parts[:dynamic] } unless parts[:dynamic].to_s.strip.empty?
|
|
89
|
+
blocks
|
|
90
|
+
rescue StandardError
|
|
91
|
+
txt = opts[:text].to_s
|
|
92
|
+
return [] if txt.strip.empty?
|
|
93
|
+
|
|
94
|
+
[{ type: 'text', text: txt }]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Supported Method Parameters::
|
|
98
|
+
# messages = PWN::AI::Agent::PromptCache.openai_messages(messages: [...])
|
|
99
|
+
#
|
|
100
|
+
# Split the first system/developer message into a stable static
|
|
101
|
+
# prefix and a turn-local dynamic tail so OpenAI / xAI implicit
|
|
102
|
+
# prefix matching can hit across turns. Does NOT emit Anthropic
|
|
103
|
+
# cache_control.
|
|
104
|
+
|
|
105
|
+
public_class_method def self.openai_messages(opts = {})
|
|
106
|
+
messages = Array(opts[:messages]).map { |m| m.is_a?(Hash) ? m.dup : m }
|
|
107
|
+
idx = messages.index do |m|
|
|
108
|
+
next false unless m.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
%w[system developer].include?((m[:role] || m['role']).to_s)
|
|
111
|
+
end
|
|
112
|
+
return messages unless idx
|
|
113
|
+
|
|
114
|
+
msg = messages[idx]
|
|
115
|
+
role = msg[:role] || msg['role']
|
|
116
|
+
text = (msg[:content] || msg['content']).to_s
|
|
117
|
+
parts = split_system(text: text)
|
|
118
|
+
return messages if parts[:static].to_s.strip.empty? || parts[:dynamic].to_s.strip.empty?
|
|
119
|
+
|
|
120
|
+
static_msg = msg.dup
|
|
121
|
+
if static_msg.key?(:content) || !static_msg.key?('content')
|
|
122
|
+
static_msg[:content] = parts[:static]
|
|
123
|
+
else
|
|
124
|
+
static_msg['content'] = parts[:static]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
dynamic_msg = if msg.key?(:role) || !msg.key?('role')
|
|
128
|
+
{ role: role, content: parts[:dynamic] }
|
|
129
|
+
else
|
|
130
|
+
{ 'role' => role, 'content' => parts[:dynamic] }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
messages[idx, 1] = [static_msg, dynamic_msg]
|
|
134
|
+
messages
|
|
135
|
+
rescue StandardError
|
|
136
|
+
Array(opts[:messages])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Supported Method Parameters::
|
|
140
|
+
# inst = PWN::AI::Agent::PromptCache.gemini_system_instruction(text: system_prompt)
|
|
141
|
+
#
|
|
142
|
+
# → { parts: [{text: static}, {text: dynamic}] } (empty omitted)
|
|
143
|
+
|
|
144
|
+
public_class_method def self.gemini_system_instruction(opts = {})
|
|
145
|
+
parts = split_system(text: opts[:text])
|
|
146
|
+
out = []
|
|
147
|
+
out << { text: parts[:static] } unless parts[:static].to_s.strip.empty?
|
|
148
|
+
out << { text: parts[:dynamic] } unless parts[:dynamic].to_s.strip.empty?
|
|
149
|
+
return nil if out.empty?
|
|
150
|
+
|
|
151
|
+
{ parts: out }
|
|
152
|
+
rescue StandardError
|
|
153
|
+
txt = opts[:text].to_s
|
|
154
|
+
return nil if txt.strip.empty?
|
|
155
|
+
|
|
156
|
+
{ parts: [{ text: txt }] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Supported Method Parameters::
|
|
160
|
+
# key = PWN::AI::Agent::PromptCache.cache_key(text: system_prompt)
|
|
161
|
+
# key = PWN::AI::Agent::PromptCache.cache_key(key: 'explicit-override')
|
|
162
|
+
#
|
|
163
|
+
# Stable OpenAI prompt_cache_key / xAI x-grok-conv-id. Hashes the
|
|
164
|
+
# static prefix so MEMORY / LEARNING churn does not rotate the key.
|
|
165
|
+
|
|
166
|
+
public_class_method def self.cache_key(opts = {})
|
|
167
|
+
explicit = opts[:key]
|
|
168
|
+
return explicit.to_s unless explicit.to_s.strip.empty?
|
|
169
|
+
|
|
170
|
+
raw = opts[:text].to_s
|
|
171
|
+
static = raw.empty? ? '' : split_system(text: raw)[:static].to_s
|
|
172
|
+
static = raw if static.strip.empty?
|
|
173
|
+
static = 'pwn-ai' if static.strip.empty?
|
|
174
|
+
"pwn-#{Digest::SHA256.hexdigest(static)[0, 24]}"
|
|
175
|
+
rescue StandardError
|
|
176
|
+
'pwn-ai'
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Supported Method Parameters::
|
|
180
|
+
# n = PWN::AI::Agent::PromptCache.cache_marks(blocks: [...])
|
|
181
|
+
|
|
182
|
+
public_class_method def self.cache_marks(opts = {})
|
|
183
|
+
Array(opts[:blocks]).count do |b|
|
|
184
|
+
next false unless b.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
cc = b[:cache_control] || b['cache_control']
|
|
187
|
+
cc.is_a?(Hash) && (cc[:type] || cc['type']).to_s == 'ephemeral'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
public_class_method def self.supports
|
|
192
|
+
%i[anthropic openai grok gemini]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
public_class_method def self.authors
|
|
196
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
public_class_method def self.help
|
|
200
|
+
puts <<~USAGE
|
|
201
|
+
USAGE:
|
|
202
|
+
parts = PWN::AI::Agent::PromptCache.split_system(text: system_prompt)
|
|
203
|
+
blocks = PWN::AI::Agent::PromptCache.anthropic_system_blocks(text: system_prompt)
|
|
204
|
+
msgs = PWN::AI::Agent::PromptCache.openai_messages(messages: [...])
|
|
205
|
+
inst = PWN::AI::Agent::PromptCache.gemini_system_instruction(text: system_prompt)
|
|
206
|
+
key = PWN::AI::Agent::PromptCache.cache_key(text: system_prompt)
|
|
207
|
+
# Engine chat_with_tools uses the helpers automatically when
|
|
208
|
+
# PWN::Env[:ai][:agent][:prompt_cache] is truthy (default).
|
|
209
|
+
|
|
210
|
+
Toggle via PWN::Env[:ai][:agent][:prompt_cache]
|
|
211
|
+
#{self}.authors
|
|
212
|
+
USAGE
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private_class_method def self.first_dynamic_index(opts = {})
|
|
216
|
+
text = opts[:text].to_s
|
|
217
|
+
idxs = DYNAMIC_HEADS.filter_map do |head|
|
|
218
|
+
i = text.index(/^#{Regexp.escape(head)}\b/)
|
|
219
|
+
i
|
|
220
|
+
end
|
|
221
|
+
idxs.min
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private_class_method def self.extract_skills(opts = {})
|
|
225
|
+
text = opts[:text].to_s
|
|
226
|
+
m = text.match(%r{^SKILLS[^\n]*\n(?:.*\n)*?(?=\n[A-Z][A-Z /]|\z)})
|
|
227
|
+
m ? m[0] : ''
|
|
228
|
+
rescue StandardError
|
|
229
|
+
''
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private_class_method def self.active_engine
|
|
233
|
+
return :openai unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
PWN::Env.dig(:ai, :active).to_s.downcase.to_sym
|
|
236
|
+
rescue StandardError
|
|
237
|
+
:openai
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private_class_method def self.agent_flag(opts = {})
|
|
241
|
+
key = opts[:key]
|
|
242
|
+
default = opts[:default]
|
|
243
|
+
return default unless defined?(PWN::Env)
|
|
244
|
+
|
|
245
|
+
val = PWN::Env.dig(:ai, :agent, key)
|
|
246
|
+
val.nil? ? default : val
|
|
247
|
+
rescue StandardError
|
|
248
|
+
opts[:default]
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
data/lib/pwn/ai/agent/reflect.rb
CHANGED
|
@@ -97,7 +97,8 @@ module PWN
|
|
|
97
97
|
system_role_content: system_role_content,
|
|
98
98
|
spinner: spinner,
|
|
99
99
|
timeout: timeout,
|
|
100
|
-
temp: temp
|
|
100
|
+
temp: temp,
|
|
101
|
+
quiet: opts[:quiet]
|
|
101
102
|
)
|
|
102
103
|
end
|
|
103
104
|
ensure
|
|
@@ -132,6 +133,7 @@ module PWN
|
|
|
132
133
|
chat_opts[:timeout] = opts[:timeout] unless opts[:timeout].nil?
|
|
133
134
|
chat_opts[:temp] = opts[:temp] unless opts[:temp].nil?
|
|
134
135
|
chat_opts[:model] = opts[:model] unless opts[:model].to_s.empty?
|
|
136
|
+
chat_opts[:quiet] = opts[:quiet] unless opts[:quiet].nil?
|
|
135
137
|
mod.chat(chat_opts)
|
|
136
138
|
end
|
|
137
139
|
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -1157,11 +1157,20 @@ module PWN
|
|
|
1157
1157
|
spinner: false,
|
|
1158
1158
|
model: judge_model,
|
|
1159
1159
|
timeout: cheap_orm_timeout,
|
|
1160
|
-
temp: CHEAP_ORM_TEMP
|
|
1160
|
+
temp: CHEAP_ORM_TEMP,
|
|
1161
|
+
quiet: true
|
|
1161
1162
|
)
|
|
1162
1163
|
text = extract_chat_text(resp: raw)
|
|
1163
1164
|
return text unless text.strip.empty?
|
|
1164
|
-
|
|
1165
|
+
|
|
1166
|
+
# Quiet ReadTimeout now returns nil instead of raising. That
|
|
1167
|
+
# empty response already spent the cheap-ORM budget — do not
|
|
1168
|
+
# immediately fire engine_chat_cheap (second 12s stall).
|
|
1169
|
+
return nil
|
|
1170
|
+
rescue StandardError => e
|
|
1171
|
+
# A 12s ReadTimeout already spent the cheap-ORM budget.
|
|
1172
|
+
return nil if timeout_error?(err: e)
|
|
1173
|
+
|
|
1165
1174
|
nil
|
|
1166
1175
|
end
|
|
1167
1176
|
end
|
|
@@ -1191,7 +1200,8 @@ module PWN
|
|
|
1191
1200
|
system_role_content: opts[:system_role_content],
|
|
1192
1201
|
spinner: false,
|
|
1193
1202
|
timeout: cheap_orm_timeout,
|
|
1194
|
-
temp: CHEAP_ORM_TEMP
|
|
1203
|
+
temp: CHEAP_ORM_TEMP,
|
|
1204
|
+
quiet: true
|
|
1195
1205
|
}
|
|
1196
1206
|
model = judge_model
|
|
1197
1207
|
chat_opts[:model] = model unless model.to_s.empty?
|
|
@@ -1200,6 +1210,20 @@ module PWN
|
|
|
1200
1210
|
nil
|
|
1201
1211
|
end
|
|
1202
1212
|
|
|
1213
|
+
private_class_method def self.timeout_error?(opts = {})
|
|
1214
|
+
err = opts[:err]
|
|
1215
|
+
return false if err.nil?
|
|
1216
|
+
|
|
1217
|
+
klass = err.class
|
|
1218
|
+
name = klass.name.to_s
|
|
1219
|
+
return true if name.include?('Timeout') || name.include?('ReadTimeout') || name.include?('OpenTimeout')
|
|
1220
|
+
return true if err.message.to_s.match?(/timed out reading|read timeout|open timeout/i)
|
|
1221
|
+
|
|
1222
|
+
false
|
|
1223
|
+
rescue StandardError
|
|
1224
|
+
false
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1203
1227
|
private_class_method def self.cheap_orm_timeout
|
|
1204
1228
|
n = agent_flag(key: :reward_llm_timeout, default: CHEAP_ORM_TIMEOUT).to_i
|
|
1205
1229
|
n = CHEAP_ORM_TIMEOUT if n < 2
|
|
@@ -566,7 +566,9 @@ module PWN
|
|
|
566
566
|
request: user,
|
|
567
567
|
system_role_content: system,
|
|
568
568
|
suppress_pii_warning: true,
|
|
569
|
-
spinner: false
|
|
569
|
+
spinner: false,
|
|
570
|
+
timeout: sidecar_timeout,
|
|
571
|
+
quiet: true
|
|
570
572
|
)
|
|
571
573
|
text = reflect_text(resp: resp)
|
|
572
574
|
return text unless text.to_s.strip.empty?
|
|
@@ -846,7 +848,9 @@ module PWN
|
|
|
846
848
|
request: user,
|
|
847
849
|
system_role_content: system,
|
|
848
850
|
suppress_pii_warning: true,
|
|
849
|
-
spinner: false
|
|
851
|
+
spinner: false,
|
|
852
|
+
timeout: sidecar_timeout,
|
|
853
|
+
quiet: true
|
|
850
854
|
)
|
|
851
855
|
text = reflect_text(resp: resp)
|
|
852
856
|
return text unless text.to_s.strip.empty?
|
|
@@ -896,7 +900,9 @@ module PWN
|
|
|
896
900
|
r = mod.chat(
|
|
897
901
|
request: opts[:request],
|
|
898
902
|
system_role_content: opts[:system_role_content],
|
|
899
|
-
spinner: false
|
|
903
|
+
spinner: false,
|
|
904
|
+
timeout: sidecar_timeout,
|
|
905
|
+
quiet: true
|
|
900
906
|
)
|
|
901
907
|
reflect_text(resp: r)
|
|
902
908
|
rescue StandardError => e
|
|
@@ -911,6 +917,17 @@ module PWN
|
|
|
911
917
|
:ollama
|
|
912
918
|
end
|
|
913
919
|
|
|
920
|
+
# Kind / plan classification is a sidecar hop. Bound it so a hung
|
|
921
|
+
# model cannot stall the user's real answer, and stay quiet on timeout.
|
|
922
|
+
private_class_method def self.sidecar_timeout
|
|
923
|
+
n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
|
|
924
|
+
n = 20 if n < 5
|
|
925
|
+
n = 60 if n > 60
|
|
926
|
+
n
|
|
927
|
+
rescue StandardError
|
|
928
|
+
20
|
|
929
|
+
end
|
|
930
|
+
|
|
914
931
|
# Parse JSON array, fenced JSON, or numbered/bulleted plain text.
|
|
915
932
|
#
|
|
916
933
|
# Supported Method Parameters::
|