feat(config): support external instruction files via instruction_file#3272
Merged
Conversation
Add an optional instruction_file field on agent config that loads an agent's instruction from a file relative to the config file's directory, resolved at load time. This keeps long behavioral prompts out of the YAML (cleaner diffs, fewer merge conflicts, no YAML syntax risk when editing prompts) while staying backward compatible with the inline instruction field. The reference must be a local relative path inside the config directory: absolute paths and traversal are rejected, and reads are confined to the directory with os.OpenRoot, mirroring the HCL file() helper and fileSource. Resolution happens once in config.Load (between migration and validation), so the rest of the pipeline only ever sees the inlined Instruction; the field is cleared afterwards to keep the in-memory config self-contained. share push re-serializes the resolved config when instruction_file is used so pushed OCI artifacts stay self-contained. Closes #1666
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The instruction_file feature is well-implemented. The security design is sound:
filepath.IsLocalprovides a first-pass rejection of..traversal and absolute pathsos.OpenRootconfines reads to the config directory, blocking symlink escapes at the OS level (TOCTOU-safe)- Parentless sources (OCI/URL/bytes) are correctly rejected with clear error messages
- Mutual exclusivity between
instructionandinstruction_fileis correctly enforced - The OCI push re-serialization path correctly inlines resolved instructions before pushing
Two hypotheses about the OCI re-serialization path were investigated and dismissed:
- HCL +
instruction_file: An HCL source fails at the initialyaml.Unmarshalversion-probe (line 43–45), returning an explicit error beforeconfigUsesInstructionFileis ever called — no silent bypass. goccy/go-yamltype assertion:goccy/go-yaml v1.19.2decodes string-keyed YAML mappings asmap[string]interface{}, so theprobe["agents"].(map[string]any)assertion succeeds correctly for valid YAML configs.
One low-severity observation (not a bug): os.OpenRoot(parentDir) is called inside the per-agent loop. Opening the directory root once before the loop and reusing it for all agents would be slightly more efficient, but this is a style/performance note rather than a correctness issue and does not warrant blocking the PR.
TestLoadExamples loaded every example through a bytes source (ParentDir empty), but instruction_file.yaml references sibling files resolved relative to the config directory, which requires a real parent dir. Switch to a file source so the example loads, keeping the temp WorkingDir to redirect disk-writing toolsets away from examples/.
dgageot
approved these changes
Jun 26, 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.
Summary
Adds an optional
instruction_filefield on agent config so an agent's instruction can live in its own file (relative to the config file's directory) instead of being inlined in the YAML. This separates infrastructure configuration from behavioral content, which keeps version-control diffs focused, reduces merge conflicts on shared configs, and avoids YAML syntax risk when editing long prompts.Scope is Option 1 (local file references) from the issue. The inline
instructionfield stays fully supported and is not deprecated.Acceptance criteria
instruction_fileparameter supportedAgentConfigSource.ParentDir()config.Load, between migration and validationinstructionDesign decisions
config.Loadis the single choke point for every consumer (run, teamloader, OCI push, sandbox kit), so resolution happens there. The whole pipeline only ever sees the inlinedInstruction; no downstream change is needed.Instruction,InstructionFileis cleared. This keeps the in-memory config self-contained and is required for the marshalling round-trip test (TestParseExamplesAfterMarshallingreloads via a parentless bytes source)...traversal are rejected viafilepath.IsLocal, and reads are confined withos.OpenRoot(TOCTOU-safe, blocks symlink escapes). This mirrors the existing HCLfile()helper andfileSource.Read.instructionandinstruction_filecannot both be set.instruction_fileis rejected there with a clear message.share pushre-serializes the resolved config wheninstruction_fileis used, so pushed artifacts inline the contents and stay self-contained.Files
pkg/config/latest/types.goInstructionFilefield (latest config only; v0..v10 frozen)pkg/config/config.goresolveInstructionFilescalled fromLoadagent-schema.jsoninstruction_fileadded to theAgentConfigdefinitionpkg/oci/package.goinstruction_fileis usedexamples/instruction_file.yaml,examples/instructions/*.mdpkg/config/instruction_file_test.go,pkg/oci/package_test.godocs/configuration/agents/index.mdTest coverage: resolution from a relative path, file in a subdirectory, missing file,
..traversal rejected, absolute path rejected, symlink escape rejected, mutual exclusivity, parentless source rejected, empty-string treated as unset, and OCI push inlining (loads from the pushed bytes without the local file).Validation
golangci-lint run ./pkg/config/... ./pkg/oci/...reports no issues.go build ./...passes (thelint/tooling module needs a network fetch unavailable in the sandbox and is unrelated).pkg/configandpkg/ocisuites pass. The only failures in the local run areTestURLSource_Read_RejectsLocalAddresses, which are network-dependent SSRF tests unrelated to this change (they time out instead of rejecting because outbound network is blocked in the sandbox).Possible follow-up
Option 2 from the issue (
instruction_reffor OCI artifact references) is not included here and can be added later if maintainers want remote versioning.Closes #1666