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
Instalación
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
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)