Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions AGENTS.md

Large diffs are not rendered by default.

46 changes: 43 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
field — inheritance from the CLI subprocess covers it. A missing directory
still fails before the provider is reached, and `strict_mcp_config` remains
enabled so a `.mcp.json` sitting in the new directory cannot inject
undeclared servers. Note that the `claude` CLI also reads `CLAUDE.md` and
`.claude/settings*.json` from its working directory, so pointing an agent at
an untrusted checkout means running that checkout's instructions and hooks.
undeclared servers. The `claude` CLI would also read `CLAUDE.md` and
`.claude/settings*.json` from its working directory, but the same release
pins `setting_sources` to an empty list (see the skills entry below), so
those are no longer loaded from wherever the agent happens to run.
Launch failures caused by a bad working directory are now reported as such
rather than as connection problems, and are no longer treated as retryable.
See
Expand Down Expand Up @@ -80,6 +81,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **`skills: []` is now a real opt-out on `claude-agent-sdk`, and agents no
longer inherit ambient skills from the machine.** The provider left the SDK's
`setting_sources` unset, so the `claude` CLI discovered and enabled skills
from `~/.claude/skills/`, every `.claude/skills/` up the directory tree, and
enabled plugins — none of which the workflow declared, and all of which
varied by developer machine and launch directory. Conductor documents
`skills: []` as an explicit opt-out; on this provider it silently opted out
of nothing. Two options now carry that fix together and neither is redundant:
`setting_sources` is always `[]` (the same unconditional isolation
`strict_mcp_config` already applies to MCP servers), and `skills` is always
passed explicitly, because the SDK treats an omitted list as "CLI defaults
apply" and re-defaults `setting_sources` to `["user", "project"]` whenever
`skills` is set without it.
**Behavior change:** agents on this provider also stop picking up ambient
`CLAUDE.md`, `.claude/rules/*.md`, user/project/local `settings.json`
(including `env` and `apiKeyHelper`), and hooks. Instruction files can be
supplied explicitly with `--workspace-instructions` (or `--instructions`);
settings and hooks have no equivalent, so move anything load-bearing there
into the environment. Note the SDK's skill list is a context filter, not a
sandbox — undeclared skills are hidden from the model's listing, but their
files stay readable on disk.
([#352](https://github.com/microsoft/conductor/issues/352))

- **`tools: []` no longer fails validation when no MCP servers are declared** —
the capability cross-check rejected an explicit empty allowlist against any
provider with `mcp_tools=True` and `workflow_tools_passthrough=False` (such
Expand Down Expand Up @@ -113,6 +137,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **`claude-agent-sdk` now loads skills natively instead of injecting them into
every prompt.** The provider previously took the eager preamble path on the
grounds that the SDK had no skill surface — out of date, and expensive: the
full `SKILL.md` plus the entire `references/` tree was prepended to every
call, every retry, and every validator pass (~27K tokens for the bundled
`conductor` skill). The owning Claude Code plugin is now registered on the
session and the skill enabled by its `<plugin>:<skill>` name, so the CLI reads
only the frontmatter up front and loads the body on demand. An agent with an
explicit `tools: []` is granted back the single `Skill` tool when it has
skills enabled, since an empty base tool set would otherwise leave the
declared skill unreachable. Wheels now also ship
`plugins/conductor/.claude-plugin/`; without the manifest no plugin root
resolves at all, so a non-editable install would fail every skills-enabled
agent on this provider.
([#352](https://github.com/microsoft/conductor/issues/352))

- The `claude-agent-sdk` optional dependency floor is now
`claude-agent-sdk>=0.2.82` — the 0.2.x line is what Conductor tests against.
([#335](https://github.com/microsoft/conductor/issues/335))
Expand Down
29 changes: 28 additions & 1 deletion docs/providers/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,37 @@ agents:
The `claude-agent-sdk` provider bridges MCP servers into the CLI, but not per-agent tool allowlists. Concretely:

- `runtime.mcp_servers` — **supported**. Servers are translated into the SDK's MCP config and attach alongside the built-in preset. Only declared servers attach: Conductor sets `strict_mcp_config`, so ambient Claude Code MCP settings are ignored. A narrowing per-server `tools:` filter is refused, since the SDK has no equivalent field.
- Per-agent `tools: []` — disables the built-in tools for that agent. Declared MCP servers still attach, so this combination is rejected at `conductor validate` when the workflow declares `mcp_servers`.
- Per-agent `tools: []` — disables the built-in tools for that agent, except the `Skill` loader when the agent has skills enabled (an empty tool set would otherwise leave a declared skill unreachable). Declared MCP servers still attach, so this combination is rejected at `conductor validate` when the workflow declares `mcp_servers`.
- Per-agent `tools: [list]` — **refused loudly**. Workflow tool names do not translate to Claude CLI tool IDs; silently passing them through would risk granting the wrong native tool.
- Workflow-level `tools:` combined with an agent that omits `tools:` — **rejected at `conductor validate`**. The agent would otherwise inherit that non-empty list at runtime and hit the same refusal with a confusing message. Remove the workflow-level `tools:` (so omitting `tools:` grants the preset) or set the agent's `tools: []`.
- Omitting `tools:` entirely (with no workflow-level `tools:`) — grants the full `claude_code` preset (filesystem, bash, web), matching the bare `claude` CLI experience.

### Important: Skills and ambient settings

Skills are loaded natively: the Claude Code plugin that ships a declared skill is
registered on the session and the skill enabled by its `<plugin>:<skill>` name, so
the CLI reads only the `SKILL.md` frontmatter up front and the body on demand.

Conductor also pins the SDK's `setting_sources` to an empty list on every run — the
skills counterpart to `strict_mcp_config`. Without it the `claude` CLI enables skills
the workflow never declared, varying by machine and launch directory, which made
`skills: []` a no-op on this provider.

That isolation is broader than skills. Agents on this provider do **not** pick up:

- ambient skills from `~/.claude/skills/` or any `.claude/skills/` directory
- `CLAUDE.md` and `.claude/rules/*.md`
- user, project, and local `settings.json` (including `env` and `apiKeyHelper`)
- hooks

Instruction files have a replacement: run with `--workspace-instructions` (or
`--instructions <file>`) to inject `AGENTS.md` / `CLAUDE.md` explicitly. Settings and
hooks have none — if you rely on `apiKeyHelper` for credentials, supply them through
the environment instead.

Note the SDK treats the enabled-skill list as a context filter rather than a sandbox:
undeclared skills are hidden from the model's listing and rejected by the `Skill`
tool, but their files remain readable on disk through `Read`/`Bash`.
- `temperature` and `max_tokens` are **rejected at the factory** — sampling behavior is controlled by the CLI.

### Example Claude Agent SDK Workflow
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ adopting one does not inflate the install surface for others.

| Provider | Upstream pin | Maintainer | Capability carve-outs |
|---|---|---|---|
| `claude-agent-sdk` | `claude-agent-sdk>=0.2.82` | `@lesandiz (best-effort)` | no `workflow_tools_passthrough`, no `reasoning_effort`, `prompt_injection` structured output, no `checkpoint_resume`. Supports `mcp_tools` as of [#335](https://github.com/microsoft/conductor/issues/335), except that a narrowing per-server `tools:` filter is refused (no SDK equivalent). Supports `working_dir` as of [#348](https://github.com/microsoft/conductor/issues/348) — note the `claude` CLI also loads `CLAUDE.md` and `.claude/settings*.json` from that directory, so point it only at trees you trust. |
| `claude-agent-sdk` | `claude-agent-sdk>=0.2.82` | `@lesandiz (best-effort)` | no `workflow_tools_passthrough`, no `reasoning_effort`, `prompt_injection` structured output, no `checkpoint_resume`. Supports `mcp_tools` as of [#335](https://github.com/microsoft/conductor/issues/335), except that a narrowing per-server `tools:` filter is refused (no SDK equivalent). Supports `working_dir` as of [#348](https://github.com/microsoft/conductor/issues/348); the CLI would load `CLAUDE.md` and `.claude/settings*.json` from that directory, but `setting_sources` is pinned empty as of [#352](https://github.com/microsoft/conductor/issues/352) so ambient instructions, settings, hooks, and skills are not inherited. |
| `hermes` | `hermes-agent` | `(community contribution)` | no `mcp_tools`, `prompt_injection` structured output, no `working_dir` |
| `aca` | `azure-identity>=1.19.0` | `(unassigned)` | no `workflow_tools_passthrough` (the wrapped in-container `CopilotProvider` never applies the `tools:` allowlist to the SDK session), no `working_dir` (only the separate, container-relative `sandbox.working_dir` is honored — not the generic host-resolved field), `prompt_injection` structured output (inherits the inner Copilot provider), no `checkpoint_resume` (ephemeral sandbox sessions, no volume mount). Declares `interrupt`/`max_session_seconds` as `True`, but the shipped runner MVP doesn't fully back either yet — see [Known Gaps](./aca.md#known-gaps-runner-mvp). |

Expand Down
3 changes: 3 additions & 0 deletions examples/skills-self-improving-workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# * Copilot: the skill directory is registered on the SDK session via
# `skill_directories`. The agent discovers and loads SKILL.md and
# references on demand (progressive disclosure).
# * Claude Agent SDK: also native, via the Claude Code plugin surface —
# the plugin shipping the skill is registered and the skill enabled
# by its `<plugin>:<skill>` name.
# * Claude: SKILL.md + references/*.md are eagerly prepended to the
# agent's rendered prompt inside <skills><skill name="conductor">
# ... </skill></skills> tags.
Expand Down
1 change: 1 addition & 0 deletions plugins/conductor/skills/conductor/references/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ See `examples/validator.yaml` for a complete example.

**Provider mechanism (same observable contract — "the agent has access to the named skill"):**
- **Copilot** — the resolved skill directory is registered on the SDK session via `skill_directories`, so the agent discovers and loads skill content natively (progressive disclosure via `SKILL.md` frontmatter). This is more token-efficient than eager injection.
- **Claude Agent SDK** — also native, through the Claude Code plugin surface: the plugin owning the skill is registered on the session and the skill enabled by its `<plugin>:<skill>` name. Skills the workflow did not declare are suppressed, so `skills: []` really is an opt-out and ambient skills from the machine never load.
- **Claude** — the loader reads `SKILL.md` plus every `references/*.md` file in the skill directory and prepends them to the agent's rendered prompt inside `<skills><skill name="...">...</skill></skills>` tags. Inserted between workspace instructions and the user prompt.

Not allowed on `script`, `human_gate`, `workflow`, `wait`, `set`, or `terminate` agent types. Unknown skill names fail at workflow validation time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ workflow:
skills: [string] # Skills enabled for every provider-backed agent (default: [])
# Currently registered built-ins: "conductor"
# Copilot loads natively via `skill_directories`;
# claude-agent-sdk loads natively via its plugin surface;
# Claude eagerly injects SKILL.md + references/*.md into the prompt.
mcp_servers: # MCP server configurations (ignored by claude-agent-sdk — uses CLI config)
<server_name>:
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ exclude = [

[tool.hatch.build.targets.wheel.force-include]
"plugins/conductor/skills/conductor" = "plugins/conductor/skills/conductor"
# The plugin manifest, not just the skill body: the claude-agent-sdk provider
# loads skills by registering `plugins/conductor` as a Claude Code plugin
# (`--plugin-dir`), which requires `.claude-plugin/plugin.json` to be present.
# Without it no plugin root resolves, so the skill works from a source checkout
# and every skills-enabled agent fails with a ProviderError on a wheel install.
"plugins/conductor/.claude-plugin" = "plugins/conductor/.claude-plugin"

[dependency-groups]
dev = [
Expand Down
23 changes: 16 additions & 7 deletions src/conductor/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,11 +1092,17 @@ class AgentDef(BaseModel):
* **Copilot** — skill directories are passed to the SDK session via
``skill_directories``; the model discovers and loads skill content
as relevant (progressive disclosure, token-efficient).
* **Claude / Claude Agent SDK** — ``SKILL.md`` plus
``references/*.md`` is eagerly injected into the agent's rendered
prompt, wrapped in ``<skill name="...">`` tags. There is no native
skill surface on the Anthropic API without adopting the
container/code-execution beta.
* **Claude Agent SDK** — the Claude Code plugin that owns the skill is
registered on the session and the skill is enabled by its
``<plugin>:<skill>`` name, so the CLI loads only the ``SKILL.md``
frontmatter up front. Skills the workflow did not declare are
filtered out of the model's listing instead of being inherited
from the machine.
* **Claude** — ``SKILL.md`` plus ``references/*.md`` is eagerly
injected into the agent's rendered prompt, wrapped in
``<skill name="...">`` tags. There is no native skill surface on
the Anthropic API without adopting the container/code-execution
beta.

Tri-state semantics via list presence:

Expand Down Expand Up @@ -2511,8 +2517,11 @@ def _coerce_provider(cls, value: Any) -> Any:
Skill content reaches the model differently per provider:

* **Copilot** — registered on the SDK session via ``skill_directories``
* **Claude / Claude Agent SDK** — eagerly injected into the rendered
prompt inside ``<skills><skill name="...">...</skill></skills>`` tags
* **Claude Agent SDK** — the owning plugin is registered via
``--plugin-dir`` and the skill enabled by its ``<plugin>:<skill>``
name, so the CLI loads it on demand
* **Claude** — eagerly injected into the rendered prompt inside
``<skills><skill name="...">...</skill></skills>`` tags

Defaults to an empty list (no skills). Phase 1 ships one built-in
skill (``conductor``); user-defined skill directories will be added
Expand Down
5 changes: 3 additions & 2 deletions src/conductor/config/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1617,7 +1617,8 @@ def _check_agent_tools(agent: AgentDef, provider_name: str, caps: ProviderCapabi
``workflow_tools_passthrough=False`` — ``aca``, whose in-container
runner attaches every configured MCP server unconditionally, and
``claude-agent-sdk``, where ``tools: []`` disables only the built-in
CLI tools). There is no allowlist value, empty or not, those
CLI tools — bar the ``Skill`` loader when the agent declares
skills). There is no allowlist value, empty or not, those
providers can honor, so ``tools: []`` would misleadingly pass
validation while every MCP tool stays attached. This only applies
when the workflow actually declares ``mcp_servers``: with nothing to
Expand Down Expand Up @@ -1653,7 +1654,7 @@ def _check_agent_tools(agent: AgentDef, provider_name: str, caps: ProviderCapabi
f"(capabilities.workflow_tools_passthrough=False). Remove the "
f"workflow-level 'tools:' so omitting 'tools:' grants the "
f"provider's default tool preset, or set this agent's "
f"'tools: []' to disable all tools."
f"'tools: []' to disable the built-in tools."
)

def _check_agent_capabilities(
Expand Down
8 changes: 5 additions & 3 deletions src/conductor/executor/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,18 @@ async def execute(
_verbose_log(f" Tools: {resolved_tools}")

# Resolve skill directories for providers with native skill support
# (Copilot passes these on session_kwargs; Claude has already had
# the skill content eager-injected into rendered_prompt above and
# ignores this).
# (Copilot passes these on session_kwargs, claude-agent-sdk maps them
# to plugin + skill-name options; providers without native support
# have already had the skill content eager-injected into
# rendered_prompt above and ignore this).
skill_dirs: list[str] | None = None
if getattr(self.provider, "supports_native_skills", False):
skill_names = self._resolve_skills_for_agent(agent)
if skill_names:
from conductor.skills import resolve_skill_directories

skill_dirs = [str(p) for p in resolve_skill_directories(skill_names)]
_verbose_log(f" Skills: {skill_names}")

# Execute via provider
output = await self.provider.execute(
Expand Down
3 changes: 2 additions & 1 deletion src/conductor/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ def supports_native_skills(self) -> bool:
passes resolved skill directories to :meth:`execute` via
``skill_directories`` and skips eager preamble injection — the
provider's SDK is expected to discover and load skill content
itself (e.g. Copilot's session-level ``skill_directories``).
itself (e.g. Copilot's session-level ``skill_directories``, or
the claude-agent-sdk's plugin-scoped ``skills`` option).

When ``False`` (default), the executor eagerly injects the full
``SKILL.md`` plus ``references/*.md`` content into the agent's
Expand Down
9 changes: 6 additions & 3 deletions src/conductor/providers/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,12 @@ class ProviderCapabilities(BaseModel):
:attr:`AgentProvider.supports_native_skills`:

* ``supports_native_skills=True`` — resolved skill directories are
passed to the SDK on the ``skill_directories`` kwarg of
:meth:`AgentProvider.execute` and the SDK loads skill content
itself (progressive disclosure via ``SKILL.md`` frontmatter).
passed on the ``skill_directories`` kwarg of
:meth:`AgentProvider.execute` and the provider forwards them to
its SDK in whatever shape that SDK accepts (Copilot registers the
directories as-is; claude-agent-sdk maps them to plugin roots and
qualified skill names). The SDK then loads skill content itself
(progressive disclosure via ``SKILL.md`` frontmatter).
* ``supports_native_skills=False`` — :class:`AgentExecutor` reads
every enabled skill's ``SKILL.md`` plus ``references/*.md`` and
eagerly prepends them to ``rendered_prompt`` inside
Expand Down
Loading
Loading