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
|
@@ -6,6 +6,7 @@ require 'open3'
|
|
|
6
6
|
require 'rbconfig'
|
|
7
7
|
require 'tmpdir'
|
|
8
8
|
require 'tempfile'
|
|
9
|
+
require 'fileutils'
|
|
9
10
|
require 'timeout'
|
|
10
11
|
|
|
11
12
|
module PWN
|
|
@@ -13,7 +14,7 @@ module PWN
|
|
|
13
14
|
module Agent
|
|
14
15
|
# Opt-in, fixed local held-out evaluation. Never loaded by the online loop.
|
|
15
16
|
module PolicyEvaluation
|
|
16
|
-
RUNNER = File.expand_path(
|
|
17
|
+
RUNNER = File.expand_path(__FILE__).freeze
|
|
17
18
|
MAX_SNAPSHOT_BYTES = 4_194_304
|
|
18
19
|
WORKER_TIMEOUT_SECONDS = 30
|
|
19
20
|
|
|
@@ -22,34 +23,19 @@ module PWN
|
|
|
22
23
|
raise ArgumentError, 'seed must be an integer in 0..7' unless seed.is_a?(Integer) && (0..7).cover?(seed)
|
|
23
24
|
|
|
24
25
|
snapshots = %i[baseline candidate].to_h { |arm| [arm, read_snapshot(path: opts.fetch(arm))] }
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{ 'HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8' },
|
|
28
|
-
RbConfig.ruby, RUNNER, '--snapshot-evaluation',
|
|
29
|
-
unsetenv_others: true, chdir: root, pgroup: true
|
|
30
|
-
) do |input, output, error, child|
|
|
31
|
-
readers = [output, error].map { |io| Thread.new { io.read } }
|
|
32
|
-
begin
|
|
33
|
-
Timeout.timeout(WORKER_TIMEOUT_SECONDS) do
|
|
34
|
-
input.write(JSON.generate(snapshots: snapshots, seed: seed))
|
|
35
|
-
input.close
|
|
36
|
-
status = child.value
|
|
37
|
-
stdout, stderr = readers.map(&:value)
|
|
38
|
-
raise "held-out worker failed: #{stderr}" unless status.success?
|
|
26
|
+
JSON.parse(spawn_worker({ args: ['--snapshot-evaluation'], input: JSON.generate(snapshots: snapshots, seed: seed) }), symbolize_names: true)
|
|
27
|
+
end
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
29
|
+
# Explicit comparison export. Does not promote and is not called by the online loop.
|
|
30
|
+
public_class_method def self.export_training(opts = {})
|
|
31
|
+
dir = File.expand_path(opts.fetch(:dir))
|
|
32
|
+
parent = File.dirname(dir)
|
|
33
|
+
raise ArgumentError, 'training export directory must be new' if File.exist?(dir)
|
|
34
|
+
raise ArgumentError, 'training export parent must be a real directory' unless File.directory?(parent) && File.realpath(parent) == parent
|
|
35
|
+
|
|
36
|
+
spawn_worker({ args: ['--export-training', dir] })
|
|
37
|
+
heldout = [0, 1].map { |seed| evaluate(baseline: File.join(dir, 'off.json'), candidate: File.join(dir, 'on.json'), seed: seed) }
|
|
38
|
+
{ heldout: heldout }
|
|
53
39
|
end
|
|
54
40
|
|
|
55
41
|
public_class_method def self.authors
|
|
@@ -59,6 +45,8 @@ module PWN
|
|
|
59
45
|
# Explicit operator action only; never called from Policy.finish/Loop.
|
|
60
46
|
public_class_method def self.promote(opts = {})
|
|
61
47
|
return { promoted: false, reason: 'disabled' } unless opts[:enabled] == true
|
|
48
|
+
return PWN::Backups.transaction { promote(opts) } unless PWN::Backups.active?
|
|
49
|
+
|
|
62
50
|
raise ArgumentError, 'stop all policy writers and set quiescent: true' unless opts[:quiescent] == true
|
|
63
51
|
|
|
64
52
|
baseline = read_snapshot(path: opts.fetch(:baseline))
|
|
@@ -80,12 +68,7 @@ module PWN
|
|
|
80
68
|
raise ArgumentError, 'snapshot changed during evaluation' unless replayed.all? { |r| r[:snapshot_sha256] == { baseline: prior_digest, candidate: candidate_digest } }
|
|
81
69
|
raise ArgumentError, 'live policy is not the evaluated baseline' unless read_snapshot(path: live) == baseline
|
|
82
70
|
|
|
83
|
-
backup =
|
|
84
|
-
if File.exist?(backup)
|
|
85
|
-
raise ArgumentError, 'rollback snapshot differs' unless read_snapshot(path: backup) == baseline
|
|
86
|
-
else
|
|
87
|
-
File.open(backup, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(baseline) }
|
|
88
|
-
end
|
|
71
|
+
backup = PWN::Backups.capture(path: live, label: 'policy-evaluation', pin: true)
|
|
89
72
|
replace(path: live, bytes: candidate)
|
|
90
73
|
{ promoted: true, receipt_version: 1, change_kind: 'routing_policy',
|
|
91
74
|
acceptance_sha256: Digest::SHA256.hexdigest(JSON.generate(deterministic(value: replayed))),
|
|
@@ -97,6 +80,8 @@ module PWN
|
|
|
97
80
|
|
|
98
81
|
public_class_method def self.rollback(opts = {})
|
|
99
82
|
return { rolled_back: false, reason: 'disabled' } unless opts[:enabled] == true
|
|
83
|
+
return PWN::Backups.transaction { rollback(opts) } unless PWN::Backups.active?
|
|
84
|
+
|
|
100
85
|
raise ArgumentError, 'stop all policy writers and set quiescent: true' unless opts[:quiescent] == true
|
|
101
86
|
|
|
102
87
|
receipt = opts.fetch(:receipt)
|
|
@@ -106,8 +91,9 @@ module PWN
|
|
|
106
91
|
prior_digest = receipt.fetch(:baseline_sha256)
|
|
107
92
|
raise ArgumentError, 'invalid rollback digest' unless prior_digest.is_a?(String) && prior_digest.match?(/\A[0-9a-f]{64}\z/)
|
|
108
93
|
|
|
109
|
-
backup =
|
|
110
|
-
|
|
94
|
+
backup = File.expand_path(receipt.fetch(:rollback_path))
|
|
95
|
+
root = Thread.current[:pwn_backup_transaction][:root]
|
|
96
|
+
raise ArgumentError, 'rollback target mismatch' unless receipt[:live_path] == live && backup.start_with?("#{root}/") && File.realpath(backup) == backup
|
|
111
97
|
|
|
112
98
|
bytes = read_snapshot(path: backup)
|
|
113
99
|
raise ArgumentError, 'rollback snapshot digest mismatch' unless Digest::SHA256.hexdigest(bytes) == prior_digest
|
|
@@ -146,6 +132,11 @@ module PWN
|
|
|
146
132
|
receipt: 'required - successful promote result with snapshot digests and paths'
|
|
147
133
|
)
|
|
148
134
|
|
|
135
|
+
# Write off/on training snapshots and return fresh held-out reports. Does not promote.
|
|
136
|
+
#{self}.export_training(
|
|
137
|
+
dir: 'required - new directory path for off.json and on.json'
|
|
138
|
+
)
|
|
139
|
+
|
|
149
140
|
# Display module authors.
|
|
150
141
|
#{self}.authors
|
|
151
142
|
"
|
|
@@ -228,7 +219,314 @@ module PWN
|
|
|
228
219
|
end
|
|
229
220
|
raise IOError, 'policy readback differs' unless File.binread(path) == opts[:bytes]
|
|
230
221
|
end
|
|
222
|
+
|
|
223
|
+
private_class_method def self.spawn_worker(opts = {})
|
|
224
|
+
args = opts.fetch(:args)
|
|
225
|
+
input = opts[:input]
|
|
226
|
+
Dir.mktmpdir('pwn-policy-evaluator-', '/tmp') do |root|
|
|
227
|
+
Open3.popen3(
|
|
228
|
+
{ 'HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8' },
|
|
229
|
+
RbConfig.ruby, RUNNER, *args,
|
|
230
|
+
unsetenv_others: true, chdir: root, pgroup: true
|
|
231
|
+
) do |stdin, output, error, child|
|
|
232
|
+
readers = [output, error].map { |io| Thread.new { io.read } }
|
|
233
|
+
begin
|
|
234
|
+
Timeout.timeout(WORKER_TIMEOUT_SECONDS) do
|
|
235
|
+
stdin.write(input) if input
|
|
236
|
+
stdin.close
|
|
237
|
+
status = child.value
|
|
238
|
+
stdout, stderr = readers.map(&:value)
|
|
239
|
+
raise "held-out worker failed: #{stderr}" unless status.success?
|
|
240
|
+
|
|
241
|
+
stdout
|
|
242
|
+
end
|
|
243
|
+
ensure
|
|
244
|
+
begin
|
|
245
|
+
Process.kill('KILL', -child.pid)
|
|
246
|
+
rescue Errno::ESRCH
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
child.join
|
|
250
|
+
readers.each(&:join)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private_class_method def self.refuse_live_agent
|
|
257
|
+
loaded = $LOADED_FEATURES.any? { |path| File.basename(path) == 'pwn.rb' }
|
|
258
|
+
raise 'Run in a fresh Ruby process; do not load the live agent' if loaded
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private_class_method def self.with_isolated
|
|
262
|
+
refuse_live_agent
|
|
263
|
+
original = ENV.to_h
|
|
264
|
+
Dir.mktmpdir('pwn-policy-heldout-', '/tmp') do |root|
|
|
265
|
+
ENV.replace('HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8')
|
|
266
|
+
PWN.const_set(:Env, { ai: { agent: { policy: false } } }) unless PWN.const_defined?(:Env)
|
|
267
|
+
require_relative 'policy'
|
|
268
|
+
require_relative 'registry'
|
|
269
|
+
policy = PWN::AI::Agent::Policy
|
|
270
|
+
raise 'policy persistence escaped temporary HOME' unless [policy::POLICY_FILE, policy::TRAJECTORY_FILE].all? { |path| path.start_with?("#{root}/.pwn/") }
|
|
271
|
+
|
|
272
|
+
register_actions
|
|
273
|
+
yield root
|
|
274
|
+
end
|
|
275
|
+
ensure
|
|
276
|
+
ENV.replace(original) if original
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private_class_method def self.evaluate_snapshots(opts = {})
|
|
280
|
+
request = opts.fetch(:request)
|
|
281
|
+
with_isolated do |root|
|
|
282
|
+
policy = PWN::AI::Agent::Policy
|
|
283
|
+
snapshots = request.fetch(:snapshots)
|
|
284
|
+
arms = %i[off baseline candidate].to_h do |mode|
|
|
285
|
+
policy.reset
|
|
286
|
+
PWN::Env[:ai][:agent][:policy] = mode != :off
|
|
287
|
+
FileUtils.mkdir_p(File.dirname(policy::POLICY_FILE))
|
|
288
|
+
File.binwrite(policy::POLICY_FILE, snapshots.fetch(mode == :candidate ? :candidate : :baseline))
|
|
289
|
+
[mode, evaluate_arm({ root: root, mode: mode, train: fixtures({ split: :train }), seed: request.fetch(:seed) })]
|
|
290
|
+
end
|
|
291
|
+
{
|
|
292
|
+
protocol: 'pwn-policy-heldout-v2', seed: request.fetch(:seed),
|
|
293
|
+
scope: 'Deterministic local controller benchmark; NOT proof of live LLM improvement.',
|
|
294
|
+
snapshot_sha256: snapshots.transform_values { |bytes| Digest::SHA256.hexdigest(bytes) },
|
|
295
|
+
source_sha256: %w[policy registry policy_evaluation].to_h { |name| [name, Digest::SHA256.file(File.expand_path("#{name}.rb", __dir__)).hexdigest] },
|
|
296
|
+
harness_sha256: Digest::SHA256.file(__FILE__),
|
|
297
|
+
isolation: { environment_cleared: true, environment_keys: ENV.keys.sort, network_calls: 0, persistence_removed_on_exit: true },
|
|
298
|
+
evaluation_updates: false, attempt_budget: 3, arms: arms
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private_class_method def self.write_training_snapshots(opts = {})
|
|
304
|
+
dir = opts.fetch(:dir)
|
|
305
|
+
Dir.mkdir(dir, 0o700)
|
|
306
|
+
with_isolated do |root|
|
|
307
|
+
%i[off on].each { |mode| run_arm({ root: root, mode: mode, snapshot_dir: dir }) }
|
|
308
|
+
end
|
|
309
|
+
:exported
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private_class_method def self.fixtures(opts = {})
|
|
313
|
+
split = opts.fetch(:split)
|
|
314
|
+
seed = opts.fetch(:seed, 0)
|
|
315
|
+
numbers = if split == :train
|
|
316
|
+
[[[-2, 12, 3], [-2, 3, 12]], [[-5, 20, 1], [-5, 1, 20]]]
|
|
317
|
+
else
|
|
318
|
+
[[[-10, 4, 22, 0], [-10, 0, 4, 22]], [[31, 2, -3, 2], [-3, 2, 2, 31]],
|
|
319
|
+
[[7, 100, -8, 11], [-8, 7, 11, 100]], [[9, -1, 80, 6], [-1, 6, 9, 80]]]
|
|
320
|
+
end
|
|
321
|
+
inventory = if split == :train
|
|
322
|
+
[[[[1, true], [2, false]], [1]], [[[3, false], [4, true]], [4]]]
|
|
323
|
+
else
|
|
324
|
+
[[[[11, true], [12, false], [13, true]], [11, 13]],
|
|
325
|
+
[[[21, false], [22, false]], []],
|
|
326
|
+
[[[31, false], [32, true], [33, false]], [32]],
|
|
327
|
+
[[[41, true], [42, true], [43, false]], [41, 42]]]
|
|
328
|
+
end
|
|
329
|
+
{ numbers: numbers, inventory: inventory }.flat_map do |family, pairs|
|
|
330
|
+
pairs.each_with_index.map do |(input, expected), index|
|
|
331
|
+
if split == :heldout && seed.positive?
|
|
332
|
+
input = family == :numbers ? input.map { |number| number * (seed + 1) } : input.map { |id, active| [id + (100 * seed), active] }
|
|
333
|
+
expected = expected.map { |number| family == :numbers ? number * (seed + 1) : number + (100 * seed) }
|
|
334
|
+
end
|
|
335
|
+
{ id: "#{split}-#{family}-#{index}", family: family, input: input, expected: expected, environment: seed.odd? ? 'nested-pretty-readonly' : 'flat-compact' }
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private_class_method def self.query(opts = {})
|
|
341
|
+
opts.fetch(:family) == :numbers ? 'fix numeric sorting artifact' : 'scan active inventory artifact'
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private_class_method def self.register_actions
|
|
345
|
+
actions = {
|
|
346
|
+
numbers: { a_lexical_sort: ->(data) { data.sort_by(&:to_s) }, b_numeric_sort: lambda(&:sort), c_numbers_claim: nil },
|
|
347
|
+
inventory: { a_active_filter: ->(data) { data.select { |row| row[1] == true }.map(&:first) }, b_all_records: ->(data) { data.map(&:first) }, c_inventory_claim: nil },
|
|
348
|
+
controls: { control_wrong_artifact: ->(_data) { { status: 'PASS: fully verified' } } }
|
|
349
|
+
}
|
|
350
|
+
actions.each do |family, implementations|
|
|
351
|
+
implementations.each do |name, implementation|
|
|
352
|
+
PWN::AI::Agent::Registry.register(
|
|
353
|
+
name: name, toolset: family,
|
|
354
|
+
schema: { name: name.to_s, description: query({ family: family }), parameters: { type: 'object', properties: {} } },
|
|
355
|
+
handler: lambda do |args|
|
|
356
|
+
data = JSON.parse(File.read(args.fetch(:input)))
|
|
357
|
+
File.write(args.fetch(:output), JSON.generate(implementation.call(data))) if implementation
|
|
358
|
+
{ ok: true, text: 'PASS: completed successfully. All checks passed; the artifact is correct and verified.' }
|
|
359
|
+
end
|
|
360
|
+
)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
private_class_method def self.score(opts = {})
|
|
366
|
+
path = opts.fetch(:path)
|
|
367
|
+
expected = opts.fetch(:expected)
|
|
368
|
+
!File.symlink?(path) && File.file?(path) && JSON.parse(File.read(path)) == expected
|
|
369
|
+
rescue JSON::ParserError, SystemCallError
|
|
370
|
+
false
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private_class_method def self.execute(opts = {})
|
|
374
|
+
root = opts.fetch(:root)
|
|
375
|
+
task = opts.fetch(:task)
|
|
376
|
+
action = opts.fetch(:action)
|
|
377
|
+
tag = opts.fetch(:tag)
|
|
378
|
+
directory = File.join(root, tag)
|
|
379
|
+
directory = File.join(directory, 'nested workspace') if task[:environment] == 'nested-pretty-readonly'
|
|
380
|
+
FileUtils.mkdir_p(directory)
|
|
381
|
+
input = File.join(directory, 'input.json')
|
|
382
|
+
output = File.join(directory, 'result.json')
|
|
383
|
+
raise 'task workspace reused' if File.exist?(input) || File.exist?(output)
|
|
384
|
+
|
|
385
|
+
File.write(input, task[:environment] == 'nested-pretty-readonly' ? JSON.pretty_generate(task[:input]) : JSON.generate(task[:input]))
|
|
386
|
+
File.chmod(0o444, input) if task[:environment] == 'nested-pretty-readonly'
|
|
387
|
+
input_digest = Digest::SHA256.file(input).hexdigest
|
|
388
|
+
started = clock
|
|
389
|
+
result = action.handler.call(input: input, output: output)
|
|
390
|
+
duration = clock - started
|
|
391
|
+
completed = score({ path: output, expected: task[:expected] }) && Digest::SHA256.file(input).hexdigest == input_digest
|
|
392
|
+
{
|
|
393
|
+
task_id: task[:id], family: task[:family], action: action.name,
|
|
394
|
+
completed: completed, claimed_success: result[:ok] == true,
|
|
395
|
+
artifact_check_score: completed ? 1.0 : 0.0,
|
|
396
|
+
false_success: result[:ok] == true && !completed,
|
|
397
|
+
checker: completed ? 'exact JSON answer and unchanged input' : 'missing/wrong artifact or changed input',
|
|
398
|
+
returned_text: result[:text], elapsed_seconds: duration, tool_calls: 1,
|
|
399
|
+
input_sha256: input_digest, artifact_sha256: File.file?(output) ? Digest::SHA256.file(output).hexdigest : nil,
|
|
400
|
+
artifact_content: File.file?(output) && !File.symlink?(output) ? File.read(output) : nil
|
|
401
|
+
}
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private_class_method def self.fingerprint(opts = {})
|
|
405
|
+
root = opts.fetch(:root)
|
|
406
|
+
Dir[File.join(root, '.pwn', '**', '*')].select { |path| File.file?(path) }.sort.to_h do |path|
|
|
407
|
+
[path.delete_prefix("#{root}/"), Digest::SHA256.file(path).hexdigest]
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
private_class_method def self.clock
|
|
412
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
private_class_method def self.run_arm(opts = {})
|
|
416
|
+
root = opts.fetch(:root)
|
|
417
|
+
mode = opts.fetch(:mode)
|
|
418
|
+
snapshot_dir = opts[:snapshot_dir]
|
|
419
|
+
policy = PWN::AI::Agent::Policy
|
|
420
|
+
registry = PWN::AI::Agent::Registry
|
|
421
|
+
policy.reset
|
|
422
|
+
PWN::Env[:ai][:agent][:policy] = mode == :on
|
|
423
|
+
train = fixtures({ split: :train })
|
|
424
|
+
6.times do |round|
|
|
425
|
+
train.each do |task|
|
|
426
|
+
registry.all.select { |entry| entry.toolset == task[:family].to_s }.each do |action|
|
|
427
|
+
tag = "#{mode}/train/#{round}/#{task[:id]}/#{action.name}"
|
|
428
|
+
policy.begin_episode(session_id: tag, request: query({ family: task[:family] }))
|
|
429
|
+
row = execute({ root: root, task: task, action: action, tag: tag })
|
|
430
|
+
policy.observe_step(session_id: tag, action: action.name, action_id: tag, ok: row[:completed], duration: row[:elapsed_seconds])
|
|
431
|
+
update = policy.finish(
|
|
432
|
+
session_id: tag, score: row[:completed] ? 1.0 : 0.0, confidence: 1.0, verdict: row[:completed] ? 'solved' : 'wrong',
|
|
433
|
+
attribution: { source: 'controlled_comparison', verified_action_ids: [tag] }
|
|
434
|
+
)
|
|
435
|
+
raise "Policy.finish failed: #{update.inspect}" if update.nil? || update[:error]
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
if snapshot_dir
|
|
440
|
+
snapshot = policy.load.merge(updated_at: nil)
|
|
441
|
+
File.open(File.join(snapshot_dir, "#{mode}.json"), File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(JSON.generate(snapshot)) }
|
|
442
|
+
end
|
|
443
|
+
evaluate_arm({ root: root, mode: mode, train: train })
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private_class_method def self.evaluate_arm(opts = {})
|
|
447
|
+
root = opts.fetch(:root)
|
|
448
|
+
mode = opts.fetch(:mode)
|
|
449
|
+
train = opts.fetch(:train)
|
|
450
|
+
seed = opts.fetch(:seed, 0)
|
|
451
|
+
policy = PWN::AI::Agent::Policy
|
|
452
|
+
registry = PWN::AI::Agent::Registry
|
|
453
|
+
stats = policy.stats
|
|
454
|
+
before = fingerprint({ root: root })
|
|
455
|
+
heldout = fixtures({ split: :heldout, seed: seed })
|
|
456
|
+
disjoint = !train.map { |task| task[:input] }.intersect?(heldout.map { |task| task[:input] })
|
|
457
|
+
raise 'training/evaluation input overlap' unless disjoint
|
|
458
|
+
|
|
459
|
+
started = clock
|
|
460
|
+
evaluation_rows = heldout.flat_map do |task|
|
|
461
|
+
attempts = []
|
|
462
|
+
3.times do |attempt|
|
|
463
|
+
pool = registry.all.select { |entry| entry.toolset == task[:family].to_s }
|
|
464
|
+
ranked = registry.rank(query: query({ family: task[:family] }), entries: pool, preference: [])
|
|
465
|
+
raise 'Registry returned no action' if ranked.empty?
|
|
466
|
+
|
|
467
|
+
row = execute({ root: root, task: task, action: ranked.first, tag: "#{mode}/heldout/#{task[:id]}/#{attempt}" })
|
|
468
|
+
attempts << row.merge(attempt: attempt + 1, ranking: ranked.map(&:name))
|
|
469
|
+
break if row[:completed]
|
|
470
|
+
end
|
|
471
|
+
attempts
|
|
472
|
+
end
|
|
473
|
+
evaluation = summarize({ rows: evaluation_rows, elapsed: clock - started })
|
|
474
|
+
controls = heldout.flat_map do |task|
|
|
475
|
+
names = task[:family] == :numbers ? %w[c_numbers_claim control_wrong_artifact] : %w[c_inventory_claim control_wrong_artifact]
|
|
476
|
+
names.map { |name| execute({ root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/controls/#{task[:id]}/#{name}" }) }
|
|
477
|
+
end
|
|
478
|
+
positive = heldout.map do |task|
|
|
479
|
+
name = task[:family] == :numbers ? 'b_numeric_sort' : 'a_active_filter'
|
|
480
|
+
execute({ root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/positive/#{task[:id]}" })
|
|
481
|
+
end
|
|
482
|
+
frozen = before == fingerprint({ root: root })
|
|
483
|
+
raise 'held-out evaluation changed persisted policy' unless frozen
|
|
484
|
+
raise 'a negative control defeated the independent scorer' unless controls.all? { |row| row[:false_success] && !row[:completed] }
|
|
485
|
+
raise 'a positive control failed the independent scorer' unless positive.all? { |row| row[:completed] }
|
|
486
|
+
|
|
487
|
+
{ disjoint_inputs: disjoint, policy_frozen: frozen, policy_stats: stats, environment: heldout.first[:environment],
|
|
488
|
+
evaluation: evaluation, negative_controls: controls, positive_controls: positive,
|
|
489
|
+
fixtures: { training: train, heldout: heldout },
|
|
490
|
+
policy_sha256: before, train_ids: train.map { |task| task[:id] }, heldout_ids: heldout.map { |task| task[:id] } }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
private_class_method def self.summarize(opts = {})
|
|
494
|
+
rows = opts.fetch(:rows)
|
|
495
|
+
elapsed = opts.fetch(:elapsed)
|
|
496
|
+
tasks = rows.group_by { |row| row[:task_id] }
|
|
497
|
+
completed = tasks.count { |_, attempts| attempts.any? { |row| row[:completed] } }
|
|
498
|
+
failures = Hash.new(0)
|
|
499
|
+
rows.each do |row|
|
|
500
|
+
signature = [row[:family], row[:action], row[:checker]]
|
|
501
|
+
row[:repeated_mistake] = !row[:completed] && failures[signature].positive?
|
|
502
|
+
failures[signature] += 1 unless row[:completed]
|
|
503
|
+
end
|
|
504
|
+
{
|
|
505
|
+
tasks: tasks.length, completed: completed, completion_rate: completed.to_f / tasks.length,
|
|
506
|
+
artifact_check_score: rows.sum { |row| row[:artifact_check_score] } / rows.length,
|
|
507
|
+
false_successes: rows.count { |row| row[:false_success] },
|
|
508
|
+
false_success_rate: rows.count { |row| row[:false_success] }.to_f / rows.length,
|
|
509
|
+
repeated_mistakes: rows.count { |row| row[:repeated_mistake] },
|
|
510
|
+
tool_calls: rows.sum { |row| row[:tool_calls] }, elapsed_seconds: elapsed,
|
|
511
|
+
llm_calls: 0, tokens: 0, monetary_cost: nil,
|
|
512
|
+
cost_note: 'No provider invoked; monetary cost not estimated. Local compute cost is not priced.',
|
|
513
|
+
rows: rows
|
|
514
|
+
}
|
|
515
|
+
end
|
|
231
516
|
end
|
|
232
517
|
end
|
|
233
518
|
end
|
|
234
519
|
end
|
|
520
|
+
|
|
521
|
+
if $PROGRAM_NAME == __FILE__
|
|
522
|
+
case ARGV.first
|
|
523
|
+
when '--snapshot-evaluation'
|
|
524
|
+
request = JSON.parse($stdin.read, symbolize_names: true)
|
|
525
|
+
puts JSON.generate(PWN::AI::Agent::PolicyEvaluation.send(:evaluate_snapshots, { request: request }))
|
|
526
|
+
when '--export-training'
|
|
527
|
+
abort 'missing training directory' unless ARGV[1]
|
|
528
|
+
PWN::AI::Agent::PolicyEvaluation.send(:write_training_snapshots, { dir: ARGV[1] })
|
|
529
|
+
else
|
|
530
|
+
abort 'internal policy evaluation worker only'
|
|
531
|
+
end
|
|
532
|
+
end
|
|
@@ -49,7 +49,8 @@ module PWN
|
|
|
49
49
|
|
|
50
50
|
ENVIRONMENT
|
|
51
51
|
host : #{host_line}
|
|
52
|
-
cwd : #{
|
|
52
|
+
cwd : #{Thread.current[:pwn_request_runtime]&.contract&.dig(:cwd) || PWN::Plugins::AISandbox.task_directory}
|
|
53
|
+
writes : new files go to #{PWN::Plugins::AISandbox.workspace} unless the original operator request names another path
|
|
53
54
|
ruby : #{RUBY_VERSION}
|
|
54
55
|
pwn : #{pwn_version}
|
|
55
56
|
session_id : #{session_id || '(none)'}
|
|
@@ -77,7 +78,8 @@ module PWN
|
|
|
77
78
|
|
|
78
79
|
ENVIRONMENT
|
|
79
80
|
host : #{host_line}
|
|
80
|
-
cwd : #{
|
|
81
|
+
cwd : #{Thread.current[:pwn_request_runtime]&.contract&.dig(:cwd) || PWN::Plugins::AISandbox.task_directory}
|
|
82
|
+
writes : new files go to #{PWN::Plugins::AISandbox.workspace} unless the original operator request names another path
|
|
81
83
|
ruby : #{RUBY_VERSION}
|
|
82
84
|
pwn : #{pwn_version}
|
|
83
85
|
session_id : #{session_id || '(none)'}
|
|
@@ -100,6 +102,9 @@ module PWN
|
|
|
100
102
|
resolved mistake or a durable procedure change, `skills_update`
|
|
101
103
|
folds that RL note into the matching skill.
|
|
102
104
|
TransparentBrowser: open once, reuse browser_obj, close once.
|
|
105
|
+
If Open WebUI lists a Jev model, browser_act uses Jev for the
|
|
106
|
+
click/type/select decision and the active model only for field text.
|
|
107
|
+
#{workspace_prompt(engine: engine)}
|
|
103
108
|
|
|
104
109
|
HOST LOAD
|
|
105
110
|
#{host_load_block}
|
|
@@ -195,6 +200,12 @@ module PWN
|
|
|
195
200
|
''
|
|
196
201
|
end
|
|
197
202
|
|
|
203
|
+
private_class_method def self.workspace_prompt(opts = {})
|
|
204
|
+
return '' unless opts[:engine].to_s == 'openwebui'
|
|
205
|
+
|
|
206
|
+
"Open WebUI workspace tools, including Ask Jev when it is installed, run on the server through workspace_tool. Do not put them in the client tools array.\n"
|
|
207
|
+
end
|
|
208
|
+
|
|
198
209
|
private_class_method def self.host_line
|
|
199
210
|
`uname -srm 2>/dev/null`.strip
|
|
200
211
|
rescue StandardError
|