lemans 0.0.0.pre → 0.2.1

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/exe/lemans +17 -0
  6. data/exe/lemans-remote +984 -0
  7. data/lib/lemans/agents/base.rb +30 -0
  8. data/lib/lemans/agents/miniswen.rb +119 -0
  9. data/lib/lemans/agents/miniswen_installed.rb +67 -0
  10. data/lib/lemans/agents/nop.rb +15 -0
  11. data/lib/lemans/agents/oracle.rb +53 -0
  12. data/lib/lemans/agents.rb +21 -0
  13. data/lib/lemans/bench.rb +280 -0
  14. data/lib/lemans/cli/board_reporter.rb +135 -0
  15. data/lib/lemans/cli/progress_reporter.rb +67 -0
  16. data/lib/lemans/cli.rb +181 -0
  17. data/lib/lemans/clobber.rb +79 -0
  18. data/lib/lemans/environments/base.rb +55 -0
  19. data/lib/lemans/environments/daytona/retries.rb +49 -0
  20. data/lib/lemans/environments/daytona/sdk_tweaks.rb +50 -0
  21. data/lib/lemans/environments/daytona/shell.rb +142 -0
  22. data/lib/lemans/environments/daytona/snapshot_store.rb +163 -0
  23. data/lib/lemans/environments/daytona.rb +175 -0
  24. data/lib/lemans/environments.rb +16 -0
  25. data/lib/lemans/network_policy.rb +66 -0
  26. data/lib/lemans/patch.rb +70 -0
  27. data/lib/lemans/restore_paths.rb +21 -0
  28. data/lib/lemans/results/aggregate.rb +114 -0
  29. data/lib/lemans/results/cost_source.rb +13 -0
  30. data/lib/lemans/results/outcome.rb +36 -0
  31. data/lib/lemans/results/report.rb +149 -0
  32. data/lib/lemans/results/sorting.rb +24 -0
  33. data/lib/lemans/results/tally.rb +19 -0
  34. data/lib/lemans/results/usage.rb +24 -0
  35. data/lib/lemans/run.rb +152 -0
  36. data/lib/lemans/setup.rb +59 -0
  37. data/lib/lemans/setup_files.rb +36 -0
  38. data/lib/lemans/snapshot.rb +55 -0
  39. data/lib/lemans/task.rb +207 -0
  40. data/lib/lemans/tree_digest.rb +24 -0
  41. data/lib/lemans/trial.rb +187 -0
  42. data/lib/lemans/units.rb +44 -0
  43. data/lib/lemans/verifier/assets/eport-lemans.rb +36 -0
  44. data/lib/lemans/verifier/assets/lemans_minitest_reporter.rb +61 -0
  45. data/lib/lemans/verifier.rb +199 -0
  46. data/lib/lemans/version.rb +5 -0
  47. data/lib/lemans.rb +29 -0
  48. data/lib/miniswen/agent.rb +678 -0
  49. data/lib/miniswen/cli.rb +224 -0
  50. data/lib/miniswen/environment.rb +14 -0
  51. data/lib/miniswen/local.rb +42 -0
  52. data/lib/miniswen/ruby_llm.rb +42 -0
  53. data/lib/miniswen/testing.rb +134 -0
  54. data/lib/miniswen/trajectory.rb +110 -0
  55. data/lib/miniswen/version.rb +5 -0
  56. data/lib/miniswen.rb +48 -0
  57. metadata +161 -7
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require "shellwords"
6
+ require "tmpdir"
7
+
8
+ module Lemans
9
+ # Verifies a trial in the sandbox the agent worked in, after Trial has closed
10
+ # its network. The tests are uploaded fresh at verification time, never before.
11
+ class Verifier
12
+ REWARD_RANGE = (0.0..1.0)
13
+
14
+ # Where the task's tests land at verification time
15
+ TESTS_DIR = "/tests"
16
+
17
+ # Harness-owned files used in verification tests
18
+ ASSETS = Pathname(File.expand_path("verifier/assets", __dir__))
19
+
20
+ VERIFY_BIN = "verify"
21
+
22
+ # The message a person finds where the suite output would have been.
23
+ TAMPERED = "The graded surfaces could not be restored from the sealed baseline: the sandbox no " \
24
+ "longer holds the tree sealed before the agent's first turn. Removing or rewriting " \
25
+ "it is a failed check, so this run scores 0.\n"
26
+
27
+ def initialize(bench:, task:, dir:, snapshot: nil)
28
+ @bench = bench
29
+ @task = task
30
+ @dir = dir
31
+ @snapshot = snapshot
32
+ end
33
+
34
+ def call(environment)
35
+ upload_tests(environment)
36
+ prepare(environment)
37
+
38
+ # A baseline the agent made unrestorable is a verdict, not an error.
39
+ unless restore_baseline(environment)
40
+ dir.mkpath
41
+ dir.join("verifier.log").write(TAMPERED)
42
+ return 0.0
43
+ end
44
+
45
+ reward = verify(environment)
46
+ download_evidence(environment)
47
+ reward
48
+ rescue InfrastructureError => e
49
+ salvage_evidence(environment)
50
+ # Everything under verification is the verifier's failure, never the
51
+ # model's
52
+ raise if e.is_a?(VerifierError)
53
+
54
+ raise VerifierError, e.message
55
+ rescue StandardError
56
+ salvage_evidence(environment)
57
+ raise
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :bench, :task, :dir
63
+
64
+ def restore_baseline(environment)
65
+ snapshot = @snapshot || Snapshot.new(environment, bench: bench, task: task,
66
+ timeout: bench.verifier.timeout_sec)
67
+ snapshot.restore!
68
+ end
69
+
70
+ def upload_tests(environment)
71
+ environment.exec!("rm -rf #{TESTS_DIR} && mkdir -p #{TESTS_DIR}")
72
+
73
+ uploads = bench.verification_files.to_h { |local, remote| [remote, local] }
74
+ .merge(task.test_files.to_h { |local, remote| [remote, local] })
75
+
76
+ uploads.each { |remote, local| environment.upload(local, "#{TESTS_DIR}/#{remote}") }
77
+ ASSETS.glob("*.rb").each { |asset| environment.upload(asset, "#{TESTS_DIR}/#{asset.basename}") }
78
+ environment.exec!("chmod +x #{TESTS_DIR}/#{VERIFY_BIN}") if uploads.key?(VERIFY_BIN)
79
+ end
80
+
81
+ def prepare(environment)
82
+ Setup.new(
83
+ commands: bench.verifier.setup,
84
+ task: task,
85
+ phase: :verifier,
86
+ timeout_sec: bench.verifier.timeout_sec
87
+ ).call(environment)
88
+ end
89
+
90
+ def verify(environment)
91
+ # Ensure $LOGS exists
92
+ environment.exec!("mkdir -p #{Shellwords.escape(bench.verifier.logs_dir)}")
93
+ # Ensure the agent hasn't pre-written reward.txt or checks.json
94
+ environment.exec!("rm -f #{Shellwords.escape(bench.verifier.reward_path)} " \
95
+ "#{Shellwords.escape(File.join(bench.verifier.logs_dir, "checks.json"))}")
96
+
97
+ env = { "WORKDIR" => bench.environment.workdir,
98
+ "TESTS" => TESTS_DIR,
99
+ "LOGS" => bench.verifier.logs_dir }
100
+ command = "cd #{Shellwords.escape(bench.environment.workdir)} && " \
101
+ "export RUBYOPT=\"${RUBYOPT:+$RUBYOPT }-I#{TESTS_DIR}\" && " \
102
+ "#{verifier_script}"
103
+ result = environment.exec(command, timeout: bench.verifier.timeout_sec, env: env)
104
+ dir.mkpath
105
+ dir.join("verifier.log").write(result.output.to_s)
106
+
107
+ read_reward(environment, result)
108
+ end
109
+
110
+ def verifier_script
111
+ [bench.verifier.preverify, bench.verifier.command].compact.map { "( #{_1} )" }.join(" && ")
112
+ end
113
+
114
+ def read_reward(environment, command_result)
115
+ reward_path = bench.verifier.reward_path
116
+ present = environment.exec("test -e #{Shellwords.escape(reward_path)}")
117
+ return reward_from_exit(command_result) unless present.success?
118
+
119
+ result = environment.exec("cat #{Shellwords.escape(reward_path)}")
120
+ raise VerifierError, "could not read #{reward_path}: #{result.output.to_s[0, 500]}" unless result.success?
121
+
122
+ value = begin
123
+ Float(result.output.to_s.strip)
124
+ rescue ArgumentError
125
+ raise VerifierError, "verifier wrote #{result.output.to_s.strip.inspect}, which is not a reward"
126
+ end
127
+ raise VerifierError, "verifier wrote a non-finite reward" unless value.finite?
128
+ raise VerifierError, "reward #{value} is outside #{REWARD_RANGE}" unless REWARD_RANGE.cover?(value)
129
+
130
+ value
131
+ end
132
+
133
+ def reward_from_exit(command_result)
134
+ case command_result.exit_code
135
+ when 0 then 1.0
136
+ when 1 then 0.0
137
+ else
138
+ raise VerifierError,
139
+ "verifier exited #{command_result.exit_code} and wrote no reward to #{bench.verifier.reward_path}"
140
+ end
141
+ end
142
+
143
+ def download_evidence(environment)
144
+ @evidence_attempted = true
145
+ root = bench.verifier.logs_dir
146
+ paths = list_files(environment, root)
147
+ return if paths.empty?
148
+
149
+ relative_to = Pathname(root).cleanpath
150
+
151
+ # Use a temp dir to download evidence to check for collisions
152
+ Dir.mktmpdir("lemans-evidence") do |staging|
153
+ paths.each do |remote|
154
+ relative = checked_remote_path(remote, root).relative_path_from(relative_to)
155
+ staged = Pathname(staging).join(relative)
156
+ staged.dirname.mkpath
157
+ environment.download(remote, staged)
158
+
159
+ destination = dir.join(relative)
160
+ if destination.exist?
161
+ warn "lemans: evidence file #{relative} collides with a harness file and was dropped"
162
+ next
163
+ end
164
+
165
+ destination.dirname.mkpath
166
+ FileUtils.cp(staged, destination)
167
+ end
168
+ end
169
+ end
170
+
171
+ def list_files(environment, declared)
172
+ return [] unless environment.exec("test -d #{Shellwords.escape(declared)}").success?
173
+
174
+ listing = environment.exec("find #{Shellwords.escape(declared)} -type f -print0")
175
+ raise VerifierError, "could not list #{declared}: #{listing.output.to_s[0, 500]}" unless listing.success?
176
+
177
+ listing.output.to_s.split("\0").reject(&:empty?)
178
+ end
179
+
180
+ def checked_remote_path(remote, declared)
181
+ root = Pathname(declared).cleanpath
182
+ candidate = Pathname(remote).cleanpath
183
+
184
+ raise VerifierError, "evidence file #{remote.inspect} escapes #{declared}" unless candidate.absolute? && candidate.to_s.start_with?("#{root}/")
185
+
186
+ raise VerifierError, "evidence file #{remote.inspect} contains control characters" if remote.match?(/[[:cntrl:]]/)
187
+
188
+ candidate
189
+ end
190
+
191
+ def salvage_evidence(environment)
192
+ return if @evidence_attempted
193
+
194
+ download_evidence(environment)
195
+ rescue StandardError => e
196
+ warn "lemans: could not save the verifier's evidence: #{e.class}: #{e.message}"
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lemans
4
+ VERSION = "0.2.1"
5
+ end
data/lib/lemans.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect("cli" => "CLI", "atif" => "ATIF")
7
+ # miniswen lives in this repo but is a plain-require gem of its own.
8
+ loader.ignore("#{__dir__}/miniswen.rb", "#{__dir__}/miniswen")
9
+ # assets/ holds files uploaded into sandboxes, not Ruby the harness loads.
10
+ loader.ignore("#{__dir__}/lemans/verifier/assets")
11
+ loader.setup
12
+
13
+ require "miniswen"
14
+
15
+ module Lemans
16
+ class Error < StandardError; end
17
+
18
+ # Anything the caller can fix: a malformed bench.yml, a task directory
19
+ # missing a file, an unknown network mode.
20
+ class ConfigError < Error; end
21
+
22
+ # A backend, agent, or verifier failing in a way that is the harness's fault
23
+ # rather than the model's. These become an Outcome, never a zero reward.
24
+ class InfrastructureError < Error; end
25
+
26
+ # The verifier itself failed, or wrote something that is not a reward. Retrying
27
+ # would verify the same patch the same way, so it is terminal.
28
+ class VerifierError < InfrastructureError; end
29
+ end