Step-by-step checklist for adding a new agent to openshell-image-builder, covering the Agent trait, mod.rs registration, unit tests, integration tests, and README
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Step-by-step checklist for adding a new agent to openshell-image-builder, covering the Agent trait, mod.rs registration, unit tests, integration tests, and README
argument-hint
<agent-name>
Add Agent
End-to-end checklist for making a new AI coding agent available via --agent.
Description
Adding a new agent touches five layers: the agent module, src/agent/mod.rs, unit tests, integration tests (including extending the image_tests! macro), and the README. The existing agents are the canonical reference:
claude — Claude Code CLI, curl installer, onboarding skip via .claude.json, agent-level network policy, anthropic+vertexai inference, skills at /sandbox/.claude/skills.
opencode — Opencode CLI, curl installer, per-inference config submodule pattern (one configure() per provider), all three inference providers, skills at /sandbox/.opencode/skills.
Step 1 — choose the file structure
Two patterns exist:
Simple agent (one implementation file, no per-inference config submodules) — use when the agent does not need to write provider-specific config files:
src/agent/myagent.rs
Module directory (separate submodule per supported inference provider) — use when set_inference() writes provider-specific config files into the image:
Create the file(s) chosen in Step 1. The trait lives in src/agent/mod.rs. Three methods are required (no default); the rest are optional:
use std::collections::HashMap;
use crate::inference;
pubstructMyAgent;
implsuper::Agent forMyAgent {
// --- required ---fnid(&self) -> &str {
"myagent"// must match the AgentKind variant's CLI value
}
fninstall(&self) ->String {
// Returns a Containerfile RUN instruction (and ENV PATH extension) that// installs the agent binary under the sandbox user. Follow the curl// pattern used by both existing agents:// RUN curl -fsSL https://... | sh// ENV PATH=/sandbox/.local/bin:$PATH"RUN curl -fsSL https://myagent.example.com/install.sh | sh\n\
ENV PATH=/sandbox/.myagent/bin:$PATH".to_string()
}
fnbinary_path(&self) -> &str {
// Absolute path to the agent binary inside the image.// Used to scope network policy rules to this binary only."/sandbox/.myagent/bin/myagent"
}
// --- optional (override when needed) ---fnpolicy_yaml(&self) ->String {
// Agent-level network policy fragment (merged with inference policy).// Return empty string if no agent-specific network rules are needed.// See claude.rs for an example with download.example.com allowlist.String::new()
}
fnskip_onboarding(&self, mut files: HashMap<String, String>) -> HashMap<String, String> {
// Insert config files (keyed by path relative to /sandbox) that// suppress interactive first-run prompts. Return files unchanged if// no onboarding skip is needed.
files.insert(
".myagent/settings.json".to_string(),
r#"{"onboardingCompleted":true}"#.to_string(),
);
files
}
fnsupported_inference(&self) ->Vec<inference::InferenceKind> {
// List the InferenceKind variants this agent can be configured for.// Return an empty vec if the agent has no inference integration.vec![
inference::InferenceKind::Anthropic,
inference::InferenceKind::VertexAi,
]
}
fnset_inference(&self, files: HashMap<String, String>, inference: Option<&inference::InferenceKind>, base_url: Option<&str>, model: Option<&str>) -> HashMap<String, String> {
// Write provider-specific config files into the image context.// Return files unchanged if no config is needed.
files
}
fnenv_vars(&self, inference: Option<&inference::InferenceKind>, endpoint: Option<&str>, _model: Option<&str>) -> HashMap<String, String> {
// Bake environment variables into the Containerfile ENV instruction.// Use when the agent reads configuration from env vars at runtime.
HashMap::new()
}
fnskills_dir(&self) -> &str {
// Path inside the image where skills are copied. Return "" to disable skills support."/sandbox/.myagent/skills"
}
}
Step 3 — register in src/agent/mod.rs
Four additions:
mod myagent; // 1. declare the module#[cfg(test)]pubuse myagent::MyAgent; // 2. export for test use// 3. add variant to the enum#[derive(Clone, PartialEq, ValueEnum)]pubenumAgentKind {
Claude,
Opencode,
MyAgent, // add here; add #[value(name = "myagent")] if CLI name differs
}
// 4. add arm to from_kind()pubfnfrom_kind(kind: AgentKind) ->Box<dyn Agent> {
match kind {
...
AgentKind::MyAgent => Box::new(myagent::MyAgent),
}
}
If the CLI value should differ from the Rust variant name (e.g., a hyphenated name), add #[value(name = "my-agent")] above the variant.
Step 4 — unit tests
In the agent file (src/agent/myagent.rs or src/agent/myagent/mod.rs)
Add a #[cfg(test)] mod tests block. Cover all implemented methods:
#[cfg(test)]mod tests {
use super::*;
#[test]fnid_is_myagent() {
assert_eq!(MyAgent.id(), "myagent");
}
#[test]fninstall_contains_installer_url() {
assert!(MyAgent.install().contains("https://myagent.example.com/install.sh"));
}
#[test]fninstall_extends_path() {
assert!(MyAgent.install().contains("ENV PATH="));
}
#[test]fnbinary_path_is_absolute() {
assert!(MyAgent.binary_path().starts_with('/'));
}
// If skip_onboarding() was implemented:#[test]fnskip_onboarding_writes_settings_file() {
letfiles = MyAgent.skip_onboarding(std::collections::HashMap::new());
assert!(files.contains_key(".myagent/settings.json"));
}
// If supported_inference() was implemented:#[test]fnsupported_inference_includes_anthropic() {
assert!(MyAgent.supported_inference().contains(&crate::inference::InferenceKind::Anthropic));
}
// If skills_dir() was implemented:#[test]fnskills_dir_is_correct() {
assert_eq!(MyAgent.skills_dir(), "/sandbox/.myagent/skills");
}
}
If per-inference config submodules exist (src/agent/myagent/anthropic.rs etc.), add unit tests inside each submodule following the opencode::anthropic pattern: assert the config file key, the host/model appearing in the value, and the ownership path.
In src/main.rs (#[cfg(test)])
build_policy_with_myagent_includes_binary — call build_policy() with the new agent and assert the binary path appears in the output.
If policy_yaml() returns non-empty content: build_policy_with_myagent_includes_agent_network_rule.
Also add the check_myagent_in_path helper alongside the existing check_claude_in_path and check_opencode_in_path helpers:
fncheck_myagent_in_path(image: &str) {
letout = run_in_image(image, &["which", "myagent"]);
assert!(
out.status.success(),
"myagent not found in PATH: {}",
String::from_utf8_lossy(&out.stderr)
);
}
If the agent has a distinct policy_yaml(), add check_myagent_policy(image, expected) alongside check_anthropic_policy / check_ollama_policy, and add a policy_has_myagent_rules test inside the macro body.
Update every existing image_tests! call to add has_myagent: false — there are around 24 calls (6 combinations × 4 base images); update all of them.
Part B — image singletons and accessors
Add one OnceLock and one accessor per base image × inference combination the new agent supports. Follow the naming convention exactly (<base_image>_<agent>_<inference>_image). At minimum cover all four base images with all supported inference providers:
Add every new image tag to the cleanup_images array in the #[ctor::dtor] block at the bottom of the file:
"openshell-test-ubuntu-myagent:integration",
"openshell-test-fedora-myagent:integration",
// ... one per new singleton ...
Part E — behavioural mod block
Add a mod myagent { use super::*; ... } block with:
Binary in PATH — which myagent succeeds in an image built with --agent myagent.
Onboarding skip — if skip_onboarding() writes a file, assert the file exists and its ownership is sandbox.
Inference config — if set_inference() writes config files, assert the file exists, contains the expected host/model, and is owned by sandbox.
Env var — if env_vars() returns entries, assert they are set in the image.
Skills dir — if skills_dir() is non-empty, assert the directory exists and is owned by sandbox.
Negative — assert the binary is absent in an image built without --agent myagent.
Step 6 — update .agents/skills/add-agent/SKILL.md
Add the new agent to the canonical reference list at the top of the Description section of this skill, following the same one-line format as the existing entries:
This keeps the reference list accurate for the next contributor.
Step 7 — update .agents/skills/README.md
No change needed — this skill is already registered. Skip this step.
Step 8 — update README.md
Six places reference agents; all must be kept in sync:
Intro layer list (near the top) — the bullet "One of two supported AI coding agents" lists agent names. Add the new agent in the same style.
"Installing an agent" section — add a subsection or paragraph for the new agent describing the --agent myagent flag and any prerequisites.
"Agent Supported Features" table — add a row for the new agent with Yes/No for each capability column (Onboarding skip, Skills support, etc.).
"Agent × Inference Supported Features" table — add a row per supported inference, filling in the "Inference settings", "Endpoint override", and "Model selection" columns.
"Sandbox policy" section — if the agent adds its own network policy rules, add a sentence describing what endpoints are allowed and why.
"Full option reference" table — update the --agent row's description to include the new value alongside the existing ones.
Checklist
File structure chosen (single file vs. module directory)