feat(evaluator-sdk): agent-skill injection for FabricContainerRuntime#841
Open
SandyChapman wants to merge 1 commit into
Open
Conversation
Contributor
|
SandyChapman
force-pushed
the
fabric-container-skill-injection/schapman
branch
from
July 22, 2026 12:22
ce8121f to
0ed94f6
Compare
SandyChapman
changed the base branch from
main
to
fabric-multi-skill-injection/schapman
July 22, 2026 12:22
Stacked on #816 (multi-skill for the host FabricAgentRuntime). Brings the same multi-skill contract to the sandboxed FabricContainerRuntime, so an isolated (Docker) Fabric eval can inject a SET of agentskills.io skills — the container L1 follow-up #816 called out. - ctor takes `skills=` (a list); `with_skills()` is additive and chainable and `with_skill()` is a thin wrapper, matching the host. Duplicate skill names are rejected up front via require_unique_skill_names. - New `stage_skills_seed()` in skills.py — the plural, containerized sibling of install_skills: loops stage_skill_seed per bundle into the sandbox seed set and, for native mode, merges every bundle into ONE skills.paths overlay (Fabric applies skills.paths last-wins). No on-disk rollback needed — the seed set is in-memory. - _run_task threads a provenance list; codex cleanup scrubs every staged bundle from the downloaded workspace evidence. Trial metadata gains a `skills` list while keeping the lone `skill` field (one-skill run, else None) via _skill_metadata, so SkillUsedMetric and single-skill consumers are unchanged. - SkillMode = Literal[...] threaded through the new plural helpers and both runtimes. Native/hermes path validated live against a real Docker sandbox (A/B marker test). Codex mode is unit-tested only — the sandbox image does not provision the codex CLI. Signed-off-by: Sandy Chapman <schapman@nvidia.com>
SandyChapman
force-pushed
the
fabric-container-skill-injection/schapman
branch
from
July 22, 2026 12:33
0ed94f6 to
83e14d8
Compare
SandyChapman
marked this pull request as ready for review
July 22, 2026 12:41
| self._skills: list[AgentSkill] = list(skills or []) | ||
| require_unique_skill_names(self._skills) | ||
|
|
||
| def with_skills(self, skills: Sequence[AgentSkill]) -> FabricContainerRuntime: |
Contributor
There was a problem hiding this comment.
one idea to reduce some duplication and also re-use for other runtimes in the future: store skills state in a standalone class
# skills.py
@dataclass(frozen=True)
class SkillSet:
skills: tuple[AgentSkill, ...] = ()
def __post_init__(self) -> None:
require_unique_skill_names(self.skills)
def with_skills(self, skills: Sequence[AgentSkill]) -> SkillSet:
combined = (*self.skills, *skills)
require_unique_skill_names(combined)
return SkillSet(skills=combined)
def with_skill(self, skill: AgentSkill) -> SkillSet:
return self.with_skills([skill])
class XYZRuntime:
# runtime.py / container_runtime.py
def __init__(..., skills: Sequence[AgentSkill] | None = None) -> None:
...
self._skill_set = SkillSet(skills=tuple(skills or ()))
def with_skills(self, skills: Sequence[AgentSkill]) -> FabricAgentRuntime: # or FabricContainerRuntime
"""Return a copy with ``skills`` added; ``self`` is not modified."""
clone = copy.copy(self)
clone._skill_set = self._skill_set.with_skills(skills)
return clone
def with_skill(self, skill: AgentSkill) -> FabricAgentRuntime:
return self.with_skills([skill])| skill_provenances: list[SkillProvenance] = [] | ||
| try: | ||
| seed_files, profile_paths = self._seed_files(task) | ||
| seed_files, profile_paths, skill_provenances = self._seed_files(task, skill_mode) |
Contributor
There was a problem hiding this comment.
Task files can overwrite potentially Codex injected skill files if their path matches:
skills files:
myskills/code-review/
└── SKILL.md
task files:
task.inputs = {
"instruction": "Review this change",
"files": {
".agents/skills/code-review/SKILL.md": """<inline_skill>"""Is it something we should guard against? Hash will also point to the injected skill, but the content will be different
Example:
reserved = PurePosixPath(".agents/skills") / skill.name
for rel_path in task.inputs.get("files", {}):
seed_path = PurePosixPath(rel_path)
if seed_path == reserved or reserved in seed_path.parents:
raise SkillInjectionError(
f"task seed collides with injected skill path {reserved}"
)| try: | ||
| bundle[rel] = path.read_text(encoding="utf-8") | ||
| except UnicodeDecodeError as exc: | ||
| raise SkillInjectionError( |
Contributor
There was a problem hiding this comment.
if we injected a binary file (image, for example, as a part of skills assets), this will error out. Should we allow passing binary assets?
ngoncharenko
approved these changes
Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds agent-skill injection to the sandboxed
FabricContainerRuntime, so an isolated (Docker) Fabric eval can run WITH an agentskills.io skill — previously only the hostFabricAgentRuntimesupported skills (limitation L1 from the LAB-examples gap analysis). It mirrors the host runtime's skill design and extends the same two-mode contract to the container.How
__init__gains askill: AgentSkill | None; newwith_skill()clone method so an A/B eval derives baseline (with_skill(None)) and treated (with_skill(skill)) runtimes from one instance._resolve_skill_mode).nemo_fabricis imported lazily on the host only when a skill is set, so the no-skill container path stays dependency-free.stage_skill_seed()inskills.py— the sandboxed sibling ofinstall_skill. Instead of a hostcopytree(the container has no host workspace), it renders the bundle into the sandbox seed set (SandboxSpec.files) at the harness's in-sandbox discovery path:accepts: ["skills"]) →/in/skills/<name>/+ askills.pathsprofile overlay (preserving any pre-configured skill paths, last-wins);<workspace>/.agents/skills/<name>/(self-discovery), no overlay./out/workspacebefore it's exposed as evidence (so injected files don't skew workspace-reading metrics). Native staging lives under/inand is never downloaded, so it needs no cleanup.SkillMode = Literal["native", "codex_skills_dir"], threaded through both runtimes andSkillProvenance.mode.Text bundles only: the seed set is
dict[str, str], so a binary file raisesSkillInjectionError(the hostinstall_skillcopytree path still handles binary bundles).Tests
test_fabric_container_runtime.pymirroring the host skill-injection suite: native seed-set + overlay, preconfigured-path preservation, runtime-discovered adapter, codex seed + evidence exclusion, fail-fast on an unsupported adapter, no-skill-no-probe, andwith_skillcopy. Full fabric suite (container + host + skills) = 76 passing; ruff + format clean;tyclean; SDK mirror re-vendored (make vendor).mode=native, staged path, content hash) stamped correctly; no leaked containers; native skill correctly absent from workspace evidence.Notes
fabric-adapter.jsondeclaresaccepts: ["models", "telemetry"](noskills), so Fabric routes codex skillsunsupported, while the codex CLI itself self-discovers.agents/skills/. When the codex adapter addsskills, this branch can be deleted and codex flows through the same native path.