Skip to navigation

nemo_gym.data.tasks.harbor.compose_overlay

View as Markdown

The job-level Compose overlay: extra Compose files, MCP entries and a prompt template from the run config.

Harbor’s job config names extra_docker_compose files, appended to every task’s docker compose -f ... command after the task’s own file, agents[].mcp_servers entries, merged by name with the task’s [[environment.mcp_servers]] (trial.py _init_agent at v0.22.0: the task’s first, the job’s after, so a job entry replaces a task entry of the same name), and a prompt_template_path the agent renders around the instruction (harbor/utils/templating.py). Gym reads the same three things from one block of the environment server’s configuration, compose_overlay (HarborComposeOverlay), and changes no task file.

The overlay files are merged onto the task’s Compose document the way Compose merges -f files (merge_compose): mappings key by key, sequences appended (ports, volumes, expose and the other keyed sequences unique on their key, the later entry winning), environment, labels and extra_hosts as mappings whatever their form, depends_on and networks as mappings, command, entrypoint and healthcheck.test replaced whole, every scalar by the last file, a key written with no value keeping the earlier file’s value. The !reset tag removes the attribute the earlier files set (ports: !reset [], networks.x.name: !reset null, environment: {DISABLE_AUTH: !reset}) and !override replaces a value instead of merging into it, as Compose defines both. Each file is interpolated with the same variables as the task’s file (Harbor’s one docker compose command sees one environment). Its relative paths resolve against the project directory, the task’s environment folder, never against the overlay file’s own folder: Compose resolves the relative paths of every -f file against the project directory (compose-go’s ResolveRelativePaths with the one WorkingDir; “Paths in override files are not resolved relative to the override file’s own location”), and Harbor runs docker compose --project-directory <task>/environment -f <task file> -f <extra> ..., so an extra file’s ./x.env or ./conf is the task’s environment/x.env or environment/conf under Harbor and here alike (the memory team’s ${GRAPHITI_CONFIG_PATH:-./config.yaml} and ${MAMS_HARBOR_MCP_BUILD_CONTEXT:-./mcp-server} defaults land in the task folder under both; set the variable to an absolute path, as the Harbor job must). An overlay file’s env_file entries are resolved to absolute paths before the merge (absolute_env_files) and the env files of the merged document are read once after it, the merged environment winning over them, as Compose reads them (so a later file’s env_file: !reset [] drops the earlier files’ lists, the task’s included). An overlay file is the operator’s, so its env_file may name any readable file (an absolute path anywhere, a relative one wherever it lands), where a task’s must stay under the task; its bind sources are left for the volume planner, which resolves a relative source against the same folder for every file of the group. A task that declares only an image gets a one-service group, main, for the overlay to extend (synthesized_group).

Module Contents

Classes

NameDescription
ComposeOverlayErrorAn overlay file, its merge or the prompt template cannot be applied as written.
HarborComposeOverlayThe compose_overlay block of the Harbor environment server’s configuration.
OverrideMappingA mapping tagged !override: it replaces the earlier files’ value instead of merging into it.
OverrideScalarA scalar tagged !override. A scalar replaces the earlier value whether tagged or not, so the tag changes
OverrideSequenceA sequence tagged !override: it replaces the earlier files’ value instead of appending to it.
_OverlayLoaderyaml.SafeLoader that reads Compose’s !reset and !override tags.
_ResetA !reset value: the attribute the earlier files set is removed; the value written after the tag is

Functions

