> This page is for version 0.3.0.
> For other versions, use one of these documentation indexes:
> - Main (default): https://docs.nvidia.com/nemo/gym/main/llms.txt
> - 0.7.0: https://docs.nvidia.com/nemo/gym/v0.7.0/llms.txt
> - 0.6.0: https://docs.nvidia.com/nemo/gym/v0.6.0/llms.txt
> - 0.5.1: https://docs.nvidia.com/nemo/gym/v0.5.1/llms.txt
> - 0.5.0: https://docs.nvidia.com/nemo/gym/v0.5.0/llms.txt
> - 0.4.0: https://docs.nvidia.com/nemo/gym/v0.4.0/llms.txt
> - 0.3.0: https://docs.nvidia.com/nemo/gym/v0.3.0/llms.txt
> - 0.2.1: https://docs.nvidia.com/nemo/gym/v0.2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/gym/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/gym/_mcp/server.

# nemo_gym.tasks.harbor.compose_variables

Compose interpolation for Harbor tasks.

The interpolation engine with Compose's operators, the dotenv reader with Compose's quoting
rules, the inlining of env_file entries into each service, the type restoration healthcheck
fields need after interpolation, the residual interpolation check, and Harbor's variable
precedence for a task: infrastructure variables, then the task's environment table, then the
process environment, then environment/.env, with Compose's empty-string rule and one warning
per unset name. It lives under tasks/harbor because it reads HarborTask and the task digest's
ignore rules. The error type and the two Compose shape helpers it raises and reads live in
nemo_gym.sandbox.compose_fields, which the adapter-side field checks share. The loader and the
build modules both resolve a task's Compose file through it.

## Module Contents

### Functions

