Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions src/openenv/core/harness/capture/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1376,9 +1376,9 @@ def main() -> None:
)
parser.add_argument(
"--host",
default="0.0.0.0",
help="bind address (default: 0.0.0.0). Non-loopback binds mint an admin key when none is "
"set so /sessions* is never left open on a reachable interface.",
default="127.0.0.1",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / non-blocking. This flips the standalone CLI's default bind from 0.0.0.0 to loopback-only 127.0.0.1 — a sensible secure-by-default that aligns with the "network access must be explicitly configured" invariant.

Do note it's a behavior change for anyone running python -m openenv.core.harness.capture.server who relied on the all-interfaces default (e.g. inside a container); they'll now need --host 0.0.0.0 --admin-key ... explicitly. Pre-1.0 breaking changes are acceptable per INVARIANTS.md, so no code change required — just worth a release-note callout.

help="bind address (default: 127.0.0.1). Non-loopback binds require --admin-key or "
"$OPENENV_CAPTURE_ADMIN_KEY so /sessions* is never left open on a reachable interface.",
)
parser.add_argument("--port", type=int, default=8100)
parser.add_argument(
Expand Down Expand Up @@ -1415,8 +1415,7 @@ def main() -> None:
"--admin-key",
default=os.environ.get("OPENENV_CAPTURE_ADMIN_KEY", ""),
help="key the session-management routes (/sessions*) require (defaults to "
"$OPENENV_CAPTURE_ADMIN_KEY). Required for non-loopback binds: if unset there, a random "
"key is minted and printed. Loopback-only binds may leave it unset.",
"$OPENENV_CAPTURE_ADMIN_KEY). Required for non-loopback binds. Never printed or logged.",
)
args = parser.parse_args()

Expand All @@ -1443,15 +1442,14 @@ def main() -> None:
for fix in report.param_fixes:
print(f" upstream compat: {fix}")

# Flag/env win; otherwise mint when the bind is reachable from outside. Leaving the key unset
# with --host 0.0.0.0 (the CLI default) would publish /sessions* ungated — the same open
# control plane `run_batch` already refuses. Loopback stays fail-open for private local use.
# Flag/env only — never mint-and-print (that would log a credential). Loopback may leave the
# key unset for private local use; non-loopback must supply one explicitly.
admin_key = args.admin_key or None
if not admin_key and not _is_loopback_host(args.host):
admin_key = secrets.token_urlsafe(32)
print(
f"capture admin key (minted for --host={args.host}; "
f"set --admin-key or $OPENENV_CAPTURE_ADMIN_KEY to pin): {admin_key}"
raise SystemExit(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / non-blocking. This admin-key precondition runs after the validate_llm upstream probe above (the if not level: block), so a non-loopback invocation without a key will make a network round-trip and print the capture level before failing here. Consider moving this check to just after args = parser.parse_args() so a config error fails fast before any network call.

Note the tests pass --capture-level, which bypasses the probe, so this ordering isn't currently exercised.

f"--admin-key or $OPENENV_CAPTURE_ADMIN_KEY is required when --host={args.host} "
"(non-loopback bind would publish /sessions* ungated). "
"Pass --host 127.0.0.1 for a private local port, or set a key."
)

uvicorn.run(
Expand Down
28 changes: 15 additions & 13 deletions tests/envs/test_harbor_capture_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,29 +171,31 @@ def test_cli_admin_key_defaults_to_the_env_var(monkeypatch):
assert seen["admin_key"] == "from-env"


def test_cli_mints_admin_key_for_default_non_loopback_host(monkeypatch):
"""Default `--host 0.0.0.0` must never leave `/sessions*` ungated."""
def test_cli_default_loopback_leaves_admin_key_unset(monkeypatch, capsys):
"""Default `--host 127.0.0.1` may leave `/sessions*` ungated on a private local port."""
monkeypatch.delenv("OPENENV_CAPTURE_ADMIN_KEY", raising=False)

seen = run_cli(monkeypatch)

assert seen["admin_key"], "default non-loopback bind must mint an admin key"
assert len(seen["admin_key"]) >= 32
assert seen["admin_key"] is None
out = capsys.readouterr().out
assert "admin key" not in out.lower()


def test_cli_mints_admin_key_for_explicit_non_loopback_host(monkeypatch):
def test_cli_refuses_non_loopback_without_admin_key(monkeypatch):
"""Non-loopback without a key must fail closed — never mint-and-print a credential."""
monkeypatch.delenv("OPENENV_CAPTURE_ADMIN_KEY", raising=False)

seen = run_cli(monkeypatch, "--host", "0.0.0.0")

assert seen["admin_key"]
assert len(seen["admin_key"]) >= 32
with pytest.raises(SystemExit, match="OPENENV_CAPTURE_ADMIN_KEY"):
run_cli(monkeypatch, "--host", "0.0.0.0")


def test_cli_leaves_admin_key_unset_on_loopback_without_flag_or_env(monkeypatch):
"""A private loopback port stays as convenient as before; no key is minted."""
def test_cli_non_loopback_accepts_explicit_admin_key(monkeypatch, capsys):
monkeypatch.delenv("OPENENV_CAPTURE_ADMIN_KEY", raising=False)

seen = run_cli(monkeypatch, "--host", "127.0.0.1")
seen = run_cli(monkeypatch, "--host", "0.0.0.0", "--admin-key", "explicit")

assert seen["admin_key"] is None
assert seen["admin_key"] == "explicit"
out = capsys.readouterr().out
assert "explicit" not in out
assert "admin key" not in out.lower()