NameDescription
_construct_override-
_construct_reset-
_dependencies-
_jinjaThe jinja2 module, imported when a prompt template is read: the Harbor environment server’s own
_mapping_key-
_match-
_merge-
_networks-
_pairsA keyed field (environment, labels, extra_hosts, …) as a mapping, whichever form it was
_passthrough_entryA list-form environment entry, a bare NAME that variables sets resolved to NAME=value.
_plainvalue without markers: !override wrappers become plain mappings, sequences and strings, and a
_port_key-
_shape_errorA field of the wrong shape, named by its path and the value’s type, never the value: a mistyped
_text_key-
_uniqueitems with one entry per key, the last one written, in the order its key first appeared.
_volume_key-
_yaml_problemWhat went wrong and where, without PyYAML’s snippet of the offending line: an overlay file commonly carries the
absolute_env_filesEach overlay service’s env_file entries replaced by the absolute paths they resolve to against
compile_prompt_templateThe template at path compiled once it passes validate_prompt_template: a jinja2.Template from
interpolate_overlaydocument with substitute applied to every string, the !reset and !override markers kept.
load_overlay_fileRead one overlay Compose file (load_overlay_text); a file that cannot be read raises
load_overlay_textParse one overlay Compose document with its !reset and !override tags, as a mapping.
merge_composeoverlay merged onto base as Compose merges a later -f file onto the earlier ones.
merged_mcp_serversThe task’s MCP entries with the overlay’s merged in by name, in Harbor’s order and with Harbor’s rule.
overlay_env_file_resolverThe resolver of an overlay file’s env_file entries: a relative path against project_dir, the Compose
overlay_filesThe overlay’s Compose files, [] without an overlay.
render_compiled_templateinstruction rendered into template (compile_prompt_template), path named in the errors:
render_prompt_templateinstruction rendered into the template at path, as Harbor renders it (render_prompt_template in
resolve_passthrough_environmentdocument with each overlay service’s pass-through environment entries resolved against variables:
synthesized_groupThe Compose document of a task that declares no Compose file: Harbor’s own main service, nothing else.
validate_prompt_templateThe template’s text, once it parses and names instruction; otherwise ComposeOverlayError.
wrap_first_instructionA copy of responses_create_params with render applied to the instruction: the first user message’s

Data

OVERRIDE_TAG

PROMPT_TEMPLATE_VARIABLE

RESET

RESET_TAG

_DEPENDS_ON

_ENV_KEY

_KEYED_MAPPINGS

_REPLACED

_SERVICE_NETWORKS

_UNIQUE_SEQUENCES

API

class nemo_gym.data.tasks.harbor.compose_overlay.ComposeOverlayError()

Bases: ValueError

An overlay file, its merge or the prompt template cannot be applied as written.

class nemo_gym.data.tasks.harbor.compose_overlay.HarborComposeOverlay()

Bases: BaseModel

The compose_overlay block of the Harbor environment server’s configuration.

files are Compose files merged, in order, onto every task’s Compose document after the task’s own file (Harbor’s extra_docker_compose); mcp_servers are MCP entries merged by name with the task’s [[environment.mcp_servers]], the overlay’s winning on a name (Harbor’s agents[].mcp_servers); prompt_template is a Jinja template with an {{ instruction }} variable, rendered around the instruction of the episode’s first turn (Harbor’s prompt_template_path). Every path must exist when the block is read: Harbor refuses a missing extra Compose file the same way (FileNotFoundError in _normalize_extra_docker_compose_paths), and a template that does not name instruction or does not parse is refused before any episode, as Harbor’s render_prompt_template refuses it. The template is read and compiled once, when the block is read (compile_prompt_template); every episode renders the compiled template (render_instruction), so the file is not re-read per episode and an edit to it under a running server is not seen until the server is started again.

_compiled_template
Any = PrivateAttr(default=None)
configured
bool
files
list[Path] = Field(default_factory=list)
mcp_servers
list[HarborMCPServer] = Field(default_factory=list)
model_config
= ConfigDict(extra='forbid')
prompt_template
Path | None = None
nemo_gym.data.tasks.harbor.compose_overlay.HarborComposeOverlay.compile_template() -> 'HarborComposeOverlay'

Read, check and compile the template once, when the block is read: a template that does not parse or does not name instruction is refused here, before any episode.

nemo_gym.data.tasks.harbor.compose_overlay.HarborComposeOverlay.existing_files(
files: list[pathlib.Path]
) -> list[pathlib.Path]
classmethod
nemo_gym.data.tasks.harbor.compose_overlay.HarborComposeOverlay.readable_template(
path: pathlib.Path | None
) -> pathlib.Path | None
classmethod
nemo_gym.data.tasks.harbor.compose_overlay.HarborComposeOverlay.render_instruction(
instruction: str
) -> str

instruction rendered into the compiled prompt_template (render_compiled_template); the instruction as given when the block names no template.

class nemo_gym.data.tasks.harbor.compose_overlay.OverrideMapping()

Bases: dict

A mapping tagged !override: it replaces the earlier files’ value instead of merging into it.

class nemo_gym.data.tasks.harbor.compose_overlay.OverrideScalar()

Bases: str

A scalar tagged !override. A scalar replaces the earlier value whether tagged or not, so the tag changes nothing where the value stays a scalar; it is kept for the fields whose scalar form is a one-entry sequence (env_file: !override /path/x.env, tmpfs, dns), which the normalization before the merge turns into an OverrideSequence, so the one entry replaces the earlier files’ lists instead of appending to them, as the tagged list form does. _plain leaves a plain str in the merged document.

class nemo_gym.data.tasks.harbor.compose_overlay.OverrideSequence()

Bases: list

A sequence tagged !override: it replaces the earlier files’ value instead of appending to it.

class nemo_gym.data.tasks.harbor.compose_overlay._OverlayLoader()

Bases: SafeLoader

yaml.SafeLoader that reads Compose’s !reset and !override tags.

class nemo_gym.data.tasks.harbor.compose_overlay._Reset()

A !reset value: the attribute the earlier files set is removed; the value written after the tag is not read (Compose ignores it too).

nemo_gym.data.tasks.harbor.compose_overlay._Reset.__repr__() -> str
nemo_gym.data.tasks.harbor.compose_overlay._construct_override(
loader: yaml.SafeLoader,
node: yaml.Node
) -> typing.Any
nemo_gym.data.tasks.harbor.compose_overlay._construct_reset(
loader: yaml.SafeLoader,
node: yaml.Node
nemo_gym.data.tasks.harbor.compose_overlay._dependencies(
value: typing.Any,
path: tuple[str, ...]
) -> dict[str, typing.Any]
nemo_gym.data.tasks.harbor.compose_overlay._jinja() -> typing.Any

The jinja2 module, imported when a prompt template is read: the Harbor environment server’s own requirements install it (Harbor renders its templates with Jinja2 too); the loader and the harnesses that import this module for the merge and the MCP rule do not need it.

nemo_gym.data.tasks.harbor.compose_overlay._mapping_key(
field: str
) -> collections.abc.Callable[[Any], typing.Any]
nemo_gym.data.tasks.harbor.compose_overlay._match(
path: tuple[str, ...],
pattern: tuple[str, ...]
) -> bool
nemo_gym.data.tasks.harbor.compose_overlay._merge(
base: typing.Any,
over: typing.Any,
path: tuple[str, ...]
) -> typing.Any
nemo_gym.data.tasks.harbor.compose_overlay._networks(
value: typing.Any,
path: tuple[str, ...]
) -> dict[str, typing.Any]
nemo_gym.data.tasks.harbor.compose_overlay._pairs(
value: typing.Any,
path: tuple[str, ...]
) -> dict[str, typing.Any]

A keyed field (environment, labels, extra_hosts, …) as a mapping, whichever form it was written in: a list of KEY=value (KEY alone is None; host:ip for extra_hosts).

nemo_gym.data.tasks.harbor.compose_overlay._passthrough_entry(
item: typing.Any,
variables: collections.abc.Mapping[str, str]
) -> typing.Any

A list-form environment entry, a bare NAME that variables sets resolved to NAME=value.

nemo_gym.data.tasks.harbor.compose_overlay._plain(
value: typing.Any
) -> typing.Any

value without markers: !override wrappers become plain mappings, sequences and strings, and a !reset under a key no earlier file set leaves the key absent, as Compose leaves it.

nemo_gym.data.tasks.harbor.compose_overlay._port_key(
entry: typing.Any
) -> typing.Any
nemo_gym.data.tasks.harbor.compose_overlay._shape_error(
path: tuple[str, ...],
value: typing.Any

A field of the wrong shape, named by its path and the value’s type, never the value: a mistyped environment: API_KEY=... would otherwise print the key in prepare’s skipped.json and the seed’s reason.

nemo_gym.data.tasks.harbor.compose_overlay._text_key(
entry: typing.Any
) -> typing.Any
nemo_gym.data.tasks.harbor.compose_overlay._unique(
items: list[typing.Any],
key: collections.abc.Callable[[Any], typing.Any]
) -> list[typing.Any]

items with one entry per key, the last one written, in the order its key first appeared.

nemo_gym.data.tasks.harbor.compose_overlay._volume_key(
entry: typing.Any
) -> typing.Any
nemo_gym.data.tasks.harbor.compose_overlay._yaml_problem(
exc: yaml.YAMLError
) -> str

What went wrong and where, without PyYAML’s snippet of the offending line: an overlay file commonly carries the deployment’s keys, and the message reaches prepare’s skipped.json and the seed’s failure reason. A MarkedYAMLError gives its context and problem (PyYAML’s own phrases, no source text) with the line and column; any other error only its kind.

nemo_gym.data.tasks.harbor.compose_overlay.absolute_env_files(
document: typing.Any,
project_dir: pathlib.Path
) -> typing.Any

Each overlay service’s env_file entries replaced by the absolute paths they resolve to against project_dir (overlay_env_file_resolver, resolve_env_file_entries), the files neither read nor checked to exist yet: Compose resolves each file’s paths before the merge and reads the env files of the merged document once after it, the merged environment winning, so a file a later overlay file drops (env_file: !reset []) is never required to exist and a missing one that survives the merge is reported at that reading. The markers are kept for the merge: env_file: !reset [] and a service tagged !reset pass through, an !override list keeps its tag and an !override scalar becomes an !override list of its one path (so it replaces the earlier files’ lists, as Compose applies the tag), and environment is not touched (its own !reset and !override markers, and its normalization, are the merge’s and the final reading’s). Bind sources are not rewritten: the volume planner resolves a relative source of any file against the same project directory, which is where Compose puts it.

nemo_gym.data.tasks.harbor.compose_overlay.compile_prompt_template(
path: pathlib.Path
) -> typing.Any

The template at path compiled once it passes validate_prompt_template: a jinja2.Template from an environment with StrictUndefined, as Harbor’s render_prompt_template builds one per render. The block compiles its template when it is read and renders the compiled one per episode, so the file is read and parsed once, not twice per episode on the event loop.

nemo_gym.data.tasks.harbor.compose_overlay.interpolate_overlay(
document: typing.Any,
substitute: collections.abc.Callable[[str], str]
) -> typing.Any

document with substitute applied to every string, the !reset and !override markers kept.

Compose interpolates each -f file against the command’s environment before merging, so the overlay files take the same substitute as the task’s file (the task’s resolver, with the deployment’s values when the server has them).

nemo_gym.data.tasks.harbor.compose_overlay.load_overlay_file(
path: pathlib.Path
) -> dict[str, typing.Any]

Read one overlay Compose file (load_overlay_text); a file that cannot be read raises ComposeOverlayError naming it.

nemo_gym.data.tasks.harbor.compose_overlay.load_overlay_text(
text: str,
source: str = '<overlay>'
) -> dict[str, typing.Any]

Parse one overlay Compose document with its !reset and !override tags, as a mapping.

Top-level extension fields (x-*, the anchors a file keeps its shared blocks under) are dropped, as Compose ignores them; their aliases were expanded at parse time. Anything that is not a mapping, or does not parse, raises ComposeOverlayError naming source.

nemo_gym.data.tasks.harbor.compose_overlay.merge_compose(
base: collections.abc.Mapping[str, typing.Any],
overlay: collections.abc.Mapping[str, typing.Any]
) -> dict[str, typing.Any]

overlay merged onto base as Compose merges a later -f file onto the earlier ones.

Mappings merge key by key; sequences are appended, the keyed ones (ports by host, published port, target and protocol; volumes by target; expose, cap_add, env_file, … by value) unique on their key with the later entry winning; environment, labels, extra_hosts, sysctls and build.args merge as mappings in either written form; depends_on and a service’s networks merge as mappings (a list becomes {name: {condition: service_started}} and {name: null}); command, entrypoint and healthcheck.test are replaced whole; any other scalar, or a value of another shape, is the later file’s. A key written with no value in a later file (a bare memory_net: under networks re-declaring one an earlier file configured) keeps the earlier value, as compose-go keeps it for a nil override, except on the replaced paths. A !reset removes the key the earlier files set; an !override mapping or sequence replaces instead of merging. Neither input is modified; the result carries no markers.

nemo_gym.data.tasks.harbor.compose_overlay.merged_mcp_servers(
task_servers: collections.abc.Iterable[nemo_gym.data.tasks.harbor.models.HarborMCPServer],

The task’s MCP entries with the overlay’s merged in by name, in Harbor’s order and with Harbor’s rule.

trial.py _init_agent (v0.22.0) builds {server.name: server} over the task’s environment.mcp_servers followed by the job’s agent.mcp_servers and hands the agent the values: a job entry replaces a task entry of the same name, in the task entry’s position; a new name goes last.

nemo_gym.data.tasks.harbor.compose_overlay.overlay_env_file_resolver(
project_dir: pathlib.Path

The resolver of an overlay file’s env_file entries: a relative path against project_dir, the Compose project directory (the task’s environment folder, Harbor’s --project-directory; Compose resolves every -f file’s relative paths against it, not against the file that wrote them), an absolute path as written, any readable file accepted. The overlay files are the operator’s own, named in the run config, so the containment rule of a task’s entries (third-party content that prepare digested, under_task_folder) does not apply, as it does not under Harbor, where Compose reads an extra file’s env files from wherever they are.

nemo_gym.data.tasks.harbor.compose_overlay.overlay_files(
) -> list[pathlib.Path]

The overlay’s Compose files, [] without an overlay.

nemo_gym.data.tasks.harbor.compose_overlay.render_compiled_template(
template: typing.Any,
path: pathlib.Path,
instruction: str
) -> str

instruction rendered into template (compile_prompt_template), path named in the errors: instruction is the one variable, so a variable the template names and nothing defines raises ComposeOverlayError (StrictUndefined), as under Harbor.

nemo_gym.data.tasks.harbor.compose_overlay.render_prompt_template(
path: pathlib.Path,
instruction: str
) -> str

instruction rendered into the template at path, as Harbor renders it (render_prompt_template in harbor/utils/templating.py at v0.22.0): Jinja2 with StrictUndefined, instruction the one variable. Reads and compiles the file on every call; the server renders a block’s compiled template instead (HarborComposeOverlay.render_instruction).

nemo_gym.data.tasks.harbor.compose_overlay.resolve_passthrough_environment(
document: typing.Any,
variables: collections.abc.Mapping[str, str]
) -> typing.Any

document with each overlay service’s pass-through environment entries resolved against variables: a bare NAME in the list form becomes NAME=value and a NAME: with no value in the mapping form becomes NAME: value for every name variables sets; a name it does not set stays bare, and the markers are kept (a !reset entry, an !override list or mapping, a service tagged either).

Compose resolves the pass-through spelling against the command’s environment, the one every -f file is interpolated with, so under Harbor it follows the order of ${NAME} in every file. Gym reads the merged document’s pass-through entries once, after the merge, against the task’s variables (inline_env_files), which is the task’s own file’s order; an overlay file’s entries follow the overlay’s order (the deployment’s values above the task’s sources) only when they are resolved before the merge, here, with the variables interpolate_overlay took for the same file. A name nobody sets stays bare and is read after the merge as the task’s entries are: unset in the container.

nemo_gym.data.tasks.harbor.compose_overlay.synthesized_group(
) -> dict[str, typing.Any]

The Compose document of a task that declares no Compose file: Harbor’s own main service, nothing else.

Harbor starts every task through Compose (its base file plus the build or prebuilt file define main), so an extra Compose file extends a plain image task the same way it extends a Compose task. The sandbox adapter fills main’s image and command from the task, as it does for a task file that leaves them out.

For a Dockerfile task whose image configuration the loader resolved, preserve its ENTRYPOINT (including an explicit clear) and final WORKDIR instead of inheriting the base image’s settings. The server’s setup hook runs any Dockerfile RUN lines and installs the image’s startup user before the command and healthcheck. The adapter defers the working directory until that setup finishes, so it cannot create a root-owned directory ahead of a non-root Dockerfile WORKDIR step. No user is written here: numeric users need resolution inside the running image, while an explicit user in a job overlay still wins over the installed default.

[agent].user and [environment].workdir apply to the agent’s later commands, independently of startup. A prebuilt docker_image task without a Dockerfile is left to image resolution; without a task, main is empty.

nemo_gym.data.tasks.harbor.compose_overlay.validate_prompt_template(
path: pathlib.Path
) -> str

The template’s text, once it parses and names instruction; otherwise ComposeOverlayError.

Harbor’s render_prompt_template refuses a template that does not use the instruction variable (the task’s instruction would be lost) and renders with StrictUndefined, so any other variable fails.

nemo_gym.data.tasks.harbor.compose_overlay.wrap_first_instruction(
responses_create_params: collections.abc.Mapping[str, typing.Any],
render: collections.abc.Callable[[str], str]
) -> dict[str, typing.Any]

A copy of responses_create_params with render applied to the instruction: the first user message’s text (a string content, or the text of its first input_text part). The row’s instruction is that message, as the loader materializes it; a request without one raises ComposeOverlayError.

nemo_gym.data.tasks.harbor.compose_overlay.OVERRIDE_TAG = '!override'
nemo_gym.data.tasks.harbor.compose_overlay.PROMPT_TEMPLATE_VARIABLE = 'instruction'
nemo_gym.data.tasks.harbor.compose_overlay.RESET = _Reset()
nemo_gym.data.tasks.harbor.compose_overlay.RESET_TAG = '!reset'
nemo_gym.data.tasks.harbor.compose_overlay._DEPENDS_ON = ('services', '*', 'depends_on')
nemo_gym.data.tasks.harbor.compose_overlay._ENV_KEY = re.compile('^[^=]+')
nemo_gym.data.tasks.harbor.compose_overlay._KEYED_MAPPINGS = (('services', '*', 'environment'), ('services', '*', 'labels'), ('services', '*'...
nemo_gym.data.tasks.harbor.compose_overlay._REPLACED = (('services', '*', 'command'), ('services', '*', 'entrypoint'), ('services', '*'...
nemo_gym.data.tasks.harbor.compose_overlay._SERVICE_NETWORKS = ('services', '*', 'networks')
nemo_gym.data.tasks.harbor.compose_overlay._UNIQUE_SEQUENCES: dict[tuple[str, ...], Callable[[Any], Any]] = {}