fix(default-mode,multimodal-looker,delegate-task): preserve user-expected behavior

default-mode (system-transform):
- e5463e2db introduced auto-activation of ultrawork+ralph-loop, and
  dc2e082ac then skipped the ultrawork system prompt whenever ralph_loop
  was also enabled. Net effect: the keyword-detector still showed
  'Default ultrawork mode enabled' to the user, but the first turn had
  none of the ultrawork behavior. Loop continuation kept the ultrawork
  prefix, so the contract was honored only on later iterations.
- Drop the skip so the initial turn matches what the toast advertises.
  New matrix test pins all four (ultrawork, ralph_loop) combinations.

multimodal-looker:
- Prompt claimed 'read' and 'call_omo_agent' were available, but the
  look_at invocation runtime explicitly disables both via READ_ENABLED
  and createAgentToolAllowlist([]). Small VL models trusted the prompt
  and looped on rejected tool calls (#4116).
- Rewrite the agent prompt to describe direct-attachment analysis and
  forbid tool/agent calls. Add a consistency test that extracts the
  prompt's 'available tools' claim and compares it against the
  configured allowlist.

delegate-task (skill-resolver):
- 088693697 filtered per-agent restricted skills at the skill tool and
  builtin agent prompt layers, but delegate-task itself happily injected
  whatever skill name a caller passed. A target agent could be force-fed
  a skill marked agent: oracle just by listing it in load_skills.
- Thread the target agent through resolveSkills and silently filter
  skills whose definition.agent does not include it. Public skills with
  no restriction are unaffected. Regression test pins the bypass.
This commit is contained in:
YeonGyu-Kim
2026-05-22 00:06:56 +09:00
parent 7cce0ad230
commit 11c3da752c
7 changed files with 352 additions and 18 deletions
@@ -173,6 +173,35 @@ describe("resolveSkillContent — nativeSkills integration", () => {
expect(result.content).toContain("SHORT_NAME_BODY")
})
it("#given an agent-restricted OMO skill #when another target agent requests it #then filters the restricted skill but keeps public skills", async () => {
// given
const oracleSkillDir = join(TEST_DIR, ".opencode", "skills", "oracle-only-skill")
mkdirSync(oracleSkillDir, { recursive: true })
writeFileSync(
join(oracleSkillDir, "SKILL.md"),
"---\nname: oracle-only-skill\ndescription: Oracle only\nagent: oracle\n---\nORACLE_ONLY_BODY",
)
const publicSkillDir = join(TEST_DIR, ".opencode", "skills", "public-skill")
mkdirSync(publicSkillDir, { recursive: true })
writeFileSync(
join(publicSkillDir, "SKILL.md"),
"---\nname: public-skill\ndescription: Public skill\n---\nPUBLIC_BODY",
)
// when
const result = await resolveSkillContent(["oracle-only-skill", "public-skill"], {
directory: TEST_DIR,
targetAgent: "explore",
})
// then
expect(result.error).toBeNull()
expect(result.content).not.toContain("ORACLE_ONLY_BODY")
expect(result.content).toContain("PUBLIC_BODY")
expect(result.contents).toHaveLength(1)
})
it("#given no nativeSkills passed #when resolved #then behaves like pre-fix (no native discovery)", async () => {
// when
const result = await resolveSkillContent(["does-not-exist"], {
+31 -1
View File
@@ -6,6 +6,7 @@ import {
injectGitMasterConfig,
} from "../../features/opencode-skill-loader/skill-content"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { log } from "../../shared/logger"
import { mergeNativeSkills } from "../skill/native-skills"
import type { NativeSkillEntry } from "../skill/native-skills"
@@ -18,10 +19,18 @@ type ResolveSkillContentOptions = {
disabledSkills?: Set<string>
teamModeEnabled?: boolean
directory?: string
targetAgent?: string
nativeSkills?: DelegateTaskToolOptions["nativeSkills"]
nativeSkillEntries?: NativeSkillEntry[]
}
function isSkillAllowedForTargetAgent(skill: LoadedSkill, targetAgent: string | undefined): boolean {
const restrictedAgent = skill.definition.agent
if (!restrictedAgent) return true
if (!targetAgent) return false
return getAgentConfigKey(restrictedAgent) === getAgentConfigKey(targetAgent)
}
async function loadNativeSkillEntries(
nativeSkills: DelegateTaskToolOptions["nativeSkills"] | undefined,
nativeSkillEntries: NativeSkillEntry[] | undefined,
@@ -55,13 +64,34 @@ export async function resolveSkillContent(
const resolved = new Map<string, string>()
const notFound: string[] = []
let unfilteredDiscoveredSkills: LoadedSkill[] | undefined
const getUnfilteredDiscoveredSkills = async (): Promise<LoadedSkill[]> => {
if (unfilteredDiscoveredSkills) return unfilteredDiscoveredSkills
unfilteredDiscoveredSkills = await discoverSkills({
includeClaudeCodePaths: true,
directory: options.directory,
})
return unfilteredDiscoveredSkills
}
for (const name of skills) {
const skill = matchSkillByName(baseSkills, name)
let skill = matchSkillByName(baseSkills, name)
if (!skill && options.browserProvider === undefined && !options.disabledSkills?.has(name)) {
skill = matchSkillByName(await getUnfilteredDiscoveredSkills(), name)
}
if (!skill) {
notFound.push(name)
continue
}
if (!isSkillAllowedForTargetAgent(skill, options.targetAgent)) {
log("[skill-resolver] filtered agent-restricted skill for delegate target", {
skill: skill.name,
restricted_agent: skill.definition.agent,
target_agent: options.targetAgent ?? "(unknown)",
})
continue
}
const template = extractSkillTemplate(skill)
if (name === "git-master") {
resolved.set(name, injectGitMasterConfig(template, options.gitMasterConfig))
+1
View File
@@ -74,6 +74,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
disabledSkills: options.disabledSkills,
teamModeEnabled: options.teamModeEnabled,
directory: options.directory,
targetAgent: delegateTaskArgs.subagent_type,
nativeSkills: options.nativeSkills,
nativeSkillEntries,
})