lemans 1.4.0 → 1.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +17 -2
- data/exe/lemans-remote +141 -3
- data/exe/lemans-viewer +998 -0
- data/lib/lemans/agent.rb +4 -1
- data/lib/lemans/agents/miniswen.rb +10 -5
- data/lib/lemans/agents/miniswen_installed.rb +16 -5
- data/lib/lemans/agents/nop.rb +1 -1
- data/lib/lemans/agents/oracle.rb +1 -1
- data/lib/lemans/agents.rb +4 -2
- data/lib/lemans/cli/regrade.rb +29 -77
- data/lib/lemans/cli/report/aggregate.rb +54 -30
- data/lib/lemans/cli/report.rb +168 -28
- data/lib/lemans/cli.rb +110 -34
- data/lib/lemans/environments/docker/proxy/Dockerfile +4 -0
- data/lib/lemans/environments/docker/proxy/README.md +49 -0
- data/lib/lemans/environments/docker/proxy/server.rb +79 -0
- data/lib/lemans/environments/docker.rb +79 -16
- data/lib/lemans/result.rb +65 -6
- data/lib/lemans/runner/task.rb +9 -5
- data/lib/lemans/runner.rb +45 -2
- data/lib/lemans/store.rb +6 -0
- data/lib/lemans/stores/fs.rb +16 -4
- data/lib/lemans/trial/patch.rb +21 -0
- data/lib/lemans/trial/verifier/test_scanner.rb +136 -0
- data/lib/lemans/trial/verifier.rb +19 -10
- data/lib/lemans/trial.rb +108 -40
- data/lib/lemans/version.rb +1 -1
- data/lib/lemans.rb +2 -0
- data/lib/miniswen/agent.rb +52 -16
- data/lib/miniswen/cli.rb +8 -3
- data/lib/miniswen/environment/docker.rb +1 -1
- data/lib/miniswen/jail.rb +1 -1
- data/lib/miniswen/local.rb +1 -1
- data/lib/miniswen/version.rb +1 -1
- metadata +49 -1
|
@@ -10,7 +10,7 @@ module Lemans
|
|
|
10
10
|
# Verifies a trial in the sandbox the agent worked in, after Trial has closed
|
|
11
11
|
# its network. The tests are uploaded fresh at verification time, never before.
|
|
12
12
|
class Verifier
|
|
13
|
-
Verification = Data.define(:reward, :credit, :logs)
|
|
13
|
+
Verification = Data.define(:reward, :credit, :features, :logs)
|
|
14
14
|
|
|
15
15
|
REWARD_RANGE = (0.0..1.0)
|
|
16
16
|
|
|
@@ -22,6 +22,13 @@ module Lemans
|
|
|
22
22
|
|
|
23
23
|
VERIFY_BIN = "verify"
|
|
24
24
|
|
|
25
|
+
# A feature passes when all its checks passed; one with a check the run
|
|
26
|
+
# never reported is left out.
|
|
27
|
+
def self.features_from(statuses, features)
|
|
28
|
+
graded = features.to_h.select { |_, checks| checks.all? { statuses.key?(it) } }
|
|
29
|
+
graded.transform_values { |checks| checks.all? { statuses[it] == "pass" } }.sort.to_h unless graded.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
25
32
|
private attr_reader :task, :environment, :snapshot, :timeout
|
|
26
33
|
|
|
27
34
|
def initialize(task, environment, snapshot)
|
|
@@ -94,7 +101,9 @@ module Lemans
|
|
|
94
101
|
result = environment.exec(command, timeout:, env:)
|
|
95
102
|
|
|
96
103
|
reward = read_reward(result)
|
|
97
|
-
|
|
104
|
+
checks = read_checks
|
|
105
|
+
features = (self.class.features_from(checks.fetch("checks", {}), TestScanner.for(task)&.features) if checks && task.final_step?)
|
|
106
|
+
Verification.new(reward:, credit: credit_from(checks, reward), features:, logs: result.output.to_s)
|
|
98
107
|
end
|
|
99
108
|
|
|
100
109
|
def verifier_script
|
|
@@ -120,20 +129,20 @@ module Lemans
|
|
|
120
129
|
value
|
|
121
130
|
end
|
|
122
131
|
|
|
123
|
-
def
|
|
132
|
+
def read_checks
|
|
124
133
|
path = File.join(task.verifier.logs_dir, "checks.json")
|
|
125
|
-
return
|
|
134
|
+
return nil unless environment.exec("test -e #{Shellwords.escape(path)}").success?
|
|
126
135
|
|
|
127
136
|
result = environment.exec("cat #{Shellwords.escape(path)}")
|
|
128
137
|
raise VerifierError, "could not read #{path}: #{result.output.to_s[0, 500]}" unless result.success?
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
139
|
+
JSON.parse(result.output.to_s)
|
|
140
|
+
rescue JSON::ParserError => e
|
|
141
|
+
raise VerifierError, "#{path} is not JSON: #{e.message[0, 500]}"
|
|
142
|
+
end
|
|
135
143
|
|
|
136
|
-
|
|
144
|
+
def credit_from(checks, reward)
|
|
145
|
+
grading = checks&.dig("grading")
|
|
137
146
|
return reward unless grading && (base_credit = grading["base_credit"])
|
|
138
147
|
return 0.0 if reward.zero?
|
|
139
148
|
|
data/lib/lemans/trial.rb
CHANGED
|
@@ -12,13 +12,15 @@ module Lemans
|
|
|
12
12
|
class Trial
|
|
13
13
|
attr_reader :task, :config, :model, :agent_name, :environment, :result
|
|
14
14
|
|
|
15
|
-
private attr_reader :agent, :store, :snapshot, :patch, :current_step_index
|
|
15
|
+
private attr_reader :agent, :store, :restart_from, :restart_mode, :snapshot, :patch, :current_step_index
|
|
16
16
|
|
|
17
|
-
def initialize(task, model = nil, result: nil, store: nil, agent: nil, environment: nil)
|
|
17
|
+
def initialize(task, model = nil, result: nil, store: nil, agent: nil, environment: nil, restart_from: nil, restart_mode: nil)
|
|
18
18
|
@task = task
|
|
19
19
|
@config = task.config
|
|
20
20
|
@model = model || config.models.first
|
|
21
21
|
@store = store
|
|
22
|
+
@restart_from = restart_from
|
|
23
|
+
@restart_mode = restart_mode
|
|
22
24
|
|
|
23
25
|
@agent = agent.is_a?(Agent) ? agent : Agents.build(agent || config.agent_name, profile: config.agent, model: @model)
|
|
24
26
|
@agent_name = @agent.name
|
|
@@ -47,9 +49,12 @@ module Lemans
|
|
|
47
49
|
@snapshot = nil
|
|
48
50
|
@patch = nil
|
|
49
51
|
@current_step_index = nil
|
|
52
|
+
@recovered_since = nil
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
def run
|
|
56
|
+
restart! if restart_from
|
|
57
|
+
|
|
53
58
|
phase(:environment_setup) do
|
|
54
59
|
environment.start
|
|
55
60
|
|
|
@@ -64,45 +69,55 @@ module Lemans
|
|
|
64
69
|
# Seal the git state to collect the agent's patch later
|
|
65
70
|
@patch = Patch.new(task, environment)
|
|
66
71
|
patch.seal!
|
|
72
|
+
patch.replay!(settled_patches, (restart_artifact("agent.patch") if restart_mode)) if restart_from
|
|
67
73
|
|
|
68
|
-
agent
|
|
74
|
+
# A reverification of the last step runs no agent
|
|
75
|
+
agent.install(task, environment) unless restart_mode == :reverify && restart_step == task.steps
|
|
69
76
|
|
|
70
77
|
environment.switch_network_policy!(config.agent.environment.network)
|
|
71
78
|
end
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
adopt_phases! if restart_from
|
|
81
|
+
|
|
82
|
+
pending = restart_mode
|
|
83
|
+
each_step(from: restart_from ? restart_step : 1) do |step_task|
|
|
84
|
+
mode, pending = pending, nil
|
|
85
|
+
|
|
86
|
+
unless mode == :reverify
|
|
87
|
+
history = restart_artifact("agent.result.json") if mode == :recover
|
|
88
|
+
response =
|
|
89
|
+
phase(:agent, started_at: (@recovered_since if mode == :recover)) do
|
|
90
|
+
agent.run(step_task, environment, history:)
|
|
91
|
+
rescue InfrastructureError, ::Miniswen::InfrastructureError => e
|
|
92
|
+
# Mark the failure here, where the agent phase is still known
|
|
93
|
+
result.failed!(:agent_error, e.message)
|
|
94
|
+
collect_patch!
|
|
95
|
+
raise
|
|
96
|
+
rescue ::Miniswen::AccountingError
|
|
97
|
+
# Classified by the outer rescue; the work is still on disk
|
|
98
|
+
collect_patch!
|
|
99
|
+
raise
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Whatever the agent brought back is evidence, a failed run's included
|
|
103
|
+
save_trajectory!(response.trajectory)
|
|
104
|
+
store&.save_artifact(result, response.raw_result, path: with_step_index("agent.result.json")) if response.raw_result
|
|
105
|
+
|
|
106
|
+
if response.error?
|
|
107
|
+
result.failed!(:agent_error, response.error)
|
|
84
108
|
collect_patch!
|
|
85
|
-
|
|
109
|
+
return result
|
|
86
110
|
end
|
|
87
111
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
result.failed!(:agent_error, response.error)
|
|
94
|
-
collect_patch!
|
|
95
|
-
return result
|
|
96
|
-
end
|
|
112
|
+
if task.multistep?
|
|
113
|
+
result.step_completed!(response.outcome, response.usage, duration: result.phases.last.duration)
|
|
114
|
+
else
|
|
115
|
+
result.completed!(response.outcome, response.usage)
|
|
116
|
+
end
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
result.step_completed!(response.outcome, response.usage, duration: result.phases.last.duration)
|
|
100
|
-
else
|
|
101
|
-
result.completed!(response.outcome, response.usage)
|
|
118
|
+
check_cost_limit!
|
|
102
119
|
end
|
|
103
120
|
|
|
104
|
-
check_cost_limit!
|
|
105
|
-
|
|
106
121
|
collect_patch!
|
|
107
122
|
if step_task.final_step?
|
|
108
123
|
patch.compile!(result, store) if task.multistep? && store
|
|
@@ -124,7 +139,7 @@ module Lemans
|
|
|
124
139
|
store&.save_artifact(result, verification.logs, path: with_step_index("verifier.log"))
|
|
125
140
|
|
|
126
141
|
if step_task.final_step?
|
|
127
|
-
result.graded!(verification.reward, credit: verification.credit)
|
|
142
|
+
result.graded!(verification.reward, credit: verification.credit, features: verification.features)
|
|
128
143
|
elsif verification.reward.zero?
|
|
129
144
|
result.graded!(0.0)
|
|
130
145
|
throw :halt
|
|
@@ -166,31 +181,84 @@ module Lemans
|
|
|
166
181
|
store.save_artifact(result, JSON.pretty_generate(trajectory.to_atif), path:)
|
|
167
182
|
end
|
|
168
183
|
|
|
169
|
-
def each_step
|
|
184
|
+
def each_step(from: 1)
|
|
170
185
|
return yield task unless task.multistep?
|
|
171
186
|
|
|
172
187
|
catch(:halt) do
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
from.upto(task.steps) do |index|
|
|
189
|
+
# The first step of a restart finds a fresh tree, already replayed
|
|
190
|
+
resume_agent! if index > from
|
|
175
191
|
@current_step_index = index
|
|
176
192
|
yield task.for_step(index)
|
|
177
193
|
end
|
|
178
194
|
end
|
|
179
195
|
end
|
|
180
196
|
|
|
197
|
+
# The step a restart begins at: the one a reverification grades again, or
|
|
198
|
+
# the first one not settled (run afresh, or recovered from its history).
|
|
199
|
+
def restart_step
|
|
200
|
+
@restart_step ||= restart_mode == :reverify ? restart_from.verified_step : restart_from.settled_steps + 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The new run carries the settled steps' evidence, and the agent's part of
|
|
204
|
+
# a step it grades again: the patch is collected anew.
|
|
205
|
+
def restart!
|
|
206
|
+
result.restart!(restart_from, step: restart_step, mode: restart_mode)
|
|
207
|
+
carried = restart_mode == :reverify ? %w[trajectory.json agent.result.json].map { restart_path(it) } : []
|
|
208
|
+
|
|
209
|
+
store.artifacts(restart_from).each do |path|
|
|
210
|
+
next unless path[%r{\.(\d+)(?:\.[^./]+)?\z}, 1].to_i.between?(1, restart_step - 1) || carried.include?(path)
|
|
211
|
+
|
|
212
|
+
store.save_artifact(result, store.read_artifact(restart_from, path), path:)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def settled_patches
|
|
217
|
+
(1...restart_step).map do |index|
|
|
218
|
+
store.read_artifact(restart_from, "agent.#{index}.patch") ||
|
|
219
|
+
raise(InfrastructureError, "#{restart_from.id} has no agent.#{index}.patch to replay")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def restart_artifact(name)
|
|
224
|
+
path = restart_path(name)
|
|
225
|
+
store.read_artifact(restart_from, path) || raise(InfrastructureError, "#{restart_from.id} has no #{path} to restart from")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def restart_path(name) = task.multistep? ? with_step_index(name, restart_step) : name
|
|
229
|
+
|
|
230
|
+
# A recovered step's agent phase reaches back over the part the source
|
|
231
|
+
# run already spent, so the step lasts as long as both parts together.
|
|
232
|
+
def adopt_phases!
|
|
233
|
+
names = restart_from.phases.map(&:name).select { it.to_s[/\.(\d+)\z/, 1].to_i.between?(1, restart_step - 1) }
|
|
234
|
+
agent_phase = restart_path("agent").to_sym
|
|
235
|
+
now = Time.now.utc
|
|
236
|
+
|
|
237
|
+
case restart_mode
|
|
238
|
+
when :reverify
|
|
239
|
+
result.adopt_phases!(restart_from, names + [ agent_phase ], now:)
|
|
240
|
+
when :recover
|
|
241
|
+
result.adopt_phases!(restart_from, names, through: agent_phase, now:)
|
|
242
|
+
partial = restart_from.phases.find { it.name == agent_phase }
|
|
243
|
+
@recovered_since = now - (partial.finished_at - partial.started_at)
|
|
244
|
+
else
|
|
245
|
+
result.adopt_phases!(restart_from, names, now:)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
181
249
|
def resume_agent!
|
|
182
250
|
patch.restore!
|
|
183
251
|
environment.exec!("rm -rf #{Verifier::TESTS_DIR} #{Shellwords.escape(task.verifier.logs_dir)}")
|
|
184
252
|
environment.switch_network_policy!(config.agent.environment.network)
|
|
185
253
|
end
|
|
186
254
|
|
|
187
|
-
def with_step_index(path)
|
|
188
|
-
return path unless
|
|
255
|
+
def with_step_index(path, index = current_step_index)
|
|
256
|
+
return path unless index
|
|
189
257
|
|
|
190
258
|
*pre, last = path.to_s.split(".")
|
|
191
|
-
return "#{last}.#{
|
|
259
|
+
return "#{last}.#{index}" if pre.empty?
|
|
192
260
|
|
|
193
|
-
[ *pre,
|
|
261
|
+
[ *pre, index, last ].join(".")
|
|
194
262
|
end
|
|
195
263
|
|
|
196
264
|
def sandbox_ttl
|
|
@@ -209,8 +277,8 @@ module Lemans
|
|
|
209
277
|
)
|
|
210
278
|
end
|
|
211
279
|
|
|
212
|
-
def phase(name)
|
|
213
|
-
result.phase_started(with_step_index(name).to_sym)
|
|
280
|
+
def phase(name, started_at: nil)
|
|
281
|
+
result.phase_started(with_step_index(name).to_sym, started_at)
|
|
214
282
|
yield
|
|
215
283
|
ensure
|
|
216
284
|
result.phase_finished(with_step_index(name).to_sym)
|
data/lib/lemans/version.rb
CHANGED
data/lib/lemans.rb
CHANGED
|
@@ -11,6 +11,8 @@ loader.ignore("#{__dir__}/miniswen.rb", "#{__dir__}/miniswen")
|
|
|
11
11
|
loader.ignore("#{__dir__}/lemans/trial/verifier/assets")
|
|
12
12
|
# templates/ holds the files `lemans init` scaffolds, not Ruby the harness loads.
|
|
13
13
|
loader.ignore("#{__dir__}/lemans/cli/templates")
|
|
14
|
+
# proxy/ holds the image the Docker backend runs for allowlists, not Ruby the harness loads.
|
|
15
|
+
loader.ignore("#{__dir__}/lemans/environments/docker/proxy")
|
|
14
16
|
loader.setup
|
|
15
17
|
|
|
16
18
|
require "miniswen"
|
data/lib/miniswen/agent.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "time"
|
|
4
5
|
require "miniswen/version"
|
|
5
6
|
require "miniswen/ruby_llm"
|
|
6
7
|
|
|
@@ -261,25 +262,17 @@ module Miniswen
|
|
|
261
262
|
@reporter = reporter
|
|
262
263
|
end
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
]
|
|
273
|
-
|
|
274
|
-
@steps = 0
|
|
275
|
-
@cost = 0.0
|
|
276
|
-
|
|
277
|
-
@totals = { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 }
|
|
265
|
+
# With a `history` (the Result of an interrupted run), the session goes on
|
|
266
|
+
# where it stopped, under the budget the earlier part already spent.
|
|
267
|
+
def run(instruction = nil, history: nil)
|
|
268
|
+
if history
|
|
269
|
+
resume(history)
|
|
270
|
+
else
|
|
271
|
+
start(instruction)
|
|
272
|
+
end
|
|
278
273
|
|
|
279
|
-
@cost_known = true
|
|
280
274
|
@consecutive_format_errors = 0
|
|
281
275
|
@refused_turns = 0
|
|
282
|
-
@started_at = @clock.call
|
|
283
276
|
|
|
284
277
|
loop do
|
|
285
278
|
(status = limit_reached) and return finish(status)
|
|
@@ -329,6 +322,49 @@ module Miniswen
|
|
|
329
322
|
|
|
330
323
|
private
|
|
331
324
|
|
|
325
|
+
def start(instruction)
|
|
326
|
+
uname = execute("uname -srvm").output.to_s.strip
|
|
327
|
+
@messages = [
|
|
328
|
+
{ role: "system", content: SYSTEM_TEMPLATE },
|
|
329
|
+
{ role: "user", content: format(INSTANCE_TEMPLATE,
|
|
330
|
+
instruction: instruction,
|
|
331
|
+
system_information: uname,
|
|
332
|
+
macos_sed_note: uname.start_with?("Darwin") ? "\n#{MACOS_SED_NOTE}" : "") }
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
@steps = 0
|
|
336
|
+
@cost = 0.0
|
|
337
|
+
@cost_known = true
|
|
338
|
+
@totals = { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 }
|
|
339
|
+
@started_at = @clock.call
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def resume(history)
|
|
343
|
+
@messages = answered_messages(history.messages)
|
|
344
|
+
|
|
345
|
+
@steps = history.steps.to_i
|
|
346
|
+
@cost = history.cost_usd.to_f
|
|
347
|
+
@cost_known = !history.cost_usd.nil?
|
|
348
|
+
@totals = { input_tokens: history.input_tokens.to_i, output_tokens: history.output_tokens.to_i,
|
|
349
|
+
cached_tokens: history.cached_tokens.to_i, thinking_tokens: history.thinking_tokens.to_i }
|
|
350
|
+
@started_at = @clock.call - elapsed(history.messages)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# A turn whose calls did not all get a result cannot go back to a provider:
|
|
354
|
+
# it is dropped, and the model decides again from the tree it left.
|
|
355
|
+
def answered_messages(messages)
|
|
356
|
+
turn = messages.rindex { it[:role] == "assistant" && it[:tool_calls] }
|
|
357
|
+
return messages.dup unless turn
|
|
358
|
+
|
|
359
|
+
answered = messages[(turn + 1)..].filter_map { it[:tool_call_id] if it[:role] == "tool" }
|
|
360
|
+
messages[turn][:tool_calls].all? { answered.include?(it[:id]) } ? messages.dup : messages[0...turn]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def elapsed(messages)
|
|
364
|
+
times = messages.filter_map { Time.parse(it[:timestamp]) if it[:role] == "assistant" && it[:timestamp] }
|
|
365
|
+
times.empty? ? 0.0 : times.max - times.min
|
|
366
|
+
end
|
|
367
|
+
|
|
332
368
|
def execute(command)
|
|
333
369
|
environment.exec(command, timeout: exec_timeout, env: EXEC_ENV)
|
|
334
370
|
end
|
data/lib/miniswen/cli.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Miniswen
|
|
|
25
25
|
@jail = false
|
|
26
26
|
@allowed_hosts = nil
|
|
27
27
|
@workdir = nil
|
|
28
|
+
@history = nil
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def run
|
|
@@ -56,7 +57,7 @@ module Miniswen
|
|
|
56
57
|
agent = Agent.new(model:, reporter:, environment:, **options)
|
|
57
58
|
|
|
58
59
|
begin
|
|
59
|
-
result = agent.run(instruction)
|
|
60
|
+
result = agent.run(instruction, history: @history && Agent::Result.from_h(JSON.parse(@history)))
|
|
60
61
|
rescue StandardError => e
|
|
61
62
|
write_results(agent.partial_result(error_message(e)))
|
|
62
63
|
raise
|
|
@@ -105,7 +106,7 @@ module Miniswen
|
|
|
105
106
|
|
|
106
107
|
def parse_args!
|
|
107
108
|
parser = OptionParser.new do |opts|
|
|
108
|
-
opts.banner = "Usage: miniswen -m MODEL -p INSTRUCTION [...options]"
|
|
109
|
+
opts.banner = "Usage: miniswen -m MODEL (-p INSTRUCTION | --continue-from PATH) [...options]"
|
|
109
110
|
|
|
110
111
|
opts.on("-m MODEL", "--model=MODEL", String,
|
|
111
112
|
"LLM to use (litellm format, e.g.: openrouter/openai/gpt-5.6-luna") do |v|
|
|
@@ -116,6 +117,10 @@ module Miniswen
|
|
|
116
117
|
@instruction = File.file?(v) ? File.read(v) : v
|
|
117
118
|
end
|
|
118
119
|
|
|
120
|
+
opts.on("--continue-from=PATH", String, "Continue the session saved by --results-path at PATH (no -p needed)") do |v|
|
|
121
|
+
@history = File.read(v)
|
|
122
|
+
end
|
|
123
|
+
|
|
119
124
|
opts.on("--max-steps=STEPS", Integer, "Max steps count") do |v|
|
|
120
125
|
options[:max_steps] = v
|
|
121
126
|
end
|
|
@@ -197,7 +202,7 @@ module Miniswen
|
|
|
197
202
|
return if @refresh_registry
|
|
198
203
|
|
|
199
204
|
raise "Use -m to specify the model" unless @model
|
|
200
|
-
raise "Please, provide instructions via -p option" unless @instruction
|
|
205
|
+
raise "Please, provide instructions via -p option" unless @instruction || @history
|
|
201
206
|
end
|
|
202
207
|
end
|
|
203
208
|
end
|
|
@@ -20,7 +20,7 @@ module Miniswen
|
|
|
20
20
|
env&.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
|
|
21
21
|
argv << id
|
|
22
22
|
argv += [ "timeout", timeout.ceil.to_s ] if timeout&.positive?
|
|
23
|
-
argv += [ "
|
|
23
|
+
argv += [ "bash", "-c", command ]
|
|
24
24
|
|
|
25
25
|
Open3.popen2e(*argv) do |stdin, pipe, wait|
|
|
26
26
|
stdin.close
|
data/lib/miniswen/jail.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Miniswen
|
|
|
61
61
|
def spawn_arguments(command, env)
|
|
62
62
|
[ command_env(env),
|
|
63
63
|
"nsenter", "--target", @holder.pid.to_s, "--net", "--mount", "--pid=/proc/#{@holder.pid}/ns/pid_for_children", "--wd=#{@workdir}",
|
|
64
|
-
"--", "setpriv", "--bounding-set=-all", "--inh-caps=-all", "--no-new-privs", "--", "
|
|
64
|
+
"--", "setpriv", "--bounding-set=-all", "--inh-caps=-all", "--no-new-privs", "--", "bash", "-c", command ]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def spawn_options = super.merge(unsetenv_others: true)
|
data/lib/miniswen/local.rb
CHANGED
data/lib/miniswen/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lemans
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -95,6 +95,34 @@ dependencies:
|
|
|
95
95
|
- - "~>"
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
97
|
version: '1.1'
|
|
98
|
+
- !ruby/object:Gem::Dependency
|
|
99
|
+
name: rack
|
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - "~>"
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '3.1'
|
|
105
|
+
type: :runtime
|
|
106
|
+
prerelease: false
|
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - "~>"
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.1'
|
|
112
|
+
- !ruby/object:Gem::Dependency
|
|
113
|
+
name: rackup
|
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - "~>"
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '2.2'
|
|
119
|
+
type: :runtime
|
|
120
|
+
prerelease: false
|
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - "~>"
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '2.2'
|
|
98
126
|
- !ruby/object:Gem::Dependency
|
|
99
127
|
name: rexml
|
|
100
128
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -137,6 +165,20 @@ dependencies:
|
|
|
137
165
|
- - "~>"
|
|
138
166
|
- !ruby/object:Gem::Version
|
|
139
167
|
version: '1.5'
|
|
168
|
+
- !ruby/object:Gem::Dependency
|
|
169
|
+
name: webrick
|
|
170
|
+
requirement: !ruby/object:Gem::Requirement
|
|
171
|
+
requirements:
|
|
172
|
+
- - "~>"
|
|
173
|
+
- !ruby/object:Gem::Version
|
|
174
|
+
version: '1.9'
|
|
175
|
+
type: :runtime
|
|
176
|
+
prerelease: false
|
|
177
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
178
|
+
requirements:
|
|
179
|
+
- - "~>"
|
|
180
|
+
- !ruby/object:Gem::Version
|
|
181
|
+
version: '1.9'
|
|
140
182
|
- !ruby/object:Gem::Dependency
|
|
141
183
|
name: zeitwerk
|
|
142
184
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -159,6 +201,7 @@ email:
|
|
|
159
201
|
executables:
|
|
160
202
|
- lemans
|
|
161
203
|
- lemans-remote
|
|
204
|
+
- lemans-viewer
|
|
162
205
|
extensions: []
|
|
163
206
|
extra_rdoc_files: []
|
|
164
207
|
files:
|
|
@@ -167,6 +210,7 @@ files:
|
|
|
167
210
|
- README.md
|
|
168
211
|
- exe/lemans
|
|
169
212
|
- exe/lemans-remote
|
|
213
|
+
- exe/lemans-viewer
|
|
170
214
|
- lib/lemans.rb
|
|
171
215
|
- lib/lemans/agent.rb
|
|
172
216
|
- lib/lemans/agents.rb
|
|
@@ -213,6 +257,9 @@ files:
|
|
|
213
257
|
- lib/lemans/environments/daytona/shell.rb
|
|
214
258
|
- lib/lemans/environments/daytona/snapshot_store.rb
|
|
215
259
|
- lib/lemans/environments/docker.rb
|
|
260
|
+
- lib/lemans/environments/docker/proxy/Dockerfile
|
|
261
|
+
- lib/lemans/environments/docker/proxy/README.md
|
|
262
|
+
- lib/lemans/environments/docker/proxy/server.rb
|
|
216
263
|
- lib/lemans/ext/deep_merge.rb
|
|
217
264
|
- lib/lemans/result.rb
|
|
218
265
|
- lib/lemans/runner.rb
|
|
@@ -229,6 +276,7 @@ files:
|
|
|
229
276
|
- lib/lemans/trial/verifier.rb
|
|
230
277
|
- lib/lemans/trial/verifier/assets/eport-lemans.rb
|
|
231
278
|
- lib/lemans/trial/verifier/assets/lemans_minitest_reporter.rb
|
|
279
|
+
- lib/lemans/trial/verifier/test_scanner.rb
|
|
232
280
|
- lib/lemans/version.rb
|
|
233
281
|
- lib/miniswen.rb
|
|
234
282
|
- lib/miniswen/agent.rb
|