Summary
When a multi-agent hand declares a top-level skills = [...] allowlist, the kernel does not propagate that list into the per-role agent manifests it derives at activation. Each derived agent ends up with AgentManifest.skills = [], and sorted_enabled_skills (crates/librefang-kernel/src/kernel/mod.rs:13185) treats empty as "unrestricted" — so the prompt_context.md of every installed skill gets inlined into every role's system prompt.
For a small role-specialised agent the impact is meaningful: a focused fetcher/dispatcher agent picks up multi-thousand-token skill bodies (vault skills, browser-automation fallbacks, etc.) it has no use for, on every turn.
Repro
HAND.toml:
id = "demo"
version = "0.1.0"
name = "Demo Hand"
description = "..."
category = "communication"
# Hand-level allowlist — author's intent: only these two for everyone.
skills = ["alpha", "beta"]
[agents.worker]
name = "demo-worker"
description = "..."
[agents.worker.model]
provider = "default"
model = "default"
system_prompt = """You are demo-worker."""
With several other unrelated skills installed (charlie, delta, epsilon), activate the hand. Inspect demo-worker's system prompt:
librefang hand activate demo
# Send any message to the agent so the system prompt is rendered;
# inspect the request transcript.
Observed: the prompt's <available_skills> section AND the inlined [EXTERNAL SKILL CONTEXT] blocks include charlie, delta, epsilon in addition to alpha, beta. The hand's allowlist had no effect.
Expected: only alpha and beta are inlined.
Workaround
Add the same skills = [...] field to each [agents.<role>] section. The per-agent allowlist IS honored:
[agents.worker]
name = "demo-worker"
description = "..."
skills = ["alpha", "beta"] # workaround — duplicate the hand-level list
[agents.worker.model]
...
This works because the kernel reads e.manifest.skills per-agent at mod.rs:12058–12068 and mod.rs:13193 filters on the per-agent value.
Trace
mod.rs:9046 — hand-activation loop iterates per-role agents, building each AgentManifest. The hand-level skills field is on HandDefinition but never copied into the role's AgentManifest.skills.
mod.rs:12058 — capabilities resolution reads e.manifest.skills.clone(), which is empty for derived agents that didn't set their own.
mod.rs:13127 — cached_skill_metadata calls collect_prompt_context(skill_allowlist) with the empty slice.
mod.rs:13185 (sorted_enabled_skills) — allowlist.is_empty() || allowlist.contains(...) short-circuits to "all enabled", so every installed skill survives the filter.
Suggested fix
At hand activation (around mod.rs:9046–9051, where manifest.model.system_prompt is augmented with the resolved settings block), also copy the hand definition's top-level skills into the derived AgentManifest.skills IF the agent's own [agents.<role>] skills field is empty. Per-agent override wins; otherwise fall back to the hand-level list.
Pseudo-patch:
// In the per-role loop after manifest is materialised:
if manifest.skills.is_empty() && !def.skills.is_empty() {
manifest.skills = def.skills.clone();
}
This preserves the existing [agents.<role>].skills override semantics and makes the hand-level field do what its presence suggests.
Out of scope
- File-level skill granularity (allowlist a specific
path within a skill). The current design is whole-skill or none, which is fine.
- The
mcp_servers allowlist follows the same pattern (mod.rs:6830 already has merge logic for it). Worth auditing parity but separate ticket.
Why it matters
Per-role agents that legitimately want narrow skill surfaces have to either:
- duplicate the hand-level list per role (current workaround — boilerplate that drifts)
- accept the inlined-everything bloat (token cost on every turn)
For small dispatcher-style agents the cost is the entire raison d'être of the role; for review-style agents it's noise. Either way, the manifest field labeled skills not actually constraining skills is a quiet footgun.
Summary
When a multi-agent hand declares a top-level
skills = [...]allowlist, the kernel does not propagate that list into the per-role agent manifests it derives at activation. Each derived agent ends up withAgentManifest.skills = [], andsorted_enabled_skills(crates/librefang-kernel/src/kernel/mod.rs:13185) treats empty as "unrestricted" — so theprompt_context.mdof every installed skill gets inlined into every role's system prompt.For a small role-specialised agent the impact is meaningful: a focused fetcher/dispatcher agent picks up multi-thousand-token skill bodies (vault skills, browser-automation fallbacks, etc.) it has no use for, on every turn.
Repro
HAND.toml:With several other unrelated skills installed (
charlie,delta,epsilon), activate the hand. Inspectdemo-worker's system prompt:Observed: the prompt's
<available_skills>section AND the inlined[EXTERNAL SKILL CONTEXT]blocks includecharlie,delta,epsilonin addition toalpha,beta. The hand's allowlist had no effect.Expected: only
alphaandbetaare inlined.Workaround
Add the same
skills = [...]field to each[agents.<role>]section. The per-agent allowlist IS honored:This works because the kernel reads
e.manifest.skillsper-agent atmod.rs:12058–12068andmod.rs:13193filters on the per-agent value.Trace
mod.rs:9046— hand-activation loop iterates per-role agents, building eachAgentManifest. The hand-levelskillsfield is onHandDefinitionbut never copied into the role'sAgentManifest.skills.mod.rs:12058— capabilities resolution readse.manifest.skills.clone(), which is empty for derived agents that didn't set their own.mod.rs:13127—cached_skill_metadatacallscollect_prompt_context(skill_allowlist)with the empty slice.mod.rs:13185(sorted_enabled_skills) —allowlist.is_empty() || allowlist.contains(...)short-circuits to "all enabled", so every installed skill survives the filter.Suggested fix
At hand activation (around
mod.rs:9046–9051, wheremanifest.model.system_promptis augmented with the resolved settings block), also copy the hand definition's top-levelskillsinto the derivedAgentManifest.skillsIF the agent's own[agents.<role>] skillsfield is empty. Per-agent override wins; otherwise fall back to the hand-level list.Pseudo-patch:
This preserves the existing
[agents.<role>].skillsoverride semantics and makes the hand-level field do what its presence suggests.Out of scope
pathwithin a skill). The current design is whole-skill or none, which is fine.mcp_serversallowlist follows the same pattern (mod.rs:6830already has merge logic for it). Worth auditing parity but separate ticket.Why it matters
Per-role agents that legitimately want narrow skill surfaces have to either:
For small dispatcher-style agents the cost is the entire raison d'être of the role; for review-style agents it's noise. Either way, the manifest field labeled
skillsnot actually constraining skills is a quiet footgun.