pwn 0.5.782 → 0.5.784
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 +3 -3
- data/Rakefile +19 -1
- data/bin/pwn_setup +1 -1
- data/build_gem.sh +1 -1
- data/documentation/Backup-Retention.md +27 -0
- data/documentation/Cron.md +64 -35
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Installation.md +280 -15
- data/documentation/Persistence.md +1 -1
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/pwn-REPL.md +31 -3
- data/documentation/pwn-ai-Agent.md +62 -2
- data/etc/default_cron/session_cleanup.rb +1544 -44
- data/lib/pwn/ai/agent/curriculum.rb +2 -2
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +59 -11
- data/lib/pwn/ai/agent/mistakes.rb +22 -3
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/swarm.rb +31 -16
- data/lib/pwn/ai/agent/tools/sessions.rb +6 -6
- data/lib/pwn/ai/http_retry.rb +3 -0
- data/lib/pwn/ai/router.rb +48 -3
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/memory.rb +62 -6
- data/lib/pwn/migrate.rb +292 -15
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/log.rb +18 -2
- data/lib/pwn/plugins/repl/ai.rb +25 -4
- data/lib/pwn/plugins/repl/ai_console.rb +41 -21
- data/lib/pwn/plugins/repl/ai_swarm.rb +4 -4
- data/lib/pwn/session_trace.rb +1 -1
- data/lib/pwn/sessions.rb +359 -92
- data/lib/pwn/setup.rb +9 -17
- data/lib/pwn/version.rb +1 -1
- data/scripts/build_gate.rb +58 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/lib/pwn/ai/agent/learning_spec.rb +65 -2
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +3 -3
- data/spec/lib/pwn/ai/router_spec.rb +30 -2
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/memory_spec.rb +58 -1
- data/spec/lib/pwn/migrate_spec.rb +160 -5
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +203 -6
- data/spec/lib/pwn/plugins/repl/ai_result_chronology_spec.rb +2 -2
- data/spec/lib/pwn/plugins/repl/ai_spec.rb +28 -11
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +11 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +166 -3
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +800 -36
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +389 -0
- data/spec/lib/pwn/sessions_spec.rb +228 -16
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/third_party/pwn_rdoc.jsonl +27 -1
- metadata +14 -7
|
@@ -277,6 +277,10 @@ module PWN
|
|
|
277
277
|
class Control < PWN::AI::Agent::Loop::Steering
|
|
278
278
|
class Stopped < Exception; end # rubocop:disable Lint/InheritException
|
|
279
279
|
|
|
280
|
+
def stopping?
|
|
281
|
+
@stopping == true
|
|
282
|
+
end
|
|
283
|
+
|
|
280
284
|
def stop
|
|
281
285
|
@stopping = true
|
|
282
286
|
request_model_cancel
|
|
@@ -637,15 +641,14 @@ module PWN
|
|
|
637
641
|
finish_request_metrics
|
|
638
642
|
@running = false
|
|
639
643
|
@control = nil
|
|
640
|
-
queue_title_update(text) if text
|
|
644
|
+
queue_title_update(*text) if text
|
|
641
645
|
when :session_title
|
|
642
646
|
@session_metadata = text if text && text[:id] == @pry.config.pwn_ai_session_id
|
|
643
647
|
when :title_done
|
|
644
648
|
@title_worker.join
|
|
645
649
|
@title_worker = nil
|
|
646
|
-
pending = @title_pending
|
|
647
|
-
|
|
648
|
-
queue_title_update(pending) if pending
|
|
650
|
+
pending = @title_pending&.shift
|
|
651
|
+
queue_title_update(*pending) if pending
|
|
649
652
|
else
|
|
650
653
|
add(type, text)
|
|
651
654
|
end
|
|
@@ -654,13 +657,16 @@ module PWN
|
|
|
654
657
|
nil
|
|
655
658
|
end
|
|
656
659
|
|
|
657
|
-
def queue_title_update(session_id)
|
|
660
|
+
def queue_title_update(session_id, ai_config = Marshal.load(Marshal.dump(PWN::Env[:ai])))
|
|
661
|
+
@title_pending ||= {}
|
|
658
662
|
if @title_worker
|
|
659
|
-
@title_pending =
|
|
663
|
+
@title_pending[session_id] = ai_config
|
|
660
664
|
return
|
|
661
665
|
end
|
|
662
666
|
@title_worker = Thread.new do
|
|
663
|
-
@events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id
|
|
667
|
+
@events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id, ai_config: ai_config, on_error: lambda { |kind|
|
|
668
|
+
@events << [:warning, "Session title refresh unavailable for #{session_id} (#{kind}); previous title retained. Resume this session to retry."]
|
|
669
|
+
})]
|
|
664
670
|
ensure
|
|
665
671
|
@events << [:title_done, nil]
|
|
666
672
|
end
|
|
@@ -689,8 +695,9 @@ module PWN
|
|
|
689
695
|
PWN::Plugins::Log.mirror_tui!(msg: "#{type.to_s.upcase}\n#{clean}") if @pry.config.pwn_ai_debug && type != :notice
|
|
690
696
|
end
|
|
691
697
|
|
|
692
|
-
def start_request(request, command: false)
|
|
698
|
+
def start_request(request, command: false, &operation)
|
|
693
699
|
session_id = @pry.config.pwn_ai_session_id
|
|
700
|
+
title_config = Marshal.load(Marshal.dump(PWN::Env[:ai]))
|
|
694
701
|
@locals = Thread.current.keys.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, Thread.current[key]] }
|
|
695
702
|
@thread_variables = Thread.current.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, Thread.current.thread_variable_get(key)] }
|
|
696
703
|
# A fresh forwarding pipe prevents unread input crossing requests.
|
|
@@ -725,16 +732,24 @@ module PWN
|
|
|
725
732
|
end
|
|
726
733
|
if command
|
|
727
734
|
control.checkpoint(messages: [], phase: :tool)
|
|
728
|
-
if
|
|
735
|
+
if operation
|
|
736
|
+
operation.call
|
|
737
|
+
elsif request.match?(%r{\A/model(?:\s|$)})
|
|
729
738
|
REPL.pwn_ai_run_model(args: request.split.drop(1), prompt: lambda { |selection|
|
|
730
739
|
control.checkpoint(messages: [])
|
|
731
740
|
@events << [:model_prompt, selection]
|
|
732
741
|
:deferred
|
|
733
742
|
})
|
|
734
743
|
else
|
|
735
|
-
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry)
|
|
744
|
+
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry, raise_errors: true)
|
|
745
|
+
end
|
|
746
|
+
# Maintenance is one safe transaction: never label its committed
|
|
747
|
+
# deletions cancelled merely because stop was requested mid-write.
|
|
748
|
+
if request.match?(%r{\A/(?:sessions\s+cleanup|maintenance)(?:\s|$)})
|
|
749
|
+
@events << [:notice, 'Session cleanup finished; completed work is not undone.']
|
|
750
|
+
else
|
|
751
|
+
control.checkpoint(messages: [])
|
|
736
752
|
end
|
|
737
|
-
control.checkpoint(messages: [])
|
|
738
753
|
else
|
|
739
754
|
workspace = File.realpath(Dir.pwd)
|
|
740
755
|
workspace_lock = File.open(File.join(Dir.tmpdir, "pwn-solve-#{Digest::SHA256.hexdigest(workspace)}.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) # rubocop:disable Style/FileOpen -- worker ensure owns the lease
|
|
@@ -757,7 +772,7 @@ module PWN
|
|
|
757
772
|
@events << [:warning, "#{e.class}: #{e.message}"]
|
|
758
773
|
ensure
|
|
759
774
|
workspace_lock&.close
|
|
760
|
-
@events << [:done, command ? nil : session_id]
|
|
775
|
+
@events << [:done, command ? nil : [session_id, title_config]]
|
|
761
776
|
end
|
|
762
777
|
@worker.report_on_exception = false
|
|
763
778
|
@control = ready.pop
|
|
@@ -810,7 +825,7 @@ module PWN
|
|
|
810
825
|
clear_view
|
|
811
826
|
elsif ['/sessions', '/sessions picker'].include?(line.strip)
|
|
812
827
|
open_operator(:sessions)
|
|
813
|
-
elsif line.strip == '/sessions cleanup'
|
|
828
|
+
elsif line.strip == '/sessions cleanup --dry-run'
|
|
814
829
|
open_operator(:cleanup)
|
|
815
830
|
elsif line.strip == '/evidence'
|
|
816
831
|
open_operator(:evidence)
|
|
@@ -837,7 +852,7 @@ module PWN
|
|
|
837
852
|
open_vault_edit(line)
|
|
838
853
|
elsif line.strip == '/system-role'
|
|
839
854
|
open_system_role
|
|
840
|
-
elsif line.match?(%r{\A/(?:mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$))})
|
|
855
|
+
elsif line.match?(%r{\A/(?:maintenance(?:\s|$)|mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$)|sessions\s+cleanup(?:\s|$))})
|
|
841
856
|
start_request(line, command: true)
|
|
842
857
|
elsif line.start_with?('/') || line.start_with?('ai.profile ', 'ai.memory ')
|
|
843
858
|
if line.match?(%r{\A/sessions[[:space:]]+resume(?:[[:space:]]|$)})
|
|
@@ -845,6 +860,7 @@ module PWN
|
|
|
845
860
|
|
|
846
861
|
REPL.pwn_ai_dispatch_slash!(request: line, pry: @pry)
|
|
847
862
|
@session_metadata = PWN::Sessions.metadata(session_id: @pry.config.pwn_ai_session_id)
|
|
863
|
+
queue_title_update(@pry.config.pwn_ai_session_id)
|
|
848
864
|
elsif !REPL.pwn_ai_dispatch_slash!(request: line, pry: @pry)
|
|
849
865
|
add(:warning, 'Unknown command. Use / or Tab for commands.')
|
|
850
866
|
end
|
|
@@ -2027,7 +2043,7 @@ module PWN
|
|
|
2027
2043
|
view[:rows] = PWN::Sessions.list.sort_by { |row| [row[:mtime].to_s, row[:id]] } if view[:kind] == :sessions
|
|
2028
2044
|
if view[:kind] == :cleanup
|
|
2029
2045
|
require File.expand_path('../../../../etc/default_cron/session_cleanup', __dir__)
|
|
2030
|
-
view[:plan] = PWN::SessionCleanup.
|
|
2046
|
+
view[:plan] = PWN::SessionCleanup.maintain(dry_run: true, current_session_id: session_id)
|
|
2031
2047
|
view[:rows] = view[:plan][:records].map do |row|
|
|
2032
2048
|
category = row[:skip] || row[:apply_blocked_by] || (if row[:classification] == :named
|
|
2033
2049
|
:summarized
|
|
@@ -2352,7 +2368,7 @@ module PWN
|
|
|
2352
2368
|
['START HERE · local guide; no work is launched here.',
|
|
2353
2369
|
'Write a task in MISSION CONTROL. /readiness checks routes, tools and workspace locally.',
|
|
2354
2370
|
'Ctrl+S opens the team workspace. v previews your exact mission draft before confirmation.',
|
|
2355
|
-
'/evidence inspects actual checks and revisions. /sessions
|
|
2371
|
+
'/evidence inspects actual checks and revisions. /sessions cleanup runs maintenance; add --dry-run to preview.',
|
|
2356
2372
|
'/learning inbox reviews retained evidence. /steer queues guidance; Ctrl+C requests cancellation, not undo.',
|
|
2357
2373
|
'1 Insert: Inspect this workspace and explain its entry points without changing files.',
|
|
2358
2374
|
'2 Insert: Add a regression test for the reported bug, fix it, and execute the test.',
|
|
@@ -2390,13 +2406,17 @@ module PWN
|
|
|
2390
2406
|
end
|
|
2391
2407
|
|
|
2392
2408
|
def apply_operator_cleanup
|
|
2409
|
+
return add(:warning, 'Maintenance waits until the active request or swarm finishes.') if busy?
|
|
2410
|
+
|
|
2393
2411
|
view = @operator_view
|
|
2394
|
-
approved = view[:plan][:records].select { |row| view[:plan][:candidate_ids].include?(row[:id]) }.to_h { |row| [row[:id], row[:files]] }
|
|
2395
|
-
|
|
2412
|
+
approved = view[:plan][:records].select { |row| view[:plan][:candidate_ids].include?(row[:id]) }.to_h { |row| [row[:id], { files: row[:files], dependency_records: row[:dependency_records] }] }
|
|
2413
|
+
session_id = @pry.config.pwn_ai_session_id
|
|
2396
2414
|
view.delete(:confirm)
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2415
|
+
@operator_view = nil
|
|
2416
|
+
start_request('/sessions cleanup (approved preview)', command: true) do
|
|
2417
|
+
result = PWN::SessionCleanup.run(dry_run: false, current_session_id: session_id, approved_candidates: approved)
|
|
2418
|
+
@events << [:command, REPL.pwn_ai_result_text(value: result)]
|
|
2419
|
+
end
|
|
2400
2420
|
end
|
|
2401
2421
|
|
|
2402
2422
|
def operator_viewport(page)
|
|
@@ -164,7 +164,7 @@ module PWN
|
|
|
164
164
|
when 'create' then create(tokens.join(' '))
|
|
165
165
|
when 'use' then use(tokens.first)
|
|
166
166
|
when 'spawn' then spawn(tokens)
|
|
167
|
-
when 'retire' then retire(tokens.first)
|
|
167
|
+
when 'retire' then retire(tokens.first, continuation: tokens.include?('--continuation'))
|
|
168
168
|
when 'dm' then ask(tokens, opts)
|
|
169
169
|
when 'mission' then solve(tokens, opts)
|
|
170
170
|
when 'debate' then debate(tokens, opts)
|
|
@@ -180,7 +180,7 @@ module PWN
|
|
|
180
180
|
def help_text
|
|
181
181
|
{
|
|
182
182
|
ok: true,
|
|
183
|
-
usage: '/swarm mission REQUEST|agents|status [JOB]|create [topic]|use ID|spawn NAME ROLE [--engine E --model M --toolsets a,b]|retire NAME|dm NAME REQUEST|debate NAMES TOPIC [--rounds N]|broadcast REQUEST [--names a,b]|tail [N]|steer JOB INSTRUCTION|pause JOB|resume JOB|cancel JOB|all'
|
|
183
|
+
usage: '/swarm mission REQUEST|agents|status [JOB]|create [topic]|use ID|spawn NAME ROLE [--engine E --model M --toolsets a,b]|retire NAME [--continuation]|dm NAME REQUEST|debate NAMES TOPIC [--rounds N]|broadcast REQUEST [--names a,b]|tail [N]|steer JOB INSTRUCTION|pause JOB|resume JOB|cancel JOB|all'
|
|
184
184
|
}
|
|
185
185
|
end
|
|
186
186
|
|
|
@@ -215,10 +215,10 @@ module PWN
|
|
|
215
215
|
{ ok: true, persona: result[:name], swarm_id: @swarm_id, scope: :swarm }
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
-
def retire(name)
|
|
218
|
+
def retire(name, continuation: false)
|
|
219
219
|
return { ok: false, error: 'invalid persona name' } unless identifier?(name)
|
|
220
220
|
|
|
221
|
-
result = PWN::AI::Agent::Swarm.retire(name: name, swarm_id: @swarm_id, local: true)
|
|
221
|
+
result = PWN::AI::Agent::Swarm.retire(name: name, swarm_id: @swarm_id, local: true, continuation: continuation)
|
|
222
222
|
{ ok: result[:removed], scope: result[:scope], name: name }
|
|
223
223
|
end
|
|
224
224
|
|
data/lib/pwn/session_trace.rb
CHANGED
|
@@ -119,7 +119,7 @@ module PWN
|
|
|
119
119
|
id = opts[:session_id].to_s
|
|
120
120
|
raise ArgumentError, 'Invalid session id' unless id.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]{0,127}\z/)
|
|
121
121
|
|
|
122
|
-
dir = File.
|
|
122
|
+
dir = File.dirname(PWN::Sessions.support_path(kind: :trace, session_id: id))
|
|
123
123
|
raise ArgumentError, 'Symlinked trace directory' if File.symlink?(dir)
|
|
124
124
|
|
|
125
125
|
File.join(dir, 'trace.jsonl')
|