lemans 1.3.3 → 1.3.4
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 +7 -0
- data/README.md +1 -0
- data/lib/lemans/agents/miniswen_installed.rb +1 -1
- data/lib/lemans/cli/templates/bench/environment/Dockerfile +2 -2
- data/lib/lemans/environments/docker.rb +1 -0
- data/lib/lemans/trial/snapshot.rb +4 -4
- data/lib/lemans/trial/verifier/assets/lemans_minitest_reporter.rb +9 -0
- data/lib/lemans/trial/verifier.rb +1 -7
- data/lib/lemans/version.rb +1 -1
- data/lib/miniswen/cli.rb +10 -0
- data/lib/miniswen/jail.rb +49 -0
- data/lib/miniswen/local.rb +7 -1
- data/lib/miniswen/ruby_llm.rb +2 -2
- data/lib/miniswen/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7480cc7d9a06ab84c2abb8ab93839d4842a6cc8bc3b709f74b9c67af20fa865
|
|
4
|
+
data.tar.gz: 9c60c87f53394ca714a4ca418fb44666ed68a48083530c937117e21a5f018ab6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1eea499a9c9a3acad2801809d62529e3d55d571d3f78202b1656da39d105c9e38fdae06850e0134bcc5a128530695e900833a2ebffcb5f98b7bad7911c70388a
|
|
7
|
+
data.tar.gz: 4214dec819269946fd6972b5d6011f2bbf700faeffdd3a65883c8d87722fbb2ad92c7c622d1e9d74193f56f573868787c195c8413c64fb80d07160f2d7a79762
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.3.4] - 2026-09-15
|
|
4
|
+
|
|
5
|
+
- Miniswen: `--jail` runs every command in its own namespaces: none of the harness's environment, no network, read-only system, none of its files. `miniswen-installed` always runs jailed.
|
|
6
|
+
- Docker: containers start with SYS_ADMIN, NET_ADMIN and AppArmor unconfined, which the jail needs.
|
|
7
|
+
- Miniswen: retry model calls for ~10 min instead of ~5.
|
|
8
|
+
- Verifier: a failed restore raises an infrastructure error instead of scoring 0. Also, to be tamper-proof, the reporter aborts the run if the agent patched Minitest so tests cannot fail.
|
|
9
|
+
|
|
3
10
|
## [1.3.3] - 2026-09-11
|
|
4
11
|
|
|
5
12
|
- Daytona: retry sandbox creation when the SDK gives up on a stalled start (the half-made sandbox is adopted or deleted first).
|
data/README.md
CHANGED
|
@@ -15,6 +15,7 @@ lemans is a harness for benchmarking coding agents, the Ruby way:
|
|
|
15
15
|
- Ruby 3.4+ is required to run `lemans`
|
|
16
16
|
- Daytona account (API token) or Docker (for local sandboxes)
|
|
17
17
|
- Some LLM provider/proxy credentials (e.g., OpenRouter)
|
|
18
|
+
- iproute2 in the sandbox image, so a jailed agent keeps loopback while cut off the internet
|
|
18
19
|
|
|
19
20
|
## Getting started
|
|
20
21
|
|
|
@@ -60,7 +60,7 @@ module Lemans
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def command_for(task)
|
|
63
|
-
argv = [ "miniswen", "-q", "--no-refresh-registry",
|
|
63
|
+
argv = [ "miniswen", "-q", "--no-refresh-registry", "--jail",
|
|
64
64
|
"-m", model.to_s, "-p", task.instruction,
|
|
65
65
|
"--results-path", RESULTS_PATH,
|
|
66
66
|
"--max-steps", profile.step_limit, "--max-time", profile.timeout.to_i,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
FROM ruby:4-slim
|
|
2
2
|
|
|
3
|
-
# The harness seals, snapshots, and grades through git.
|
|
3
|
+
# The harness seals, snapshots, and grades through git. The agent's jail brings loopback up with ip.
|
|
4
4
|
RUN apt-get update \
|
|
5
|
-
&& apt-get install -y --no-install-recommends git ca-certificates \
|
|
5
|
+
&& apt-get install -y --no-install-recommends git ca-certificates iproute2 \
|
|
6
6
|
&& rm -rf /var/lib/apt/lists/*
|
|
7
7
|
|
|
8
8
|
RUN gem install minitest --no-document
|
|
@@ -102,6 +102,7 @@ module Lemans
|
|
|
102
102
|
def run_args
|
|
103
103
|
args = [ "--detach", "--init", "--name", @name,
|
|
104
104
|
"--cpus", resources.cpus.to_s, "--memory", "#{resources.memory}m",
|
|
105
|
+
"--cap-add", "SYS_ADMIN", "--cap-add", "NET_ADMIN", "--security-opt", "apparmor=unconfined",
|
|
105
106
|
"--entrypoint", "sh" ]
|
|
106
107
|
args += [ "--network", "none" ] if network.none?
|
|
107
108
|
env.each { |key, value| args += [ "--env", "#{key}=#{value}" ] }
|
|
@@ -36,17 +36,17 @@ module Lemans
|
|
|
36
36
|
@baseline = tree
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def restore!
|
|
40
|
-
return
|
|
39
|
+
def restore!
|
|
40
|
+
return if paths.empty?
|
|
41
41
|
raise VerifierError, "restore is declared but no baseline was sealed" unless baseline
|
|
42
42
|
|
|
43
43
|
escaped_paths = Shellwords.join(paths)
|
|
44
44
|
|
|
45
|
-
environment.exec(
|
|
45
|
+
environment.exec!(
|
|
46
46
|
"cd #{Shellwords.escape(workdir)} && #{git} cat-file -e #{baseline} && " \
|
|
47
47
|
"rm -rf -- #{escaped_paths} && #{git} checkout #{baseline} -- #{escaped_paths}",
|
|
48
48
|
timeout:
|
|
49
|
-
)
|
|
49
|
+
)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
private
|
|
@@ -36,6 +36,8 @@ module LemansReport
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def report
|
|
39
|
+
abort "lemans: a false assertion no longer raises, so no result can be trusted" if tampered?
|
|
40
|
+
|
|
39
41
|
graded = @results.select { graded?(it) }
|
|
40
42
|
prior = existing.fetch("checks", {})
|
|
41
43
|
return if graded.empty? && prior.empty?
|
|
@@ -63,6 +65,13 @@ module LemansReport
|
|
|
63
65
|
|
|
64
66
|
private
|
|
65
67
|
|
|
68
|
+
def tampered?
|
|
69
|
+
Minitest::Test.new("probe").assert(false)
|
|
70
|
+
true
|
|
71
|
+
rescue Minitest::Assertion
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
def graded?(result)
|
|
67
76
|
dir = ENV["TESTS"]
|
|
68
77
|
# The trailing slash matters: /testsuite must not count as /tests.
|
|
@@ -22,11 +22,6 @@ module Lemans
|
|
|
22
22
|
|
|
23
23
|
VERIFY_BIN = "verify"
|
|
24
24
|
|
|
25
|
-
# The message a person finds where the suite output would have been.
|
|
26
|
-
TAMPERED = "The graded surfaces could not be restored from the sealed baseline: the sandbox no " \
|
|
27
|
-
"longer holds the tree sealed before the agent's first turn. Removing or rewriting " \
|
|
28
|
-
"it is a failed check, so this run scores 0.\n"
|
|
29
|
-
|
|
30
25
|
private attr_reader :task, :environment, :snapshot, :timeout
|
|
31
26
|
|
|
32
27
|
def initialize(task, environment, snapshot)
|
|
@@ -40,8 +35,7 @@ module Lemans
|
|
|
40
35
|
upload_tests!
|
|
41
36
|
prepare_env!
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
return Verification.new(reward: 0.0, credit: 0.0, logs: TAMPERED) unless snapshot.restore!
|
|
38
|
+
snapshot.restore!
|
|
45
39
|
|
|
46
40
|
verification = run_tests!
|
|
47
41
|
|
data/lib/lemans/version.rb
CHANGED
data/lib/miniswen/cli.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Miniswen
|
|
|
22
22
|
@atif_path = nil
|
|
23
23
|
@refresh_registry = false
|
|
24
24
|
@skip_registry_refresh = false
|
|
25
|
+
@jail = false
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def run
|
|
@@ -42,6 +43,9 @@ module Miniswen
|
|
|
42
43
|
if @docker_id
|
|
43
44
|
require "miniswen/environment/docker"
|
|
44
45
|
Environment::Docker.new(@docker_id)
|
|
46
|
+
elsif @jail
|
|
47
|
+
require "miniswen/jail"
|
|
48
|
+
Jail.new.start
|
|
45
49
|
else
|
|
46
50
|
Local.new
|
|
47
51
|
end
|
|
@@ -53,6 +57,8 @@ module Miniswen
|
|
|
53
57
|
rescue StandardError => e
|
|
54
58
|
write_results(agent.partial_result(error_message(e)))
|
|
55
59
|
raise
|
|
60
|
+
ensure
|
|
61
|
+
environment.stop
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
write_results(result)
|
|
@@ -151,6 +157,10 @@ module Miniswen
|
|
|
151
157
|
@docker_id = v
|
|
152
158
|
end
|
|
153
159
|
|
|
160
|
+
opts.on("--jail", "Run every command in its own namespaces: none of the harness's environment, no network, read-only system, none of its files") do
|
|
161
|
+
@jail = true
|
|
162
|
+
end
|
|
163
|
+
|
|
154
164
|
opts.on("--refresh-registry", "Refresh the model registry, persist it, and exit") do
|
|
155
165
|
@refresh_registry = true
|
|
156
166
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "miniswen/local"
|
|
4
|
+
|
|
5
|
+
module Miniswen
|
|
6
|
+
# Runs every command in its own namespaces: none of the harness's environment,
|
|
7
|
+
# no network, read-only system, none of its files.
|
|
8
|
+
class Jail < Local
|
|
9
|
+
SETUP = <<~SH
|
|
10
|
+
set -eu
|
|
11
|
+
ip link set lo up
|
|
12
|
+
for dir in /usr /etc /opt; do mount -o bind,ro "$dir" "$dir"; done
|
|
13
|
+
for dir in /tmp /run /root; do mkdir -p "/var/lib/miniswen$dir" && mount --bind "/var/lib/miniswen$dir" "$dir"; done
|
|
14
|
+
echo ready
|
|
15
|
+
exec sleep infinity
|
|
16
|
+
SH
|
|
17
|
+
|
|
18
|
+
def initialize(workdir: Dir.pwd)
|
|
19
|
+
@workdir = workdir
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
_, @stdout, @stderr, @holder = Open3.popen3(
|
|
24
|
+
"unshare", "--net", "--mount", "--pid", "--fork", "--kill-child", "--mount-proc", "sh", "-c", SETUP, pgroup: true
|
|
25
|
+
)
|
|
26
|
+
return self if @stdout.gets == "ready\n"
|
|
27
|
+
|
|
28
|
+
raise InfrastructureError, "jail did not start: #{@stderr.read}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stop
|
|
32
|
+
Process.kill(:KILL, -@holder.pid) if @holder.alive?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def spawn_arguments(command, env)
|
|
38
|
+
[ ENV.to_h.merge(env.to_h).slice(*container_variables),
|
|
39
|
+
"nsenter", "--target", @holder.pid.to_s, "--net", "--mount", "--pid=/proc/#{@holder.pid}/ns/pid_for_children", "--wd=#{@workdir}",
|
|
40
|
+
"--", "setpriv", "--bounding-set=-all", "--inh-caps=-all", "--no-new-privs", "--", "sh", "-c", command ]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def spawn_options = super.merge(unsetenv_others: true)
|
|
44
|
+
|
|
45
|
+
def container_variables
|
|
46
|
+
@container_variables ||= File.read("/proc/1/environ").split("\0").map { it.split("=").first } | Agent::EXEC_ENV.keys
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/miniswen/local.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Miniswen
|
|
|
12
12
|
# Always through a shell: Ruby execs a metacharacter-free string directly,
|
|
13
13
|
# and a missing binary would then raise ENOENT here instead of exiting 127.
|
|
14
14
|
def exec(command, timeout: nil, env: nil)
|
|
15
|
-
Open3.popen2e(
|
|
15
|
+
Open3.popen2e(*spawn_arguments(command, env), **spawn_options) do |stdin, io, wait_thr|
|
|
16
16
|
stdin.close
|
|
17
17
|
reader = Thread.new { io.read }
|
|
18
18
|
|
|
@@ -27,8 +27,14 @@ module Miniswen
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def stop = nil
|
|
31
|
+
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
def spawn_arguments(command, env) = [ env || {}, "sh", "-c", command ]
|
|
35
|
+
|
|
36
|
+
def spawn_options = { pgroup: true }
|
|
37
|
+
|
|
32
38
|
def kill_group(pid)
|
|
33
39
|
Process.kill(:KILL, -pid)
|
|
34
40
|
rescue Errno::ESRCH, Errno::EPERM
|
data/lib/miniswen/ruby_llm.rb
CHANGED
|
@@ -8,10 +8,10 @@ RubyLLM.configure do |config|
|
|
|
8
8
|
config.logger = Logger.new(IO::NULL) unless ENV["MINISWEN_DEBUG"] == "1"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
# About
|
|
11
|
+
# About ten minutes of retries (1, 2, 4, ... 256s plus jitter): provider
|
|
12
12
|
# outages and rate-limit windows outlast the minute this used to allow.
|
|
13
13
|
RubyLLM.configure do |config|
|
|
14
|
-
config.max_retries =
|
|
14
|
+
config.max_retries = 9
|
|
15
15
|
config.retry_interval = 1
|
|
16
16
|
end
|
|
17
17
|
|
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.3.
|
|
4
|
+
version: 1.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -236,6 +236,7 @@ files:
|
|
|
236
236
|
- lib/miniswen/cli/reporter.rb
|
|
237
237
|
- lib/miniswen/environment.rb
|
|
238
238
|
- lib/miniswen/environment/docker.rb
|
|
239
|
+
- lib/miniswen/jail.rb
|
|
239
240
|
- lib/miniswen/local.rb
|
|
240
241
|
- lib/miniswen/ruby_llm.rb
|
|
241
242
|
- lib/miniswen/testing.rb
|