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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08d62b21b79425a5c1e99867270b4cf2dd41ccb07f8ab7f5e5381a416170907f'
4
- data.tar.gz: 802e7e3f665913b95a5409882f10b067f4118efe1e2de650a07dd93382f035af
3
+ metadata.gz: b7480cc7d9a06ab84c2abb8ab93839d4842a6cc8bc3b709f74b9c67af20fa865
4
+ data.tar.gz: 9c60c87f53394ca714a4ca418fb44666ed68a48083530c937117e21a5f018ab6
5
5
  SHA512:
6
- metadata.gz: fabad5219961a7514c44f888bd79713db444a1dc5b8b20c6153ae1e55c4252cc4065939bb6671a4e6511a1809f4b89da00ff2a642825348c184969fdf49a7c15
7
- data.tar.gz: '08a08843cb79dcb1da14e27b1b11d2126fe0d8049dc7cdbcccb573c0c93d81f44613ed6f4e905ec23ab3e9f41e2f6eac44a49e9d520c50ebfca3f82a6d9f4f5f'
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! # rubocop:disable Naming/PredicateMethod
40
- return true if paths.empty?
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
- ).success?
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
- # A baseline the agent made unrestorable is a verdict, not an error.
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lemans
4
- VERSION = "1.3.3"
4
+ VERSION = "1.3.4"
5
5
  end
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
@@ -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(env || {}, "sh", "-c", command, pgroup: true) do |stdin, io, wait_thr|
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
@@ -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 five minutes of retries (1, 2, 4, ... 128s plus jitter): provider
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 = 8
14
+ config.max_retries = 9
15
15
  config.retry_interval = 1
16
16
  end
17
17
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Miniswen
4
- VERSION = "1.3.3"
4
+ VERSION = "1.3.4"
5
5
  end
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.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