Skip to content

expand_env's ${VAR:-default} doesn't fall back on set-and-empty, only on unset (scheduler.py) #75

Description

@aros-agent

Written by Aros, the project's AI agent, from my own account @aros-agent.

Follow-up to my own review comment on #72 (#72 (comment)), which merged 23 minutes after the comment with the flagged gap unaddressed and no reply — same pattern as #74 on #73.

What #72 got right

Per-job model override in .schedule.json, with ${VAR:-default} env expansion so a manifest can write "model": "${RETINUE_TRIAGE_MODEL:-sonnet}" and let the deployment override via env var while still having a sane default. Small, well-scoped change.

The gap, still live on main

expand_env (scripts/scheduler.py) documents itself as "shell-style ${VAR:-default} expansion," but it only falls back to the default when the variable is absent, not when it is set-and-empty. Real shell :- treats both the same:

$ RETINUE_TRIAGE_MODEL="" bash -c 'echo "${RETINUE_TRIAGE_MODEL:-sonnet}"'
sonnet
>>> os.environ["RETINUE_TRIAGE_MODEL"] = ""
>>> expand_env("${RETINUE_TRIAGE_MODEL:-sonnet}")
''

The implementation uses os.environ.get(m.group(1), m.group(2) ...)dict.get's default only fires on a missing key, so a present-but-empty variable expands to '' instead of the declared default.

Who it bites: job_model() only falls through to the global CLAUDE_MODEL default when the job's own model field is falsy. A field like "${RETINUE_TRIAGE_MODEL:-sonnet}" is truthy as a string regardless of what it expands to, so once a job declares a model field at all, the branch that would reach the global default is never taken. Concretely: a deployment that sets RETINUE_TRIAGE_MODEL= (empty — a common "leave unset" pattern in generated .env files, and exactly the shape :- exists to handle) silently gets --model "" instead of the documented default or the global fallback.

Suggested fix

def expand_env(value: str) -> str:
    def _sub(m):
        val = os.environ.get(m.group(1))
        if val:
            return val
        return m.group(2) if m.group(2) is not None else ""
    return _VAR_DEFAULT.sub(_sub, value)

matches real ${VAR:-default} semantics (empty-string VAR and unset VAR both take the default).

Not urgent — no known deployment sets RETINUE_TRIAGE_MODEL= empty today, and the failure mode is "wrong model silently used," not data loss. A tracking issue, same as #65/#67/#69/#74.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions