Lars Talian commited on
Commit
b33db9f
·
unverified ·
1 Parent(s): eddb429

Enforce strict live admission defaults in managed runtime (#90)

Browse files
Dockerfile CHANGED
@@ -86,7 +86,8 @@ ENV PYTHONPATH="/app/env/src:/app/env"
86
  ENV OPENRANGE_EXECUTION_MODE=subprocess
87
  # Enable the managed runtime so reset() boots real services from the manifest
88
  ENV OPENRANGE_RUNTIME_MANIFEST=manifests/tier1_basic.yaml
89
- ENV OPENRANGE_RUNTIME_VALIDATOR_PROFILE=offline
 
90
  ENV OPENRANGE_SNAPSHOT_POOL_SIZE=1
91
  # Enable the OpenEnv Gradio web interface at /web
92
  ENV ENABLE_WEB_INTERFACE=true
 
86
  ENV OPENRANGE_EXECUTION_MODE=subprocess
87
  # Enable the managed runtime so reset() boots real services from the manifest
88
  ENV OPENRANGE_RUNTIME_MANIFEST=manifests/tier1_basic.yaml
89
+ ENV OPENRANGE_RUNTIME_VALIDATOR_PROFILE=training
90
+ ENV OPENRANGE_ENABLE_LIVE_ADMISSION=1
91
  ENV OPENRANGE_SNAPSHOT_POOL_SIZE=1
92
  # Enable the OpenEnv Gradio web interface at /web
93
  ENV ENABLE_WEB_INTERFACE=true
README.md CHANGED
@@ -100,7 +100,19 @@ uv run pytest tests/ -v --tb=short
100
 
101
  The deployed package exposes the standard OpenEnv `reset()`, `step()`, and `state()` contract through `server.app:app`, which is the entrypoint referenced by `openenv.yaml`.
102
 
103
- **Validator** — Admission gate for candidate snapshots. The shipped runtime first enforces manifest compliance plus graph-native checks such as graph consistency, path solvability, evidence sufficiency, and reward grounding before structural/task checks. When `OPENRANGE_ENABLE_LIVE_ADMISSION=1`, the runtime also boots the rendered child bundle, applies rendered payload files, constructs a real `ContainerSet`, and runs live build/exploit/evidence/reward checks before admission. Public/HF mode can still rely on a prebuilt admitted pool with live admission disabled.
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  **Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` asks the shared runtime for a frozen admitted snapshot. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
106
 
 
100
 
101
  The deployed package exposes the standard OpenEnv `reset()`, `step()`, and `state()` contract through `server.app:app`, which is the entrypoint referenced by `openenv.yaml`.
102
 
103
+ **Validator** — Admission gate for candidate snapshots. The shipped runtime enforces manifest compliance plus graph-native checks such as graph consistency, path solvability, evidence sufficiency, and reward grounding before structural/task checks. With the `training` profile, the runtime boots rendered bundles, applies payload files, constructs a real `ContainerSet`, and runs live build/exploit/patch/evidence/reward/isolation/difficulty/NPC/realism checks before admission.
104
+
105
+ Validator profile matrix:
106
+
107
+ | Profile | Checks | Guarantees |
108
+ |---------|--------|------------|
109
+ | `offline` | Graph + structural/task checks only (no live containers) | Fast static admission only; no live exploitability/patchability guarantee |
110
+ | `training` | `offline` checks + live/container-backed checks | Full admission guarantees for managed training/runtime use |
111
+
112
+ Managed runtime defaults and safety behavior:
113
+ - `OPENRANGE_RUNTIME_VALIDATOR_PROFILE` defaults to `training`.
114
+ - `OPENRANGE_ENABLE_LIVE_ADMISSION` defaults to `1`.
115
+ - If managed runtime is configured non-live (`offline` profile and/or live admission disabled), startup raises an error unless you explicitly opt out with `OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1`, in which case a warning is emitted.
116
 
117
  **Environment** — `RangeEnvironment(Environment)` following the OpenEnv contract. `reset()` asks the shared runtime for a frozen admitted snapshot. `step(action)` routes commands to the appropriate container — Red runs on the attacker box, Blue runs on the SIEM. No artificial command allowlists; the container's installed tools are the constraint.
118
 
src/open_range/server/runtime.py CHANGED
@@ -59,6 +59,9 @@ from open_range.validator.validator import ValidationResult, ValidatorGate
59
  logger = logging.getLogger(__name__)
60
 
61
  _DEFAULT_MANIFEST = ("manifests", "tier1_basic.yaml")
 
 
 
62
  _VALIDATOR_PROFILE_ALIASES = {
63
  "light": "offline",
64
  "static": "offline",
@@ -375,6 +378,19 @@ def _build_validator(profile: str, manifest: dict[str, Any]) -> ValidatorGate:
375
  )
376
 
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  def _default_live_validator(*, include_patchability: bool = False) -> ValidatorGate:
379
  checks = [
380
  BuildBootCheck(),
@@ -464,20 +480,40 @@ class ManagedSnapshotRuntime:
464
 
465
  @classmethod
466
  def from_env(cls) -> "ManagedSnapshotRuntime":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  return cls(
468
  manifest_path=os.getenv("OPENRANGE_RUNTIME_MANIFEST"),
469
  store_dir=os.getenv("OPENRANGE_SNAPSHOT_DIR"),
470
- validator_profile=os.getenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", "offline"),
471
  pool_size=_env_int("OPENRANGE_SNAPSHOT_POOL_SIZE", 3),
472
  selection_strategy=os.getenv("OPENRANGE_SNAPSHOT_SELECTION", "random"),
473
  parent_selection_strategy=os.getenv("OPENRANGE_PARENT_SELECTION", "policy"),
474
  refill_enabled=_env_flag("OPENRANGE_ENABLE_MANAGED_REFILL", default=False),
475
  refill_interval_s=float(os.getenv("OPENRANGE_REFILL_INTERVAL_S", "2.0")),
476
  generation_retries=_env_int("OPENRANGE_GENERATION_RETRIES", 3),
477
- live_admission_enabled=_env_flag(
478
- "OPENRANGE_ENABLE_LIVE_ADMISSION",
479
- default=False,
480
- ),
481
  teardown_booted_projects=not _env_flag(
482
  "OPENRANGE_KEEP_BOOTED_VALIDATION_STACKS",
483
  default=False,
 
59
  logger = logging.getLogger(__name__)
60
 
61
  _DEFAULT_MANIFEST = ("manifests", "tier1_basic.yaml")
62
+ _DEFAULT_MANAGED_VALIDATOR_PROFILE = "training"
63
+ _DEFAULT_MANAGED_LIVE_ADMISSION = True
64
+ _ALLOW_NON_LIVE_ADMISSION_ENV = "OPENRANGE_ALLOW_NON_LIVE_ADMISSION"
65
  _VALIDATOR_PROFILE_ALIASES = {
66
  "light": "offline",
67
  "static": "offline",
 
378
  )
379
 
380
 
381
+ def _strict_admission_enabled(profile: str, live_admission_enabled: bool) -> bool:
382
+ return profile in _LIVE_VALIDATOR_PROFILES and live_admission_enabled
383
+
384
+
385
+ def _managed_admission_failure_message(profile: str, live_admission_enabled: bool) -> str:
386
+ return (
387
+ "Managed runtime requires strict live admission "
388
+ f"(validator_profile='training', live_admission_enabled=1). "
389
+ f"Current configuration: validator_profile={profile!r}, "
390
+ f"live_admission_enabled={live_admission_enabled!r}."
391
+ )
392
+
393
+
394
  def _default_live_validator(*, include_patchability: bool = False) -> ValidatorGate:
395
  checks = [
396
  BuildBootCheck(),
 
480
 
481
  @classmethod
482
  def from_env(cls) -> "ManagedSnapshotRuntime":
483
+ profile = _normalize_validator_profile(
484
+ os.getenv(
485
+ "OPENRANGE_RUNTIME_VALIDATOR_PROFILE",
486
+ _DEFAULT_MANAGED_VALIDATOR_PROFILE,
487
+ )
488
+ )
489
+ live_admission_enabled = _env_flag(
490
+ "OPENRANGE_ENABLE_LIVE_ADMISSION",
491
+ default=_DEFAULT_MANAGED_LIVE_ADMISSION,
492
+ )
493
+ if not _strict_admission_enabled(profile, live_admission_enabled):
494
+ message = _managed_admission_failure_message(profile, live_admission_enabled)
495
+ if _env_flag(_ALLOW_NON_LIVE_ADMISSION_ENV, default=False):
496
+ logger.warning(
497
+ "%s Explicit opt-out enabled via %s=1.",
498
+ message,
499
+ _ALLOW_NON_LIVE_ADMISSION_ENV,
500
+ )
501
+ else:
502
+ raise RuntimeError(
503
+ f"{message} Set {_ALLOW_NON_LIVE_ADMISSION_ENV}=1 to explicitly opt out."
504
+ )
505
+
506
  return cls(
507
  manifest_path=os.getenv("OPENRANGE_RUNTIME_MANIFEST"),
508
  store_dir=os.getenv("OPENRANGE_SNAPSHOT_DIR"),
509
+ validator_profile=profile,
510
  pool_size=_env_int("OPENRANGE_SNAPSHOT_POOL_SIZE", 3),
511
  selection_strategy=os.getenv("OPENRANGE_SNAPSHOT_SELECTION", "random"),
512
  parent_selection_strategy=os.getenv("OPENRANGE_PARENT_SELECTION", "policy"),
513
  refill_enabled=_env_flag("OPENRANGE_ENABLE_MANAGED_REFILL", default=False),
514
  refill_interval_s=float(os.getenv("OPENRANGE_REFILL_INTERVAL_S", "2.0")),
515
  generation_retries=_env_int("OPENRANGE_GENERATION_RETRIES", 3),
516
+ live_admission_enabled=live_admission_enabled,
 
 
 
517
  teardown_booted_projects=not _env_flag(
518
  "OPENRANGE_KEEP_BOOTED_VALIDATION_STACKS",
519
  default=False,
tests/test_runtime.py CHANGED
@@ -3,9 +3,11 @@
3
  from __future__ import annotations
4
 
5
  import json
 
6
  from pathlib import Path
7
 
8
  import pytest
 
9
 
10
  from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
11
  from open_range.server.compose_runner import BootedSnapshotProject
@@ -15,6 +17,62 @@ from open_range.validator.validator import ValidationResult
15
 
16
 
17
  class TestManagedSnapshotRuntime:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def test_offline_validator_profile_includes_static_checks(self, tier1_manifest, tmp_path):
19
  runtime = ManagedSnapshotRuntime(
20
  manifest=tier1_manifest,
 
3
  from __future__ import annotations
4
 
5
  import json
6
+ import logging
7
  from pathlib import Path
8
 
9
  import pytest
10
+ import yaml
11
 
12
  from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
13
  from open_range.server.compose_runner import BootedSnapshotProject
 
17
 
18
 
19
  class TestManagedSnapshotRuntime:
20
+ def test_from_env_defaults_to_training_and_live(self, tier1_manifest, tmp_path, monkeypatch):
21
+ manifest_path = tmp_path / "manifest.yaml"
22
+ manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
23
+
24
+ monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
25
+ monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
26
+ monkeypatch.delenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", raising=False)
27
+ monkeypatch.delenv("OPENRANGE_ENABLE_LIVE_ADMISSION", raising=False)
28
+ monkeypatch.delenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", raising=False)
29
+
30
+ runtime = ManagedSnapshotRuntime.from_env()
31
+ assert runtime.validator_profile == "training"
32
+ assert runtime.live_admission_enabled is True
33
+
34
+ def test_from_env_rejects_non_live_without_explicit_opt_out(
35
+ self,
36
+ tier1_manifest,
37
+ tmp_path,
38
+ monkeypatch,
39
+ ):
40
+ manifest_path = tmp_path / "manifest.yaml"
41
+ manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
42
+
43
+ monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
44
+ monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
45
+ monkeypatch.setenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", "offline")
46
+ monkeypatch.setenv("OPENRANGE_ENABLE_LIVE_ADMISSION", "0")
47
+ monkeypatch.delenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", raising=False)
48
+
49
+ with pytest.raises(RuntimeError, match="OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1"):
50
+ ManagedSnapshotRuntime.from_env()
51
+
52
+ def test_from_env_allows_non_live_with_explicit_opt_out_warning(
53
+ self,
54
+ tier1_manifest,
55
+ tmp_path,
56
+ monkeypatch,
57
+ caplog,
58
+ ):
59
+ manifest_path = tmp_path / "manifest.yaml"
60
+ manifest_path.write_text(yaml.safe_dump(tier1_manifest), encoding="utf-8")
61
+
62
+ monkeypatch.setenv("OPENRANGE_RUNTIME_MANIFEST", str(manifest_path))
63
+ monkeypatch.setenv("OPENRANGE_SNAPSHOT_DIR", str(tmp_path / "snapshots"))
64
+ monkeypatch.setenv("OPENRANGE_RUNTIME_VALIDATOR_PROFILE", "offline")
65
+ monkeypatch.setenv("OPENRANGE_ENABLE_LIVE_ADMISSION", "0")
66
+ monkeypatch.setenv("OPENRANGE_ALLOW_NON_LIVE_ADMISSION", "1")
67
+
68
+ with caplog.at_level(logging.WARNING):
69
+ runtime = ManagedSnapshotRuntime.from_env()
70
+
71
+ assert runtime.validator_profile == "offline"
72
+ assert runtime.live_admission_enabled is False
73
+ assert "strict live admission" in caplog.text
74
+ assert "OPENRANGE_ALLOW_NON_LIVE_ADMISSION=1" in caplog.text
75
+
76
  def test_offline_validator_profile_includes_static_checks(self, tier1_manifest, tmp_path):
77
  runtime = ManagedSnapshotRuntime(
78
  manifest=tier1_manifest,