| Name                                                                                        | Description                                                                                                   |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [`_closing_brace`](#nemo_gym-tasks-harbor-compose_variables-_closing_brace)                 | Index of the `&#125;` that closes the `$&#123;` opening at `start`, nested `$&#123;...&#125;` included.       |
| [`_dotenv_variables`](#nemo_gym-tasks-harbor-compose_variables-_dotenv_variables)           | `environment/.env` read as Compose's dotenv reader reads it (`compose-go/dotenv`): an unquoted or             |
| [`_env_file_path`](#nemo_gym-tasks-harbor-compose_variables-_env_file_path)                 | `path` resolved under `base_dir` (Harbor's `$&#123;CONTEXT_DIR&#125;/x.env` is absolute and lands there too). |
| [`_literal`](#nemo_gym-tasks-harbor-compose_variables-_literal)                             | `value` as the document's interpolation and literal checks read a literal: every `$` doubled.                 |
| [`_quoted_env_value`](#nemo_gym-tasks-harbor-compose_variables-_quoted_env_value)           | The quoted env_file value opening at `text[0]`: `(value, quote, characters consumed)`. The value runs         |
| [`_resolve`](#nemo_gym-tasks-harbor-compose_variables-_resolve)                             | -                                                                                                             |
| [`_substitute_text`](#nemo_gym-tasks-harbor-compose_variables-_substitute_text)             | Compose interpolation: `$$` (a literal dollar, kept doubled for `_compose_literals`), `$&#123;NAME&#125;`,    |
| [`_task_env`](#nemo_gym-tasks-harbor-compose_variables-_task_env)                           | `[environment].env` as Harbor's `resolve_env_vars` hands it to `docker compose` (see                          |
| [`_warn_unset`](#nemo_gym-tasks-harbor-compose_variables-_warn_unset)                       | -                                                                                                             |
| [`compose_variables`](#nemo_gym-tasks-harbor-compose_variables-compose_variables)           | The variables a task's Compose file may interpolate, resolved in the order Harbor's `docker compose` sees.    |
| [`env_file_entries`](#nemo_gym-tasks-harbor-compose_variables-env_file_entries)             | The `KEY=value` lines of a Compose env file in file order, as `(key, value, quote)`. `quote` is               |
| [`inline_env_files`](#nemo_gym-tasks-harbor-compose_variables-inline_env_files)             | Read each service's `env_file` entries (relative to `base_dir`) into its `environment` mapping.               |
| [`parse_env_file`](#nemo_gym-tasks-harbor-compose_variables-parse_env_file)                 | `KEY=value` lines of a Compose `env_file`, taken literally.                                                   |
| [`residual_interpolation`](#nemo_gym-tasks-harbor-compose_variables-residual_interpolation) | The key path (`services.api.environment.DB_PASS`, `services.a.command[1]`) of the first string still          |
| [`restore_compose_types`](#nemo_gym-tasks-harbor-compose_variables-restore_compose_types)   | Give the typed healthcheck fields their types back after interpolation, which turns every scalar it           |
| [`substitute_compose`](#nemo_gym-tasks-harbor-compose_variables-substitute_compose)         | Interpolate `$&#123;VAR&#125;` references in every string of a Compose document from `variables` alone.       |
| [`substitute_with_empty`](#nemo_gym-tasks-harbor-compose_variables-substitute_with_empty)   | `substitute_compose` with Compose's rule for a bare `$&#123;VAR&#125;` or `$VAR` nobody sets: an empty        |

### Data

[`COMPOSE_DOTENV`](#nemo_gym-tasks-harbor-compose_variables-COMPOSE_DOTENV)

[`HARBOR_ENV_TEMPLATE`](#nemo_gym-tasks-harbor-compose_variables-HARBOR_ENV_TEMPLATE)

[`LOGGER`](#nemo_gym-tasks-harbor-compose_variables-LOGGER)

[`LOG_MOUNT_PATHS`](#nemo_gym-tasks-harbor-compose_variables-LOG_MOUNT_PATHS)

[`TEST_DIR`](#nemo_gym-tasks-harbor-compose_variables-TEST_DIR)

[`_DOUBLE_QUOTE_ESCAPES`](#nemo_gym-tasks-harbor-compose_variables-_DOUBLE_QUOTE_ESCAPES)

[`_HEALTHCHECK_BOOLEANS`](#nemo_gym-tasks-harbor-compose_variables-_HEALTHCHECK_BOOLEANS)

[`_NAME`](#nemo_gym-tasks-harbor-compose_variables-_NAME)

[`_WARNED_UNSET`](#nemo_gym-tasks-harbor-compose_variables-_WARNED_UNSET)

### API

```python
nemo_gym.tasks.harbor.compose_variables._closing_brace(
    text: str,
    start: int
) -> int
```

Index of the `&#125;` that closes the `$&#123;` opening at `start`, nested `$&#123;...&#125;` included.

```python
nemo_gym.tasks.harbor.compose_variables._dotenv_variables(
    task: nemo_gym.tasks.harbor.task.HarborTask,
    context: pathlib.Path,
    above: collections.abc.Mapping[str, str],
    unset: set[str]
) -> dict[str, str]
```

`environment/.env` read as Compose's dotenv reader reads it (`compose-go/dotenv`): an unquoted or
double-quoted value is interpolated against `above` and then the lines before it, a single-quoted value is
literal. A dollar that stays literal is kept doubled, as `parse_env_file` keeps it, so the document's
literal check leaves it alone.

```python
nemo_gym.tasks.harbor.compose_variables._env_file_path(
    base_dir: pathlib.Path,
    path: str,
    service: str
) -> pathlib.Path
```

`path` resolved under `base_dir` (Harbor's `$&#123;CONTEXT_DIR&#125;/x.env` is absolute and lands there too).
A path that resolves outside, or a symlink, is refused: task folders are third-party content and the
file must be part of what prepare digested. So is a file the task digest skips (`*~`, `*.swp`,
`__pycache__` and the like, see `nemo_gym.tasks.harbor.digest.is_ignored`): it would change the
document without changing the identity the server checks at seed.

```python
nemo_gym.tasks.harbor.compose_variables._literal(
    value: str
) -> str
```

`value` as the document's interpolation and literal checks read a literal: every `$` doubled.

```python
nemo_gym.tasks.harbor.compose_variables._quoted_env_value(
    text: str,
    line: int,
    escaped_dollar: str
) -> tuple[str, str, int]
```

The quoted env_file value opening at `text[0]`: `(value, quote, characters consumed)`. The value runs
to its closing quote, across newlines if need be. Double quotes decode `\n`, `\r`, `\t`, `\\`,
`\"` and `\$` (the last becoming `escaped_dollar`) and keep any other `\X` as written; single
quotes are literal except for `\'`, which is an escaped quote in either style.

```python
nemo_gym.tasks.harbor.compose_variables._resolve(
    name: str,
    operator: str,
    argument: str,
    variables: dict[str, str]
) -> str
```

```python
nemo_gym.tasks.harbor.compose_variables._substitute_text(
    text: str,
    variables: dict[str, str]
) -> str
```

Compose interpolation: `$$` (a literal dollar, kept doubled for `_compose_literals`), `$&#123;NAME&#125;`,
`$&#123;NAME:-default&#125;`, `$&#123;NAME-default&#125;`, `$&#123;NAME:?error&#125;`, `$&#123;NAME?error&#125;`, `$&#123;NAME:+alternative&#125;`,
`$&#123;NAME+alternative&#125;` and bare `$NAME`. Defaults, alternatives and error texts are interpolated too, so
`$&#123;A:-$&#123;B:-x&#125;&#125;` nests the way Compose documents.

```python
nemo_gym.tasks.harbor.compose_variables._task_env(
    task: nemo_gym.tasks.harbor.task.HarborTask,
    env: collections.abc.Mapping[str, typing.Any]
) -> dict[str, str]
```

`[environment].env` as Harbor's `resolve_env_vars` hands it to `docker compose` (see
`compose_variables`), each value literal to the document (`_literal`).

```python
nemo_gym.tasks.harbor.compose_variables._warn_unset(
    task: nemo_gym.tasks.harbor.task.HarborTask,
    unset: set[str]
) -> None
```

```python
nemo_gym.tasks.harbor.compose_variables.compose_variables(
    task: nemo_gym.tasks.harbor.task.HarborTask,
    unset: set[str] | None = None
) -> dict[str, str]
```

The variables a task's Compose file may interpolate, resolved in the order Harbor's `docker compose` sees.

Harbor runs `docker compose` with the process environment, the task's `[environment].env` over it and
its own infrastructure variables over both (`merge_compose_env` in
`harbor/environments/docker/compose_env.py` and `_compose_env_vars` in `docker.py` at v0.22.0, called
with `include_os_env=True`), and Compose reads the project directory's `.env` below everything the
command's environment sets. Highest first:

1. Gym's infrastructure set: `MAIN_IMAGE_NAME` (the resolved task image), `CONTEXT_DIR` (the task's
   `environment/` folder), `TEST_DIR` (`TEST_DIR`), `COMPOSE_PROJECT_NAME` (`hb__&lt;hash&gt;`,
   Harbor's environment id of the context and the `main` image name, a stable placeholder: compose-go
   injects the real value from `--project-name`, the trial session id under Harbor and `compose-&lt;uuid&gt;`
   per episode in Gym's adapter, which has no Compose-generated names for it to match), `PREBUILT_IMAGE_NAME`
   when `docker_image` is declared, `CPUS` and `MEMORY` (as `&lt;n&gt;M`, Harbor's spelling) from the
   task's resources, and the `HOST_*_PATH` / `ENV_*_PATH` log mounts as the `/logs` tree;
2. the task's `[environment].env`, each value read as Harbor's `resolve_env_vars` reads it
   (`harbor/utils/env.py`): a whole value `$&#123;VAR&#125;` or `$&#123;VAR:-default&#125;` comes from the process
   environment, the default standing in when the variable is unset, and one with neither raises
   `ImageConfigError` naming the task and the variable, as Harbor fails the trial; any other value
   is literal;
3. the process environment;
4. `environment/.env`, its unquoted and double-quoted values interpolated against the levels above and
   the lines before them, as Compose's dotenv reader does; single-quoted values are literal.

A reference none of them sets takes its default (`$&#123;VAR:-x&#125;`, `$&#123;VAR-x&#125;`), else it is an empty string,
as docker compose makes it, with one warning naming the task and the variable (added to `unset` when
given, so the caller warns once for the whole document, else logged here). `$&#123;VAR:?msg&#125;` and
`$&#123;VAR?msg&#125;` raise `ImageConfigError` with the message. The process environment is not part of
the task's digest: prepare and the environment server must run with the same variables, as Harbor's one
process does.

Every value is literal to the document, as it is to Compose (which splices a variable's value in verbatim and
never re-scans it): a `$` in a host, task or infrastructure value is doubled here, the convention
`parse_env_file` and `_dotenv_variables` keep for a literal dollar, so a password such as
`$2b$12$...` neither trips the document's residual check nor loses a `$$` when the sandbox starts.

```python
nemo_gym.tasks.harbor.compose_variables.env_file_entries(
    text: str,
    escaped_dollar: str = '$'
) -> list[tuple[str, str, str]]
```

The `KEY=value` lines of a Compose env file in file order, as `(key, value, quote)`. `quote` is
`'`, `"` or `""` for an unquoted value, which is how Compose's dotenv reader decides what to do with
it: a single-quoted value is literal, the other two it interpolates. Blank lines and `#` comments are
skipped and an optional `export` prefix dropped. A quoted value ends at its closing quote, which may be on
a later line, and the rest of that line is a comment; its escapes are decoded as `_quoted_env_value`
says, a double-quoted `\$` becoming `escaped_dollar`. An unquoted value ends at the first `#` that
follows whitespace and is returned as written, `$` included. A line that is not `KEY=value` or a quote
that never closes raises `ComposeSubstitutionError` naming the line number only, as env files hold
secrets.

```python
nemo_gym.tasks.harbor.compose_variables.inline_env_files(
    document: dict,
    base_dir: pathlib.Path,
    variables: dict[str, str] | None = None
) -> dict
```

Read each service's `env_file` entries (relative to `base_dir`) into its `environment` mapping.

Every service's `environment` becomes a mapping of strings. A service's own entries win over its
files, as in Compose. A bare `NAME` entry in a list-form `environment` (or `NAME:` with no value in
the mapping form) takes the task variable of that name; when there is none the variable is left unset,
as Compose leaves it, so it also removes the `env_file` value of that name. A missing required file
raises `ComposeSubstitutionError` naming it; an entry with `required: false` is skipped when
absent; an entry with a `format` is refused, as only the default dotenv format is read. A file that
does not parse raises with the service, the file and the line number, never the line.

```python
nemo_gym.tasks.harbor.compose_variables.parse_env_file(
    text: str
) -> dict[str, str]
```

`KEY=value` lines of a Compose `env_file`, taken literally.

Lines are read as `env_file_entries` reads them. Values are not interpolated: a `$` in a value
is escaped as `$$` so the document's later interpolation and literal checks leave it as one `$`.

```python
nemo_gym.tasks.harbor.compose_variables.residual_interpolation(
    value: typing.Any,
    path: str = ''
) -> str | None
```

The key path (`services.api.environment.DB_PASS`, `services.a.command[1]`) of the first string still
carrying a `$NAME` or `$&#123;...&#125;` after substitution, or `None`. The path, not the value: the value next to
a residual may be a secret the host supplied, and the caller reports this in an error message.

```python
nemo_gym.tasks.harbor.compose_variables.restore_compose_types(
    document: dict
) -> dict
```

Give the typed healthcheck fields their types back after interpolation, which turns every scalar it
touches into a string: `disable` becomes a boolean (`$&#123;DISABLE:-false&#125;` must not disable the check by
being a non-empty string) and `retries` an integer. Any other string value raises
`ComposeSubstitutionError` naming the field. Mutates and returns `document`.

```python
nemo_gym.tasks.harbor.compose_variables.substitute_compose(
    value: typing.Any,
    variables: dict[str, str]
) -> typing.Any
```

Interpolate `$&#123;VAR&#125;` references in every string of a Compose document from `variables` alone.

The process environment is never consulted, so a dataset resolves the same way on every
deployment. A variable that is neither defined nor given a default raises
`ComposeSubstitutionError` naming it.

```python
nemo_gym.tasks.harbor.compose_variables.substitute_with_empty(
    value: typing.Any,
    variables: collections.abc.Mapping[str, str],
    unset: set[str]
) -> typing.Any
```

`substitute_compose` with Compose's rule for a bare `$&#123;VAR&#125;` or `$VAR` nobody sets: an empty
string, the name added to `unset` for the caller to warn about once. Only the bare form raises the resolver's
default message; `$&#123;VAR:-x&#125;` keeps its default, `$&#123;VAR:?msg&#125;` its refusal, and every other failure passes
through unchanged. The loader and the Dockerfile build (`nemo_gym.tasks.harbor.image_build`) interpolate a
task's Compose file through this one rule, so the two never disagree on the same file.

```python
nemo_gym.tasks.harbor.compose_variables.COMPOSE_DOTENV = '.env'
```

```python
nemo_gym.tasks.harbor.compose_variables.HARBOR_ENV_TEMPLATE = re.compile('\\$\\{([^}:]+)(?::-(.*))?\\}')
```

```python
nemo_gym.tasks.harbor.compose_variables.LOGGER = logging.getLogger(__name__)
```

```python
nemo_gym.tasks.harbor.compose_variables.LOG_MOUNT_PATHS = {'VERIFIER_LOGS': '/logs/verifier', 'AGENT_LOGS': '/logs/agent', 'ARTIFACTS': '/...
```

```python
nemo_gym.tasks.harbor.compose_variables.TEST_DIR = '/tests'
```

```python
nemo_gym.tasks.harbor.compose_variables._DOUBLE_QUOTE_ESCAPES = {'n': '\n', 'r': '\r', 't': '\t', '\\': '\\', '"': '"'}
```

```python
nemo_gym.tasks.harbor.compose_variables._HEALTHCHECK_BOOLEANS = {'true': True, 'false': False}
```

```python
nemo_gym.tasks.harbor.compose_variables._NAME = re.compile('[A-Za-z_][A-Za-z0-9_]*')
```

```python
nemo_gym.tasks.harbor.compose_variables._WARNED_UNSET: set[tuple[str, str]] = set()
```