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
data/scripts/benchmark_policy.rb
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'tmpdir'
|
|
5
|
-
require 'fileutils'
|
|
6
|
-
require 'digest'
|
|
7
|
-
require 'optparse'
|
|
8
|
-
|
|
9
|
-
# Standalone experiment, deliberately outside the production module tree.
|
|
10
|
-
module PolicyBenchmark
|
|
11
|
-
def self.score(path:, expected:)
|
|
12
|
-
!File.symlink?(path) && File.file?(path) && JSON.parse(File.read(path)) == expected
|
|
13
|
-
rescue JSON::ParserError, SystemCallError
|
|
14
|
-
false
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.self_check
|
|
18
|
-
Dir.mktmpdir('pwn-policy-check-', '/tmp') do |root|
|
|
19
|
-
path = File.join(root, 'result.json')
|
|
20
|
-
File.write(path, JSON.generate([2, 10]))
|
|
21
|
-
raise 'checker rejected a correct artifact' unless score(path: path, expected: [2, 10])
|
|
22
|
-
|
|
23
|
-
File.write(path, 'PASS: all checks passed; result.json was verified successfully.')
|
|
24
|
-
raise 'checker accepted convincing prose' if score(path: path, expected: [2, 10])
|
|
25
|
-
|
|
26
|
-
File.write(path, JSON.generate([10, 2]))
|
|
27
|
-
raise 'checker accepted wrong artifact' if score(path: path, expected: [2, 10])
|
|
28
|
-
|
|
29
|
-
File.unlink(path)
|
|
30
|
-
raise 'checker accepted missing artifact' if score(path: path, expected: [2, 10])
|
|
31
|
-
|
|
32
|
-
other = File.join(root, 'other.json')
|
|
33
|
-
File.write(other, JSON.generate([2, 10]))
|
|
34
|
-
File.symlink(other, path)
|
|
35
|
-
raise 'checker accepted a symlink instead of a new artifact' if score(path: path, expected: [2, 10])
|
|
36
|
-
end
|
|
37
|
-
report = Dir.mktmpdir('pwn-policy-snapshot-check-', '/tmp') do |snapshots|
|
|
38
|
-
measured = run(snapshot_dir: snapshots)
|
|
39
|
-
require_relative '../lib/pwn/ai/agent/policy_evaluation'
|
|
40
|
-
evaluator = PWN::AI::Agent::PolicyEvaluation
|
|
41
|
-
[0, 1].each do |seed|
|
|
42
|
-
heldout = evaluator.evaluate(baseline: File.join(snapshots, 'off.json'), candidate: File.join(snapshots, 'on.json'), seed: seed)
|
|
43
|
-
raise 'snapshot protocol missing' unless heldout[:protocol] == 'pwn-policy-heldout-v2'
|
|
44
|
-
raise 'snapshot arms missing' unless heldout[:arms].keys == %i[off baseline candidate]
|
|
45
|
-
|
|
46
|
-
heldout[:arms].each_value do |arm|
|
|
47
|
-
raise 'snapshot evaluation unfrozen or contaminated' unless arm[:policy_frozen] && arm[:disjoint_inputs] && !arm.key?(:training)
|
|
48
|
-
raise 'snapshot checks missing' unless arm[:evaluation][:tasks] == 8 && arm[:negative_controls].length == 16 && arm[:positive_controls].length == 8
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
puts 'Snapshot self-checks passed'
|
|
52
|
-
measured
|
|
53
|
-
end
|
|
54
|
-
raise 'benchmark did not run both learning arms' unless report[:arms]&.keys == %i[off on]
|
|
55
|
-
|
|
56
|
-
report[:arms].each do |mode, arm|
|
|
57
|
-
raise 'missing held-out tasks' unless arm[:evaluation][:tasks] == 8
|
|
58
|
-
raise 'training contaminated held-out data' unless arm[:disjoint_inputs]
|
|
59
|
-
raise 'evaluation mutated the learned policy' unless arm[:policy_frozen]
|
|
60
|
-
raise 'negative controls trusted PASS text' unless arm[:negative_controls].length == 16 && arm[:negative_controls].all? { |row| row[:false_success] && !row[:completed] }
|
|
61
|
-
raise 'positive controls failed' unless arm[:positive_controls]&.length == 8 && arm[:positive_controls].all? { |row| row[:completed] }
|
|
62
|
-
raise 'training has no real executed actions' unless arm[:training][:tool_calls] == 72
|
|
63
|
-
raise 'off arm learned' if mode == :off && arm[:policy_stats][:n_updates].positive?
|
|
64
|
-
raise 'on arm did not learn' if mode == :on && !arm[:policy_stats][:n_updates].positive?
|
|
65
|
-
raise 'provider usage was invented' unless arm[:evaluation][:llm_calls].zero? && arm[:evaluation][:monetary_cost].nil?
|
|
66
|
-
end
|
|
67
|
-
puts 'Self-checks passed'
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def self.run(snapshot_dir: nil)
|
|
71
|
-
raise 'Run in a fresh Ruby process; do not load the live agent' if defined?(PWN)
|
|
72
|
-
|
|
73
|
-
original_env = ENV.to_h
|
|
74
|
-
started = clock
|
|
75
|
-
Dir.mktmpdir('pwn-policy-benchmark-', '/tmp') do |root|
|
|
76
|
-
# Set HOME BEFORE requiring code with Dir.home-based constants. Do not
|
|
77
|
-
# load pwn.rb, user config, tools, network libraries, or provider clients.
|
|
78
|
-
ENV.replace('HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8')
|
|
79
|
-
Object.const_set(:PWN, Module.new)
|
|
80
|
-
PWN.const_set(:Env, { ai: { agent: { policy: false } } })
|
|
81
|
-
require_relative '../lib/pwn/ai/agent/policy'
|
|
82
|
-
require_relative '../lib/pwn/ai/agent/registry'
|
|
83
|
-
policy = PWN::AI::Agent::Policy
|
|
84
|
-
raise 'policy persistence escaped temporary HOME' unless [policy::POLICY_FILE, policy::TRAJECTORY_FILE].all? { |path| path.start_with?("#{root}/.pwn/") }
|
|
85
|
-
|
|
86
|
-
register_actions
|
|
87
|
-
arms = %i[off on].to_h { |mode| [mode, run_arm(root: root, mode: mode, snapshot_dir: snapshot_dir)] }
|
|
88
|
-
{
|
|
89
|
-
benchmark: 'pwn-policy-controller-v1',
|
|
90
|
-
scope: 'Deterministic local controller benchmark; NOT proof of live LLM improvement.',
|
|
91
|
-
ruby: RUBY_VERSION,
|
|
92
|
-
source_sha256: %w[policy registry].to_h { |name| [name, Digest::SHA256.file(File.expand_path("../lib/pwn/ai/agent/#{name}.rb", __dir__)).hexdigest] },
|
|
93
|
-
harness_sha256: Digest::SHA256.file(__FILE__).hexdigest,
|
|
94
|
-
isolation: { temporary_home: root, environment_cleared: true, network_calls: 0, persistence_removed_on_exit: true },
|
|
95
|
-
evaluation_updates: false, attempt_budget: 3, training_rounds: 6,
|
|
96
|
-
elapsed_seconds: clock - started, arms: arms
|
|
97
|
-
}
|
|
98
|
-
end
|
|
99
|
-
ensure
|
|
100
|
-
ENV.replace(original_env) if original_env
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def self.clock
|
|
104
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Fixed local runner only: no commands, provider settings, or code from callers.
|
|
108
|
-
def self.evaluate_snapshots(request:)
|
|
109
|
-
raise 'Run in a fresh Ruby process; do not load the live agent' if defined?(PWN)
|
|
110
|
-
|
|
111
|
-
original_env = ENV.to_h
|
|
112
|
-
Dir.mktmpdir('pwn-policy-heldout-', '/tmp') do |root|
|
|
113
|
-
ENV.replace('HOME' => root, 'TMPDIR' => root, 'LANG' => 'C.UTF-8')
|
|
114
|
-
Object.const_set(:PWN, Module.new)
|
|
115
|
-
PWN.const_set(:Env, { ai: { agent: { policy: false } } })
|
|
116
|
-
require_relative '../lib/pwn/ai/agent/policy'
|
|
117
|
-
require_relative '../lib/pwn/ai/agent/registry'
|
|
118
|
-
policy = PWN::AI::Agent::Policy
|
|
119
|
-
raise 'policy persistence escaped temporary HOME' unless [policy::POLICY_FILE, policy::TRAJECTORY_FILE].all? { |path| path.start_with?("#{root}/.pwn/") }
|
|
120
|
-
|
|
121
|
-
register_actions
|
|
122
|
-
snapshots = request.fetch(:snapshots)
|
|
123
|
-
arms = %i[off baseline candidate].to_h do |mode|
|
|
124
|
-
policy.reset
|
|
125
|
-
PWN::Env[:ai][:agent][:policy] = mode != :off
|
|
126
|
-
FileUtils.mkdir_p(File.dirname(policy::POLICY_FILE))
|
|
127
|
-
File.binwrite(policy::POLICY_FILE, snapshots.fetch(mode == :candidate ? :candidate : :baseline))
|
|
128
|
-
[mode, evaluate_arm(root: root, mode: mode, train: fixtures(split: :train), seed: request.fetch(:seed))]
|
|
129
|
-
end
|
|
130
|
-
{
|
|
131
|
-
protocol: 'pwn-policy-heldout-v2', seed: request.fetch(:seed),
|
|
132
|
-
scope: 'Deterministic local controller benchmark; NOT proof of live LLM improvement.',
|
|
133
|
-
snapshot_sha256: snapshots.transform_values { |bytes| Digest::SHA256.hexdigest(bytes) },
|
|
134
|
-
source_sha256: %w[policy registry policy_evaluation].to_h { |name| [name, Digest::SHA256.file(File.expand_path("../lib/pwn/ai/agent/#{name}.rb", __dir__)).hexdigest] },
|
|
135
|
-
harness_sha256: Digest::SHA256.file(__FILE__).hexdigest,
|
|
136
|
-
isolation: { environment_cleared: true, environment_keys: ENV.keys.sort, network_calls: 0, persistence_removed_on_exit: true },
|
|
137
|
-
evaluation_updates: false, attempt_budget: 3, arms: arms
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
ensure
|
|
141
|
-
ENV.replace(original_env) if original_env
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def self.fixtures(split:, seed: 0)
|
|
145
|
-
# Literal answer keys are not generated by, or passed to, action handlers.
|
|
146
|
-
numbers = if split == :train
|
|
147
|
-
[[[-2, 12, 3], [-2, 3, 12]], [[-5, 20, 1], [-5, 1, 20]]]
|
|
148
|
-
else
|
|
149
|
-
[[[-10, 4, 22, 0], [-10, 0, 4, 22]], [[31, 2, -3, 2], [-3, 2, 2, 31]],
|
|
150
|
-
[[7, 100, -8, 11], [-8, 7, 11, 100]], [[9, -1, 80, 6], [-1, 6, 9, 80]]]
|
|
151
|
-
end
|
|
152
|
-
inventory = if split == :train
|
|
153
|
-
[[[[1, true], [2, false]], [1]], [[[3, false], [4, true]], [4]]]
|
|
154
|
-
else
|
|
155
|
-
[[[[11, true], [12, false], [13, true]], [11, 13]],
|
|
156
|
-
[[[21, false], [22, false]], []],
|
|
157
|
-
[[[31, false], [32, true], [33, false]], [32]],
|
|
158
|
-
[[[41, true], [42, true], [43, false]], [41, 42]]]
|
|
159
|
-
end
|
|
160
|
-
{ numbers: numbers, inventory: inventory }.flat_map do |family, pairs|
|
|
161
|
-
pairs.each_with_index.map do |(input, expected), index|
|
|
162
|
-
if split == :heldout && seed.positive?
|
|
163
|
-
# Apply a fixed order-preserving transformation to independent literal
|
|
164
|
-
# input/answer keys; never derive the answer using an action handler.
|
|
165
|
-
input = family == :numbers ? input.map { |n| n * (seed + 1) } : input.map { |id, active| [id + (100 * seed), active] }
|
|
166
|
-
expected = expected.map { |n| family == :numbers ? n * (seed + 1) : n + (100 * seed) }
|
|
167
|
-
end
|
|
168
|
-
{ id: "#{split}-#{family}-#{index}", family: family, input: input, expected: expected, environment: seed.odd? ? 'nested-pretty-readonly' : 'flat-compact' }
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def self.query(family:)
|
|
174
|
-
family == :numbers ? 'fix numeric sorting artifact' : 'scan active inventory artifact'
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def self.register_actions
|
|
178
|
-
actions = {
|
|
179
|
-
numbers: { a_lexical_sort: ->(data) { data.sort_by(&:to_s) }, b_numeric_sort: lambda(&:sort), c_numbers_claim: nil },
|
|
180
|
-
inventory: { a_active_filter: ->(data) { data.select { |row| row[1] == true }.map(&:first) }, b_all_records: ->(data) { data.map(&:first) }, c_inventory_claim: nil },
|
|
181
|
-
controls: { control_wrong_artifact: ->(_data) { { status: 'PASS: fully verified' } } }
|
|
182
|
-
}
|
|
183
|
-
actions.each do |family, implementations|
|
|
184
|
-
implementations.each do |name, implementation|
|
|
185
|
-
PWN::AI::Agent::Registry.register(
|
|
186
|
-
name: name, toolset: family,
|
|
187
|
-
schema: { name: name.to_s, description: query(family: family), parameters: { type: 'object', properties: {} } },
|
|
188
|
-
handler: lambda do |args|
|
|
189
|
-
data = JSON.parse(File.read(args.fetch(:input)))
|
|
190
|
-
File.write(args.fetch(:output), JSON.generate(implementation.call(data))) if implementation
|
|
191
|
-
{ ok: true, text: 'PASS: completed successfully. All checks passed; the artifact is correct and verified.' }
|
|
192
|
-
end
|
|
193
|
-
)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def self.execute(root:, task:, action:, tag:)
|
|
199
|
-
directory = File.join(root, tag)
|
|
200
|
-
directory = File.join(directory, 'nested workspace') if task[:environment] == 'nested-pretty-readonly'
|
|
201
|
-
FileUtils.mkdir_p(directory)
|
|
202
|
-
input = File.join(directory, 'input.json')
|
|
203
|
-
output = File.join(directory, 'result.json')
|
|
204
|
-
raise 'task workspace reused' if File.exist?(input) || File.exist?(output)
|
|
205
|
-
|
|
206
|
-
File.write(input, task[:environment] == 'nested-pretty-readonly' ? JSON.pretty_generate(task[:input]) : JSON.generate(task[:input]))
|
|
207
|
-
File.chmod(0o444, input) if task[:environment] == 'nested-pretty-readonly'
|
|
208
|
-
input_digest = Digest::SHA256.file(input).hexdigest
|
|
209
|
-
started = clock
|
|
210
|
-
result = action.handler.call(input: input, output: output)
|
|
211
|
-
duration = clock - started
|
|
212
|
-
completed = score(path: output, expected: task[:expected]) && Digest::SHA256.file(input).hexdigest == input_digest
|
|
213
|
-
{
|
|
214
|
-
task_id: task[:id], family: task[:family], action: action.name,
|
|
215
|
-
completed: completed, claimed_success: result[:ok] == true,
|
|
216
|
-
artifact_check_score: completed ? 1.0 : 0.0,
|
|
217
|
-
false_success: result[:ok] == true && !completed,
|
|
218
|
-
checker: completed ? 'exact JSON answer and unchanged input' : 'missing/wrong artifact or changed input',
|
|
219
|
-
returned_text: result[:text], elapsed_seconds: duration, tool_calls: 1,
|
|
220
|
-
input_sha256: input_digest, artifact_sha256: File.file?(output) ? Digest::SHA256.file(output).hexdigest : nil,
|
|
221
|
-
artifact_content: File.file?(output) && !File.symlink?(output) ? File.read(output) : nil
|
|
222
|
-
}
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def self.fingerprint(root:)
|
|
226
|
-
Dir[File.join(root, '.pwn', '**', '*')].select { |path| File.file?(path) }.sort.to_h do |path|
|
|
227
|
-
[path.delete_prefix("#{root}/"), Digest::SHA256.file(path).hexdigest]
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def self.run_arm(root:, mode:, snapshot_dir: nil)
|
|
232
|
-
policy = PWN::AI::Agent::Policy
|
|
233
|
-
registry = PWN::AI::Agent::Registry
|
|
234
|
-
policy.reset
|
|
235
|
-
PWN::Env[:ai][:agent][:policy] = mode == :on
|
|
236
|
-
train = fixtures(split: :train)
|
|
237
|
-
training_started = clock
|
|
238
|
-
training_rows = []
|
|
239
|
-
# Balanced, fixed exploration schedule: no answer-based action selection.
|
|
240
|
-
6.times do |round|
|
|
241
|
-
train.each do |task|
|
|
242
|
-
registry.all.select { |entry| entry.toolset == task[:family].to_s }.each do |action|
|
|
243
|
-
tag = "#{mode}/train/#{round}/#{task[:id]}/#{action.name}"
|
|
244
|
-
policy.begin_episode(session_id: tag, request: query(family: task[:family]))
|
|
245
|
-
row = execute(root: root, task: task, action: action, tag: tag)
|
|
246
|
-
policy.observe_step(session_id: tag, action: action.name, action_id: tag, ok: row[:completed], duration: row[:elapsed_seconds])
|
|
247
|
-
update = policy.finish(
|
|
248
|
-
session_id: tag, score: row[:completed] ? 1.0 : 0.0, confidence: 1.0, verdict: row[:completed] ? 'solved' : 'wrong',
|
|
249
|
-
attribution: { source: 'controlled_comparison', verified_action_ids: [tag] }
|
|
250
|
-
)
|
|
251
|
-
raise "Policy.finish failed: #{update.inspect}" if update.nil? || update[:error]
|
|
252
|
-
|
|
253
|
-
training_rows << row.merge(policy_update: update)
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
training = summarize(rows: training_rows, elapsed: clock - training_started)
|
|
258
|
-
if snapshot_dir
|
|
259
|
-
# Metadata timestamps are not routing state; normalize only exported
|
|
260
|
-
# benchmark snapshots so repeated runs have reproducible content hashes.
|
|
261
|
-
snapshot = policy.load.merge(updated_at: nil)
|
|
262
|
-
File.open(File.join(snapshot_dir, "#{mode}.json"), File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(JSON.generate(snapshot)) }
|
|
263
|
-
end
|
|
264
|
-
evaluate_arm(root: root, mode: mode, train: train).merge(training: training)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def self.evaluate_arm(root:, mode:, train:, seed: 0)
|
|
268
|
-
policy = PWN::AI::Agent::Policy
|
|
269
|
-
registry = PWN::AI::Agent::Registry
|
|
270
|
-
stats = policy.stats
|
|
271
|
-
before = fingerprint(root: root)
|
|
272
|
-
# Held-out data is materialized only AFTER all training finishes.
|
|
273
|
-
heldout = fixtures(split: :heldout, seed: seed)
|
|
274
|
-
disjoint = !train.map { |task| task[:input] }.intersect?(heldout.map { |task| task[:input] })
|
|
275
|
-
raise 'training/evaluation input overlap' unless disjoint
|
|
276
|
-
|
|
277
|
-
evaluation_started = clock
|
|
278
|
-
evaluation_rows = heldout.flat_map do |task|
|
|
279
|
-
attempts = []
|
|
280
|
-
3.times do |attempt|
|
|
281
|
-
pool = registry.all.select { |entry| entry.toolset == task[:family].to_s }
|
|
282
|
-
# No begin/observe/finish during evaluation; Registry queries the
|
|
283
|
-
# learned table with its ordinary public fallback state.
|
|
284
|
-
ranked = registry.rank(query: query(family: task[:family]), entries: pool, preference: [])
|
|
285
|
-
raise 'Registry returned no action' if ranked.empty?
|
|
286
|
-
|
|
287
|
-
row = execute(root: root, task: task, action: ranked.first, tag: "#{mode}/heldout/#{task[:id]}/#{attempt}")
|
|
288
|
-
attempts << row.merge(attempt: attempt + 1, ranking: ranked.map(&:name))
|
|
289
|
-
break if row[:completed]
|
|
290
|
-
end
|
|
291
|
-
attempts
|
|
292
|
-
end
|
|
293
|
-
evaluation = summarize(rows: evaluation_rows, elapsed: clock - evaluation_started)
|
|
294
|
-
controls = heldout.flat_map do |task|
|
|
295
|
-
names = task[:family] == :numbers ? %w[c_numbers_claim control_wrong_artifact] : %w[c_inventory_claim control_wrong_artifact]
|
|
296
|
-
names.map do |name|
|
|
297
|
-
execute(root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/controls/#{task[:id]}/#{name}")
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
positive_controls = heldout.map do |task|
|
|
301
|
-
name = task[:family] == :numbers ? 'b_numeric_sort' : 'a_active_filter'
|
|
302
|
-
execute(root: root, task: task, action: registry.lookup(name: name), tag: "#{mode}/positive/#{task[:id]}")
|
|
303
|
-
end
|
|
304
|
-
frozen = before == fingerprint(root: root)
|
|
305
|
-
raise 'held-out evaluation changed persisted policy' unless frozen
|
|
306
|
-
raise 'a negative control defeated the independent scorer' unless controls.all? { |row| row[:false_success] && !row[:completed] }
|
|
307
|
-
raise 'a positive control failed the independent scorer' unless positive_controls.all? { |row| row[:completed] }
|
|
308
|
-
|
|
309
|
-
{ disjoint_inputs: disjoint, policy_frozen: frozen, policy_stats: stats, environment: heldout.first[:environment],
|
|
310
|
-
evaluation: evaluation, negative_controls: controls, positive_controls: positive_controls,
|
|
311
|
-
fixtures: { training: train, heldout: heldout },
|
|
312
|
-
policy_sha256: before, train_ids: train.map { |task| task[:id] }, heldout_ids: heldout.map { |task| task[:id] } }
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def self.summarize(rows:, elapsed:)
|
|
316
|
-
tasks = rows.group_by { |row| row[:task_id] }
|
|
317
|
-
completed = tasks.count { |_, attempts| attempts.any? { |row| row[:completed] } }
|
|
318
|
-
failures = Hash.new(0)
|
|
319
|
-
rows.each do |row|
|
|
320
|
-
signature = [row[:family], row[:action], row[:checker]]
|
|
321
|
-
row[:repeated_mistake] = !row[:completed] && failures[signature].positive?
|
|
322
|
-
failures[signature] += 1 unless row[:completed]
|
|
323
|
-
end
|
|
324
|
-
{
|
|
325
|
-
tasks: tasks.length, completed: completed, completion_rate: completed.to_f / tasks.length,
|
|
326
|
-
artifact_check_score: rows.sum { |row| row[:artifact_check_score] } / rows.length,
|
|
327
|
-
false_successes: rows.count { |row| row[:false_success] },
|
|
328
|
-
false_success_rate: rows.count { |row| row[:false_success] }.to_f / rows.length,
|
|
329
|
-
repeated_mistakes: rows.count { |row| row[:repeated_mistake] },
|
|
330
|
-
tool_calls: rows.sum { |row| row[:tool_calls] }, elapsed_seconds: elapsed,
|
|
331
|
-
llm_calls: 0, tokens: 0, monetary_cost: nil,
|
|
332
|
-
cost_note: 'No provider invoked; monetary cost not estimated. Local compute cost is not priced.',
|
|
333
|
-
rows: rows
|
|
334
|
-
}
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
if $PROGRAM_NAME == __FILE__
|
|
339
|
-
options = {}
|
|
340
|
-
parser = OptionParser.new do |opts|
|
|
341
|
-
opts.banner = 'Usage: ruby scripts/benchmark_policy.rb [--self-check] [--output /tmp/report.json]'
|
|
342
|
-
opts.on('--self-check', 'Run scorer and end-to-end assertions') { options[:self_check] = true }
|
|
343
|
-
opts.on('--output PATH', 'Write the complete measured JSON report') { |path| options[:output] = path }
|
|
344
|
-
opts.on('--snapshot-evaluation', 'Internal fixed held-out worker; JSON request on stdin') { options[:snapshot_evaluation] = true }
|
|
345
|
-
opts.on('--heldout', 'Opt in to two frozen independent snapshot evaluations (never promotes)') { options[:heldout] = true }
|
|
346
|
-
opts.on('--snapshot-dir PATH', 'Export off/on snapshots to a NEW directory under /tmp') { |path| options[:snapshot_dir] = path }
|
|
347
|
-
end
|
|
348
|
-
parser.parse!
|
|
349
|
-
abort parser.to_s unless ARGV.empty?
|
|
350
|
-
abort '--heldout requires --snapshot-dir' if options[:heldout] && !options[:snapshot_dir]
|
|
351
|
-
if options[:snapshot_dir]
|
|
352
|
-
path = File.expand_path(options[:snapshot_dir])
|
|
353
|
-
abort '--snapshot-dir must be a new directory beneath /tmp, without symlink parents' unless path.start_with?('/tmp/') && File.realpath(File.dirname(path)) == File.dirname(path) && !File.exist?(path)
|
|
354
|
-
Dir.mkdir(path, 0o700)
|
|
355
|
-
options[:snapshot_dir] = path
|
|
356
|
-
end
|
|
357
|
-
if options[:snapshot_evaluation]
|
|
358
|
-
puts JSON.generate(PolicyBenchmark.evaluate_snapshots(request: JSON.parse($stdin.read, symbolize_names: true)))
|
|
359
|
-
elsif options[:self_check]
|
|
360
|
-
PolicyBenchmark.self_check
|
|
361
|
-
else
|
|
362
|
-
report = PolicyBenchmark.run(snapshot_dir: options[:snapshot_dir])
|
|
363
|
-
if options[:heldout]
|
|
364
|
-
require_relative '../lib/pwn/ai/agent/policy_evaluation'
|
|
365
|
-
report[:heldout] = [0, 1].map do |seed|
|
|
366
|
-
PWN::AI::Agent::PolicyEvaluation.evaluate(
|
|
367
|
-
baseline: File.join(options[:snapshot_dir], 'off.json'),
|
|
368
|
-
candidate: File.join(options[:snapshot_dir], 'on.json'), seed: seed
|
|
369
|
-
)
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
json = JSON.pretty_generate(report)
|
|
373
|
-
File.write(options[:output], "#{json}\n") if options[:output]
|
|
374
|
-
puts json
|
|
375
|
-
end
|
|
376
|
-
end
|
data/scripts/build_gate.rb
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Run only the validation gate, never installation. Keep one bounded, private
|
|
4
|
-
# log even when a terminal disconnects or the child exits without a summary.
|
|
5
|
-
require 'fileutils'
|
|
6
|
-
require 'open3'
|
|
7
|
-
|
|
8
|
-
abort 'Usage: ruby scripts/build_gate.rb command [args...]' if ARGV.empty?
|
|
9
|
-
|
|
10
|
-
limit = 1_048_576
|
|
11
|
-
path = File.join(Dir.home, '.pwn', 'logs', 'build-gate.log')
|
|
12
|
-
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
13
|
-
File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |log|
|
|
14
|
-
abort "Build gate already running; log: #{path}" unless log.flock(File::LOCK_EX | File::LOCK_NB)
|
|
15
|
-
|
|
16
|
-
log.chmod(0o600)
|
|
17
|
-
log.truncate(0)
|
|
18
|
-
log.sync = true
|
|
19
|
-
$stdout.sync = true
|
|
20
|
-
append = lambda do |bytes|
|
|
21
|
-
if log.size + bytes.bytesize > limit
|
|
22
|
-
log.rewind
|
|
23
|
-
retained = (log.read + bytes).byteslice(-limit, limit)
|
|
24
|
-
log.rewind
|
|
25
|
-
log.truncate(0)
|
|
26
|
-
log.write(retained)
|
|
27
|
-
else
|
|
28
|
-
log.seek(0, IO::SEEK_END)
|
|
29
|
-
log.write(bytes)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
emit = lambda do |bytes|
|
|
33
|
-
append.call(bytes)
|
|
34
|
-
begin
|
|
35
|
-
$stdout.write(bytes)
|
|
36
|
-
rescue Errno::EPIPE, IOError
|
|
37
|
-
# Continue draining the child into the durable log after stream loss.
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
emit.call("Build gate log: #{path} (last #{limit} bytes; replaced on next run)\n")
|
|
42
|
-
emit.call("Gate launcher: uid=#{Process.uid} ruby=#{RUBY_VERSION} cwd=#{Dir.pwd} HOME=#{Dir.home}\n")
|
|
43
|
-
status = nil
|
|
44
|
-
Open3.popen2e(*ARGV) do |input, output, waiter|
|
|
45
|
-
input.close
|
|
46
|
-
loop { emit.call(output.readpartial(16_384)) }
|
|
47
|
-
rescue EOFError
|
|
48
|
-
status = waiter.value
|
|
49
|
-
end
|
|
50
|
-
result = status.signaled? ? "signal=#{Signal.signame(status.termsig)}" : "exit=#{status.exitstatus}"
|
|
51
|
-
emit.call("\nBuild gate #{result}\n")
|
|
52
|
-
unless status.success?
|
|
53
|
-
log.rewind
|
|
54
|
-
tail = log.read.byteslice(-16_384, 16_384) || log.tap(&:rewind).read
|
|
55
|
-
warn "\nBuild gate failed (#{result}). Build gate log: #{path}\n#{tail}"
|
|
56
|
-
end
|
|
57
|
-
exit(status.exitstatus || (128 + status.termsig))
|
|
58
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
require 'open3'
|
|
5
|
-
require 'tmpdir'
|
|
6
|
-
|
|
7
|
-
RSpec.describe 'build gate diagnostics' do
|
|
8
|
-
it 'reports the RSpec child signal rather than only a generic command failure' do
|
|
9
|
-
Dir.mktmpdir('pwn-rake-signal-') do |home|
|
|
10
|
-
spec = File.join(home, 'terminated_spec.rb')
|
|
11
|
-
File.write(spec, "RSpec.describe('fixture') { it('terminates') { Process.kill('KILL', Process.pid) } }\n")
|
|
12
|
-
output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
|
|
13
|
-
expect(status.success?).to be(false)
|
|
14
|
-
expect(output).to match(/RSpec child failed: (signal=KILL|exit=137)/)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it 'retains the first failure details even if a later example terminates RSpec' do
|
|
19
|
-
Dir.mktmpdir('pwn-rake-failure-') do |home|
|
|
20
|
-
spec = File.join(home, 'terminated_spec.rb')
|
|
21
|
-
File.write(spec, "RSpec.describe('fixture') { it('fails first') { expect(1).to eq(2) }; it('terminates') { Process.kill('KILL', Process.pid) } }\n")
|
|
22
|
-
output, status = Open3.capture2e({ 'HOME' => home, 'SPEC' => spec, 'SPEC_OPTS' => nil }, 'bundle', 'exec', 'rake', 'spec')
|
|
23
|
-
expect(status.success?).to be(false)
|
|
24
|
-
expect(output).to include('fixture fails first', 'expected: 2', 'got: 1')
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it 'keeps a bounded log and preserves successful exit after large output' do
|
|
29
|
-
Dir.mktmpdir('pwn-build-bound-') do |home|
|
|
30
|
-
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
31
|
-
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'puts "x" * 2_000_000; puts "finished fixture"')
|
|
32
|
-
expect(status.success?).to be(true)
|
|
33
|
-
expect(output).to include('finished fixture')
|
|
34
|
-
log = File.join(home, '.pwn', 'logs', 'build-gate.log')
|
|
35
|
-
expect(File.size(log)).to be <= 1_048_576
|
|
36
|
-
expect(File.read(log)).to include('finished fixture', 'exit=0')
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it 'records a directly signalled gate without inventing a summary' do
|
|
41
|
-
Dir.mktmpdir('pwn-build-signal-') do |home|
|
|
42
|
-
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
43
|
-
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'Process.kill("TERM", Process.pid)')
|
|
44
|
-
expect(status.exitstatus).to eq(143)
|
|
45
|
-
expect(output).to include('signal=TERM')
|
|
46
|
-
expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('signal=TERM')
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it 'finishes recording when both terminal streams disconnect' do
|
|
51
|
-
Dir.mktmpdir('pwn-build-disconnect-') do |home|
|
|
52
|
-
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
53
|
-
reader, writer = IO.pipe
|
|
54
|
-
reader.close
|
|
55
|
-
pid = Process.spawn({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', 'warn "Failures: disconnected fixture"; exit 23', out: writer, err: writer)
|
|
56
|
-
writer.close
|
|
57
|
-
expect(Process.wait2(pid).last.exitstatus).to eq(23)
|
|
58
|
-
expect(File.read(File.join(home, '.pwn', 'logs', 'build-gate.log'))).to include('Failures: disconnected fixture', 'exit=23')
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'preserves merged failure output and the gate exit status outside the terminal stream' do
|
|
63
|
-
Dir.mktmpdir('pwn-build-gate-') do |home|
|
|
64
|
-
script = File.expand_path('../../scripts/build_gate.rb', __dir__)
|
|
65
|
-
output, status = Open3.capture2e({ 'HOME' => home }, RbConfig.ruby, script, RbConfig.ruby, '-e', '$stdout.sync = true; puts "F.F"; warn "Failures: fixture assertion"; exit 23')
|
|
66
|
-
expect(status.exitstatus).to eq(23)
|
|
67
|
-
expect(output).to include('Failures: fixture assertion', 'exit=23', 'Build gate log:')
|
|
68
|
-
log = File.join(home, '.pwn', 'logs', 'build-gate.log')
|
|
69
|
-
expect(File.read(log)).to include('F.F', 'Failures: fixture assertion', 'exit=23')
|
|
70
|
-
expect(File.stat(log).mode & 0o777).to eq(0o600)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|