pwn 0.5.784 → 0.5.787
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/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/learning.rb +14 -18
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +4 -0
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/setup.rb +114 -24
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +8 -16
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/migrate_spec.rb +29 -81
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
|
@@ -28,8 +28,8 @@ module PWN
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# Private, in-memory configuration transaction. No plaintext filesystem stage.
|
|
31
|
-
#
|
|
32
|
-
class VaultEdit
|
|
31
|
+
# Uses the shared vault codec for legacy sidecars and YubiKey envelopes.
|
|
32
|
+
class VaultEdit
|
|
33
33
|
class Error < StandardError; end
|
|
34
34
|
|
|
35
35
|
attr_reader :text
|
|
@@ -82,15 +82,17 @@ module PWN
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def serialize(value)
|
|
85
|
-
YAML.dump(value)
|
|
85
|
+
YAML.dump(value)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def credentials
|
|
89
|
+
return PWN::Plugins::Vault.access_options(file: @path) if PWN::Plugins::Vault.hardware_encrypted?(file: @path)
|
|
90
|
+
|
|
89
91
|
safe_file(@decryptor_path)
|
|
90
92
|
result = parse(File.read(@decryptor_path))
|
|
91
93
|
raise Error, 'Decryptor requires the existing key and IV; restore the original sidecar.' unless result.is_a?(Hash) && result[:key].is_a?(String) && result[:iv].is_a?(String)
|
|
92
94
|
|
|
93
|
-
result.slice(:key, :iv)
|
|
95
|
+
PWN::Plugins::Vault.access_options(result.slice(:key, :iv).merge(file: @path, decryptor_file: @decryptor_path))
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def safe_file(path)
|
|
@@ -154,12 +156,7 @@ module PWN
|
|
|
154
156
|
end
|
|
155
157
|
|
|
156
158
|
def encrypt(text)
|
|
157
|
-
|
|
158
|
-
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
|
159
|
-
cipher.encrypt
|
|
160
|
-
cipher.key = Base64.strict_decode64(secrets.fetch(:key))
|
|
161
|
-
cipher.iv = Base64.strict_decode64(secrets.fetch(:iv))
|
|
162
|
-
"#{Base64.strict_encode64(cipher.update(text) + cipher.final)}\n"
|
|
159
|
+
PWN::Plugins::Vault.encrypt_data(credentials.merge(data: text))
|
|
163
160
|
end
|
|
164
161
|
|
|
165
162
|
def atomic_write(encrypted, previous)
|
|
@@ -464,6 +461,40 @@ module PWN
|
|
|
464
461
|
end
|
|
465
462
|
end
|
|
466
463
|
|
|
464
|
+
# JSON-only, generation-tagged child events; never deserialize Ruby objects.
|
|
465
|
+
class MissionEvents
|
|
466
|
+
def initialize(io, generation)
|
|
467
|
+
@io = io
|
|
468
|
+
@generation = generation
|
|
469
|
+
@mutex = Mutex.new
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def <<(event)
|
|
473
|
+
@mutex.synchronize { @io.write("#{JSON.generate([@generation, *event])}\n") }
|
|
474
|
+
self
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Parent-side steering endpoint; cancellation belongs to the process owner.
|
|
479
|
+
class MissionControl
|
|
480
|
+
def initialize(io)
|
|
481
|
+
@io = io
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def submit(text)
|
|
485
|
+
@io.puts(JSON.generate(text.to_s))
|
|
486
|
+
@io.flush
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def phase
|
|
490
|
+
'mission process'
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def stop
|
|
494
|
+
:stopping
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
467
498
|
# Own the terminal, event pump and the lifetime of exactly one request.
|
|
468
499
|
class Console # rubocop:disable Metrics/ClassLength -- cohesive single-owner terminal state machine
|
|
469
500
|
COLORS = { operator: :operator, task: :task, thinking: :notice, tool: :tool, result: :result, assistant: :assistant, notice: :notice, warning: :warning }.freeze
|
|
@@ -616,8 +647,27 @@ module PWN
|
|
|
616
647
|
|
|
617
648
|
def drain
|
|
618
649
|
200.times do
|
|
619
|
-
type, text = @events.pop(true)
|
|
650
|
+
type, text, generation = @events.pop(true)
|
|
651
|
+
next if generation && generation != @mission_generation && generation != @swarm_generation
|
|
652
|
+
|
|
620
653
|
case type
|
|
654
|
+
when :command_state
|
|
655
|
+
if text[:ai].is_a?(Hash)
|
|
656
|
+
text[:ai][:active] = text[:ai][:active].to_sym if PWN::Env.dig(:ai, :active).is_a?(Symbol) && text[:ai][:active].is_a?(String)
|
|
657
|
+
PWN::Env[:ai] = text[:ai]
|
|
658
|
+
end
|
|
659
|
+
text.fetch(:config, {}).each do |key, value|
|
|
660
|
+
@pry.config.public_send("#{key}=", value) if %i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace].include?(key)
|
|
661
|
+
end
|
|
662
|
+
when :usage
|
|
663
|
+
@usage.record(text)
|
|
664
|
+
when :mission_done
|
|
665
|
+
@worker.join
|
|
666
|
+
finish_request_metrics
|
|
667
|
+
@running = false
|
|
668
|
+
@control = nil
|
|
669
|
+
@mission = nil
|
|
670
|
+
@mission_commands&.close unless @mission_commands&.closed?
|
|
621
671
|
when :operator_loaded
|
|
622
672
|
generation, data = text
|
|
623
673
|
if @operator_view && @operator_view[:generation] == generation
|
|
@@ -695,97 +745,230 @@ module PWN
|
|
|
695
745
|
PWN::Plugins::Log.mirror_tui!(msg: "#{type.to_s.upcase}\n#{clean}") if @pry.config.pwn_ai_debug && type != :notice
|
|
696
746
|
end
|
|
697
747
|
|
|
698
|
-
def
|
|
748
|
+
private_class_method def self.mission_entry(opts = {})
|
|
749
|
+
data = opts[:descriptor]
|
|
750
|
+
console = allocate
|
|
751
|
+
pry = Pry.new
|
|
752
|
+
data.fetch(:config).each { |key, value| pry.config.public_send("#{key}=", value) }
|
|
753
|
+
console.instance_variable_set(:@pry, pry)
|
|
754
|
+
console.instance_variable_set(:@input, PWN::AI::Agent::MissionNamespace.io(5))
|
|
755
|
+
writer = PWN::AI::Agent::MissionNamespace.io(4)
|
|
756
|
+
if data[:operation] == :console
|
|
757
|
+
console.send(:mission_request_child, data, writer, PWN::AI::Agent::MissionNamespace.io(6))
|
|
758
|
+
else
|
|
759
|
+
console.send(:mission_command_child, data[:request], data[:generation], data[:baseline_ai], writer, data[:cleanup])
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def mission_config
|
|
764
|
+
%i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace pwn_ai_speak pwn_ai_agent].to_h { |key| [key, @pry.config.public_send(key)] }
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def mission_request_child(data, writer, commands)
|
|
768
|
+
request, generation, session_id, title_config = data.values_at(:request, :generation, :session_id, :title_config)
|
|
769
|
+
events = MissionEvents.new(writer, generation)
|
|
770
|
+
output = EventIO.new(events)
|
|
771
|
+
$stdout = $stderr = output
|
|
772
|
+
$stdin = @input
|
|
773
|
+
@pry.config.output = output
|
|
774
|
+
control = Control.new(input: @input, output: output)
|
|
775
|
+
Thread.current[:pwn_steering_input] = control
|
|
776
|
+
Thread.current[:pwn_usage_observer] = ->(usage) { events << [:usage, usage] }
|
|
777
|
+
Thread.new do
|
|
778
|
+
commands.each_line { |line| control.submit(JSON.parse(line)) }
|
|
779
|
+
end
|
|
780
|
+
begin
|
|
781
|
+
workspace = File.realpath(Dir.pwd)
|
|
782
|
+
lock_path = File.join(Dir.tmpdir, "pwn-solve-#{Digest::SHA256.hexdigest(workspace)}.lock")
|
|
783
|
+
File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lease|
|
|
784
|
+
raise IOError, 'Workspace busy: another mission owns this directory.' unless lease.flock(File::LOCK_EX | File::LOCK_NB)
|
|
785
|
+
|
|
786
|
+
events << [:session_title, PWN::Sessions.provisional_title(session_id: session_id, request: request)]
|
|
787
|
+
on_tool = lambda do |name, args, result|
|
|
788
|
+
if %w[task thinking].include?(name.to_s)
|
|
789
|
+
events << [name.to_sym, args.to_s]
|
|
790
|
+
else
|
|
791
|
+
events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
|
|
792
|
+
events << [:result, result.to_s]
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
final = PWN::AI::Agent::Loop.run(request: request, session_id: session_id,
|
|
796
|
+
enabled_toolsets: PWN::Env.dig(:ai, :agent, :toolsets),
|
|
797
|
+
on_tool: on_tool, steering: control, debug: @pry.config.pwn_ai_debug,
|
|
798
|
+
trace: @pry.config.pwn_ai_trace, debug_tee: output)
|
|
799
|
+
promoted = Thread.current[:pwn_thinking_promoted] && final.to_s.strip == Thread.current[:pwn_last_thinking].to_s.strip
|
|
800
|
+
events << [:assistant, final.to_s] unless promoted
|
|
801
|
+
events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id, ai_config: title_config, on_error: ->(kind) { events << [:warning, "Session title refresh unavailable (#{kind}); previous title retained."] })]
|
|
802
|
+
end
|
|
803
|
+
rescue StandardError, Interrupt, Control::Stopped => e
|
|
804
|
+
events << [:warning, "#{e.class}: #{e.message}"]
|
|
805
|
+
ensure
|
|
806
|
+
output.flush
|
|
807
|
+
writer.close
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def mission_command_child(request, generation, baseline_ai, writer, cleanup)
|
|
812
|
+
events = MissionEvents.new(writer, generation)
|
|
813
|
+
output = EventIO.new(events)
|
|
814
|
+
$stdout = $stderr = output
|
|
815
|
+
$stdin = @input
|
|
816
|
+
@pry.config.output = output
|
|
817
|
+
Thread.current[:pwn_steering_input] = Control.new(input: @input, output: output)
|
|
818
|
+
@events = events
|
|
819
|
+
@output = output
|
|
820
|
+
begin
|
|
821
|
+
if cleanup
|
|
822
|
+
result = PWN::SessionCleanup.run(dry_run: false, current_session_id: @pry.config.pwn_ai_session_id, approved_candidates: cleanup)
|
|
823
|
+
events << [:command, REPL.pwn_ai_result_text(value: result)]
|
|
824
|
+
elsif request.match?(%r{\A/model(?:\s|$)})
|
|
825
|
+
REPL.pwn_ai_run_model(args: request.split.drop(1), prompt: lambda { |selection|
|
|
826
|
+
events << [:model_prompt, selection]
|
|
827
|
+
:deferred
|
|
828
|
+
})
|
|
829
|
+
else
|
|
830
|
+
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry, raise_errors: true)
|
|
831
|
+
end
|
|
832
|
+
config = %i[pwn_ai_session_id pwn_ai_debug pwn_ai_trace].to_h { |key| [key, @pry.config.public_send(key)] }
|
|
833
|
+
ai = PWN::Env[:ai] unless JSON.generate(PWN::Env[:ai]) == baseline_ai
|
|
834
|
+
events << [:command_state, { ai: ai, config: config }]
|
|
835
|
+
rescue StandardError, Interrupt, Control::Stopped => e
|
|
836
|
+
events << [:warning, "#{e.class}: #{e.message}"]
|
|
837
|
+
ensure
|
|
838
|
+
output.flush
|
|
839
|
+
writer.close
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def start_mission_request(request)
|
|
844
|
+
return add(:warning, 'A mission is still running; cancel and reap it first.') if busy?
|
|
845
|
+
|
|
846
|
+
@mission_generation = SecureRandom.uuid
|
|
847
|
+
generation = @mission_generation
|
|
699
848
|
session_id = @pry.config.pwn_ai_session_id
|
|
700
849
|
title_config = Marshal.load(Marshal.dump(PWN::Env[:ai]))
|
|
701
|
-
@
|
|
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)] }
|
|
703
|
-
# A fresh forwarding pipe prevents unread input crossing requests.
|
|
704
|
-
@tool_input.close unless @tool_input.closed?
|
|
850
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
705
851
|
forwarded, @tool_input = IO.pipe
|
|
706
852
|
@input.reopen(forwarded)
|
|
707
853
|
forwarded.close
|
|
708
|
-
@cancelling = false
|
|
709
|
-
@command = command
|
|
854
|
+
@cancelling = @command = false
|
|
710
855
|
begin_request_metrics
|
|
711
856
|
add(:operator, request)
|
|
857
|
+
reader, writer = IO.pipe
|
|
858
|
+
commands, @mission_commands = IO.pipe
|
|
859
|
+
@mission = PWN::Plugins::AISandbox::MissionProcess.new(
|
|
860
|
+
descriptor: { operation: :console, request: request, generation: generation, session_id: session_id, title_config: title_config, config: mission_config },
|
|
861
|
+
fds: { 4 => writer, 5 => @input, 6 => commands }
|
|
862
|
+
)
|
|
863
|
+
writer.close
|
|
864
|
+
commands.close
|
|
865
|
+
@control = MissionControl.new(@mission_commands)
|
|
712
866
|
@running = true
|
|
713
|
-
|
|
714
|
-
gate = Queue.new
|
|
867
|
+
mission = @mission
|
|
715
868
|
@worker = Thread.new do
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
ready << control
|
|
722
|
-
gate.pop
|
|
723
|
-
on_tool = lambda do |name, args, result|
|
|
724
|
-
if name.to_s == 'task'
|
|
725
|
-
@events << [:task, args.to_s]
|
|
726
|
-
elsif name.to_s == 'thinking'
|
|
727
|
-
@events << [:thinking, args.to_s]
|
|
728
|
-
else
|
|
729
|
-
@events << [:tool, "#{name}\n#{args.is_a?(String) ? args : args.inspect}"]
|
|
730
|
-
@events << [:result, result.to_s]
|
|
731
|
-
end
|
|
869
|
+
reader.each_line do |line|
|
|
870
|
+
tag, type, text = JSON.parse(line, symbolize_names: true)
|
|
871
|
+
next unless tag == generation && %w[task thinking tool result assistant notice warning session_title usage].include?(type)
|
|
872
|
+
|
|
873
|
+
@events << [type.to_sym, text, tag]
|
|
732
874
|
end
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
875
|
+
rescue StandardError => e
|
|
876
|
+
@events << [:warning, "Mission IPC: #{e.message}", generation]
|
|
877
|
+
ensure
|
|
878
|
+
mission.close
|
|
879
|
+
reader.close
|
|
880
|
+
@events << [:mission_done, nil, generation]
|
|
881
|
+
end
|
|
882
|
+
@worker.report_on_exception = false
|
|
883
|
+
rescue StandardError => e
|
|
884
|
+
[reader, writer, commands, @mission_commands].compact.each { |io| io.close unless io.closed? }
|
|
885
|
+
@mission&.close
|
|
886
|
+
@mission = nil
|
|
887
|
+
@running = false
|
|
888
|
+
add(:warning, "Mission not started: #{e.message}")
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def cancel_mission
|
|
892
|
+
# Invalidate queued events first. Ready is set only after the reader
|
|
893
|
+
# reaps namespace PID 1, including all provider and tool descendants.
|
|
894
|
+
@cancelling = true
|
|
895
|
+
@mission_generation = nil
|
|
896
|
+
@mission.close
|
|
897
|
+
@worker&.join
|
|
898
|
+
@mission_commands&.close unless @mission_commands&.closed?
|
|
899
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
900
|
+
@mission = @control = nil
|
|
901
|
+
@running = false
|
|
902
|
+
finish_request_metrics
|
|
903
|
+
add(:warning, 'Mission cancelled; all owned processes reaped. Completed work is not undone.')
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def start_request(request, command: false, cleanup: nil)
|
|
907
|
+
return start_mission_request(request) unless command
|
|
908
|
+
return add(:warning, 'A mission is still running; cancel and reap it first.') if busy?
|
|
909
|
+
|
|
910
|
+
@mission_generation = SecureRandom.uuid
|
|
911
|
+
generation = @mission_generation
|
|
912
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
913
|
+
forwarded, @tool_input = IO.pipe
|
|
914
|
+
@input.reopen(forwarded)
|
|
915
|
+
forwarded.close
|
|
916
|
+
@cancelling = false
|
|
917
|
+
@command = true
|
|
918
|
+
baseline_ai = JSON.generate(PWN::Env[:ai])
|
|
919
|
+
begin_request_metrics
|
|
920
|
+
add(:operator, request)
|
|
921
|
+
reader, writer = IO.pipe
|
|
922
|
+
@mission = PWN::Plugins::AISandbox::MissionProcess.new(
|
|
923
|
+
descriptor: { operation: :slash, request: request, generation: generation, baseline_ai: baseline_ai, config: mission_config, cleanup: cleanup },
|
|
924
|
+
fds: { 4 => writer, 5 => @input }
|
|
925
|
+
)
|
|
926
|
+
writer.close
|
|
927
|
+
@running = true
|
|
928
|
+
mission = @mission
|
|
929
|
+
@worker = Thread.new do
|
|
930
|
+
reader.each_line do |line|
|
|
931
|
+
tag, type, text = JSON.parse(line, symbolize_names: true)
|
|
932
|
+
next unless tag == generation && %w[notice warning command command_notice model_prompt command_state].include?(type)
|
|
933
|
+
|
|
934
|
+
@events << [type.to_sym, text, tag]
|
|
766
935
|
end
|
|
767
|
-
rescue
|
|
768
|
-
@events << [:warning, e.message]
|
|
769
|
-
rescue StandardError, Interrupt => e
|
|
770
|
-
Thread.current[:pwn_log_progress] = false
|
|
771
|
-
PWN::Plugins::Log.note_exception!(error: e, where: 'curses agent', which_self: AIConsole)
|
|
772
|
-
@events << [:warning, "#{e.class}: #{e.message}"]
|
|
936
|
+
rescue StandardError => e
|
|
937
|
+
@events << [:warning, "Command IPC: #{e.message}", generation]
|
|
773
938
|
ensure
|
|
774
|
-
|
|
775
|
-
|
|
939
|
+
mission.close
|
|
940
|
+
reader.close
|
|
941
|
+
@events << [:mission_done, nil, generation]
|
|
776
942
|
end
|
|
777
943
|
@worker.report_on_exception = false
|
|
778
|
-
|
|
779
|
-
|
|
944
|
+
rescue StandardError => e
|
|
945
|
+
[reader, writer].compact.each { |io| io.close unless io.closed? }
|
|
946
|
+
@mission&.close
|
|
947
|
+
@mission = nil
|
|
948
|
+
@running = false
|
|
949
|
+
add(:warning, "Command not started: #{e.message}")
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def swarm_event_callback
|
|
953
|
+
@swarm_generation ||= SecureRandom.uuid
|
|
954
|
+
generation = @swarm_generation
|
|
955
|
+
->(type, text) { @events << [type, text, generation] }
|
|
780
956
|
end
|
|
781
957
|
|
|
782
958
|
def cancel
|
|
959
|
+
@swarm_generation = SecureRandom.uuid
|
|
960
|
+
if @mission
|
|
961
|
+
cancel_mission
|
|
962
|
+
@swarm&.cancel_all
|
|
963
|
+
return
|
|
964
|
+
end
|
|
965
|
+
|
|
783
966
|
if busy?
|
|
784
967
|
first = !@cancelling
|
|
785
968
|
@cancelling = true
|
|
786
969
|
@control&.stop
|
|
787
970
|
@swarm&.cancel_all
|
|
788
|
-
@tool_input
|
|
971
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
789
972
|
add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
|
|
790
973
|
else
|
|
791
974
|
@editor.replace('')
|
|
@@ -793,11 +976,12 @@ module PWN
|
|
|
793
976
|
end
|
|
794
977
|
|
|
795
978
|
def stop_worker
|
|
979
|
+
cancel_mission if @mission
|
|
796
980
|
return unless @worker&.alive? || @swarm&.busy?
|
|
797
981
|
|
|
798
982
|
@control&.stop
|
|
799
983
|
@swarm&.cancel_all
|
|
800
|
-
@tool_input
|
|
984
|
+
@tool_input&.close unless @tool_input&.closed?
|
|
801
985
|
@worker&.join
|
|
802
986
|
@swarm&.close
|
|
803
987
|
ensure
|
|
@@ -914,7 +1098,7 @@ module PWN
|
|
|
914
1098
|
|
|
915
1099
|
return add(:warning, 'Workspace busy: wait or cancel the ordinary request before launching a swarm writer.') if @running && line.match?(%r{\A/swarm\s+(?:mission|dm|broadcast|debate)\b})
|
|
916
1100
|
|
|
917
|
-
result = @swarm.execute(mission_options.merge(line: line, on_event:
|
|
1101
|
+
result = @swarm.execute(mission_options.merge(line: line, on_event: swarm_event_callback, usage_observer: @usage.method(:record)))
|
|
918
1102
|
text = result[:job_id] ? "Swarm job #{result[:job_id]} queued (#{result[:state]})." : (result[:usage] || REPL.pwn_ai_result_text(value: result))
|
|
919
1103
|
add(:command, text)
|
|
920
1104
|
end
|
|
@@ -2410,13 +2594,9 @@ module PWN
|
|
|
2410
2594
|
|
|
2411
2595
|
view = @operator_view
|
|
2412
2596
|
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
|
|
2414
2597
|
view.delete(:confirm)
|
|
2415
2598
|
@operator_view = nil
|
|
2416
|
-
start_request('/sessions cleanup (approved preview)', command: true)
|
|
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
|
|
2599
|
+
start_request('/sessions cleanup (approved preview)', command: true, cleanup: approved)
|
|
2420
2600
|
end
|
|
2421
2601
|
|
|
2422
2602
|
def operator_viewport(page)
|
|
@@ -2581,7 +2761,7 @@ module PWN
|
|
|
2581
2761
|
@workspace[:notice] = 'Workspace busy: wait or cancel the ordinary request first.'
|
|
2582
2762
|
return
|
|
2583
2763
|
end
|
|
2584
|
-
options = mission_options.merge(line: Shellwords.join(['/swarm'] + args), on_event:
|
|
2764
|
+
options = mission_options.merge(line: Shellwords.join(['/swarm'] + args), on_event: swarm_event_callback, usage_observer: @usage.method(:record))
|
|
2585
2765
|
options[:solve_request] = args.last if args.first == 'mission'
|
|
2586
2766
|
result = @swarm.execute(options)
|
|
2587
2767
|
@workspace[:notice] = result[:error] || "#{args.first}: accepted#{" · job #{result[:job_id]}" if result[:job_id]}"
|