Skip to content
Closed
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
11 changes: 8 additions & 3 deletions src/dstack/_internal/cli/services/presets/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from dstack._internal.core.errors import CLIError
from dstack._internal.core.models.common import validate_extra_ignore
from dstack._internal.core.models.configurations import PresetConfiguration
from dstack._internal.core.models.envs import EnvSentinel
from dstack._internal.utils.common import get_dstack_dir

if TYPE_CHECKING:
Expand Down Expand Up @@ -300,9 +301,13 @@ def create_preset_session(
)
)
record = configuration.model_dump(mode="json", exclude_none=True)
# Env values may be secrets: the session records only the variable names,
# which read back as passthrough references resolved from the environment.
record["env"] = list(configuration.env)
# Keep literal values so a resumed session does not depend on the
# caller's current environment. Passthrough entries remain names and are
# resolved again on resume. The session manifest is written mode 0600.
record["env"] = [
key if isinstance(value, EnvSentinel) else f"{key}={value}"
for key, value in configuration.env.items()
]
if not configuration.env:
record.pop("env")
_write_private_text(
Expand Down
12 changes: 9 additions & 3 deletions src/tests/_internal/cli/services/presets/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
get_claude_auth,
run_preset_agent,
)
from dstack._internal.cli.services.presets.create import load_session_configuration
from dstack._internal.cli.services.presets.redaction import (
contains_redacted_value,
redact,
Expand All @@ -49,6 +50,7 @@
from dstack._internal.compat import IS_WINDOWS
from dstack._internal.core.errors import CLIError
from dstack._internal.core.models.configurations import PresetConfiguration
from dstack._internal.core.models.envs import EnvSentinel
from dstack._internal.core.services.configs import ConfigManager
from tests._internal.cli.common import get_session_run, get_session_state

Expand Down Expand Up @@ -223,15 +225,19 @@ def test_creates_private_session_with_log_and_manifest(self, tmp_path, monkeypat
assert session.path.stat().st_mode & 0o777 == 0o700
assert session.log_path.stat().st_mode & 0o777 == 0o600

def test_session_saves_scrubbed_configuration(self, tmp_path, monkeypatch):
def test_session_saves_resumable_configuration(self, tmp_path, monkeypatch):
self._home(tmp_path, monkeypatch)

session = create_preset_session(self._configuration(), previous=())

data = yaml.safe_load((session.path / "preset.dstack.yml").read_text())
assert data["max_price"] == 0.5
assert data["env"] == ["HF_TOKEN", "TOKENIZERS_PARALLELISM"]
assert "false" not in (session.path / "preset.dstack.yml").read_text()
assert data["env"] == ["HF_TOKEN", "TOKENIZERS_PARALLELISM=false"]
loaded = load_session_configuration(session)
assert isinstance(loaded.env["HF_TOKEN"], EnvSentinel)
assert loaded.env["TOKENIZERS_PARALLELISM"] == "false"
if not IS_WINDOWS:
assert (session.path / "preset.dstack.yml").stat().st_mode & 0o777 == 0o600

@pytest.mark.parametrize("status", ["success", "failed"])
def test_finish_records_terminal_status(self, tmp_path, monkeypatch, status):
Expand Down