Merge pull request #3656 from code-yeongyu/feature/hide-grep-glob-for-frontier-agents
fix(agents): hide grep glob for frontier agents
This commit is contained in:
@@ -8,6 +8,7 @@ import { applyEnvironmentContext } from "./environment-context"
|
||||
import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides"
|
||||
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
|
||||
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"
|
||||
import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard"
|
||||
|
||||
export function maybeCreateHephaestusConfig(input: {
|
||||
disabledAgents: string[]
|
||||
@@ -89,6 +90,13 @@ export function maybeCreateHephaestusConfig(input: {
|
||||
}
|
||||
|
||||
const resolvedModel = hephaestusConfig.model ?? ""
|
||||
hephaestusConfig.permission = applyFrontierToolSchemaPermission(
|
||||
hephaestusConfig.permission,
|
||||
resolvedModel,
|
||||
hephaestusOverride?.permission,
|
||||
(hephaestusOverride as { tools?: Record<string, boolean> } | undefined)?.tools
|
||||
)
|
||||
|
||||
const gptDeny = getGptApplyPatchPermission(resolvedModel)
|
||||
if (Object.keys(gptDeny).length > 0 && hephaestusConfig.permission) {
|
||||
Object.assign(hephaestusConfig.permission, gptDeny)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { maybeCreateSisyphusConfig } from "./sisyphus-agent";
|
||||
import type { AgentOverrides } from "../types";
|
||||
@@ -12,7 +14,7 @@ describe("maybeCreateSisyphusConfig", () => {
|
||||
model: "openai/gpt-5.4",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
@@ -46,7 +48,7 @@ describe("maybeCreateSisyphusConfig", () => {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
@@ -73,6 +75,178 @@ describe("maybeCreateSisyphusConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given Opus 4.7 model with user override allowing grep and glob", () => {
|
||||
test("#when config is created #then grep and glob are still denied", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
sisyphus: {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
permission: {
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateSisyphusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["anthropic/claude-opus-4-7"]),
|
||||
systemDefaultModel: "anthropic/claude-opus-4-7",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given GPT 5.5 model with user override allowing grep and glob", () => {
|
||||
test("#when config is created #then grep and glob are still denied", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
sisyphus: {
|
||||
model: "openai/gpt-5.5",
|
||||
permission: {
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateSisyphusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.5"]),
|
||||
systemDefaultModel: "openai/gpt-5.5",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given frontier default model with category override to non-frontier model", () => {
|
||||
test("#when config is created #then stale grep and glob denies are cleared", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
sisyphus: {
|
||||
category: "non-frontier",
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {
|
||||
"non-frontier": {
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
};
|
||||
|
||||
// when
|
||||
const config = maybeCreateSisyphusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"]),
|
||||
systemDefaultModel: "anthropic/claude-opus-4-7",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.model).toBe("openai/gpt-5.4");
|
||||
expect(config?.permission).not.toHaveProperty("grep");
|
||||
expect(config?.permission).not.toHaveProperty("glob");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given non-frontier model with user override denying grep and glob", () => {
|
||||
test("#when config is created #then explicit user denies are preserved", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
sisyphus: {
|
||||
model: "openai/gpt-5.4",
|
||||
permission: {
|
||||
grep: "deny",
|
||||
glob: "deny",
|
||||
} as Record<string, "deny">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateSisyphusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "openai/gpt-5.4",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given non-frontier model with legacy user tools denying grep and glob", () => {
|
||||
test("#when config is created #then explicit legacy denies are preserved", () => {
|
||||
// given
|
||||
const legacyOverride = {
|
||||
model: "openai/gpt-5.4",
|
||||
tools: {
|
||||
grep: false,
|
||||
glob: false,
|
||||
},
|
||||
};
|
||||
const agentOverrides: AgentOverrides = {
|
||||
sisyphus: legacyOverride,
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateSisyphusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "openai/gpt-5.4",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given generic GPT model with user override allowing apply_patch", () => {
|
||||
test("#when config is created #then apply_patch is still denied", () => {
|
||||
// given
|
||||
@@ -81,7 +255,7 @@ describe("maybeCreateSisyphusConfig", () => {
|
||||
model: "openai/gpt-4o",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { applyOverrides } from "./agent-overrides"
|
||||
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
|
||||
import { createSisyphusAgent } from "../sisyphus"
|
||||
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard"
|
||||
import { applyFrontierToolSchemaPermission } from "../frontier-tool-schema-guard"
|
||||
|
||||
export function maybeCreateSisyphusConfig(input: {
|
||||
disabledAgents: string[]
|
||||
@@ -83,6 +84,13 @@ export function maybeCreateSisyphusConfig(input: {
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory)
|
||||
|
||||
const resolvedModel = sisyphusConfig.model ?? ""
|
||||
sisyphusConfig.permission = applyFrontierToolSchemaPermission(
|
||||
sisyphusConfig.permission,
|
||||
resolvedModel,
|
||||
sisyphusOverride?.permission,
|
||||
(sisyphusOverride as { tools?: Record<string, boolean> } | undefined)?.tools
|
||||
)
|
||||
|
||||
const gptDeny = getGptApplyPatchPermission(resolvedModel)
|
||||
if (Object.keys(gptDeny).length > 0 && sisyphusConfig.permission) {
|
||||
Object.assign(sisyphusConfig.permission, gptDeny)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { isGpt5_5Model } from "./types"
|
||||
import type { PermissionValue } from "../shared/permission-compat"
|
||||
|
||||
const FRONTIER_TOOL_SCHEMA_NAMES = ["grep", "glob"] as const
|
||||
type MutablePermission = Record<string, PermissionValue | Record<string, PermissionValue>>
|
||||
|
||||
function isOpus47Model(model: string): boolean {
|
||||
const modelName = model.includes("/") ? (model.split("/").pop() ?? model) : model
|
||||
return modelName.toLowerCase().includes("claude-opus-4-7")
|
||||
}
|
||||
|
||||
export function getFrontierToolSchemaPermission(model: string): Record<string, "deny"> {
|
||||
return isOpus47Model(model) || isGpt5_5Model(model)
|
||||
? { grep: "deny" as const, glob: "deny" as const }
|
||||
: {}
|
||||
}
|
||||
|
||||
export function applyFrontierToolSchemaPermission(
|
||||
permission: AgentConfig["permission"] | undefined,
|
||||
model: string,
|
||||
explicitPermission?: AgentConfig["permission"],
|
||||
explicitTools?: Record<string, boolean>
|
||||
): AgentConfig["permission"] | undefined {
|
||||
if (!permission) return permission
|
||||
|
||||
const nextPermission: MutablePermission = { ...permission }
|
||||
const explicitPermissionMap = explicitPermission as MutablePermission | undefined
|
||||
const frontierDeny = getFrontierToolSchemaPermission(model)
|
||||
if (Object.keys(frontierDeny).length > 0) {
|
||||
Object.assign(nextPermission, frontierDeny)
|
||||
return nextPermission as AgentConfig["permission"]
|
||||
}
|
||||
|
||||
for (const toolName of FRONTIER_TOOL_SCHEMA_NAMES) {
|
||||
if (explicitPermissionMap?.[toolName] === "deny") continue
|
||||
if (explicitTools?.[toolName] === false) continue
|
||||
delete nextPermission[toolName]
|
||||
}
|
||||
return nextPermission as AgentConfig["permission"]
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
getHephaestusPromptSource,
|
||||
@@ -321,7 +323,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
|
||||
model: "openai/gpt-5.4",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
@@ -355,7 +357,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
@@ -389,7 +391,7 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
|
||||
model: "openai/gpt-4o",
|
||||
permission: {
|
||||
apply_patch: "allow",
|
||||
},
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
@@ -414,4 +416,176 @@ describe("maybeCreateHephaestusConfig GPT apply_patch guard", () => {
|
||||
expect(config?.permission).toHaveProperty("apply_patch", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given Opus 4.7 model with user override allowing grep and glob", () => {
|
||||
test("#when config is created #then grep and glob are still denied", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
hephaestus: {
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
permission: {
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateHephaestusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["anthropic/claude-opus-4-7"]),
|
||||
systemDefaultModel: "anthropic/claude-opus-4-7",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given GPT 5.5 model with user override allowing grep and glob", () => {
|
||||
test("#when config is created #then grep and glob are still denied", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
hephaestus: {
|
||||
model: "openai/gpt-5.5",
|
||||
permission: {
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
} as Record<string, "allow">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateHephaestusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.5"]),
|
||||
systemDefaultModel: "openai/gpt-5.5",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given frontier default model with category override to non-frontier model", () => {
|
||||
test("#when config is created #then stale grep and glob denies are cleared", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
hephaestus: {
|
||||
category: "non-frontier",
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {
|
||||
"non-frontier": {
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
};
|
||||
|
||||
// when
|
||||
const config = maybeCreateHephaestusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.5", "openai/gpt-5.4"]),
|
||||
systemDefaultModel: "openai/gpt-5.5",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.model).toBe("openai/gpt-5.4");
|
||||
expect(config?.permission).not.toHaveProperty("grep");
|
||||
expect(config?.permission).not.toHaveProperty("glob");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given non-frontier model with user override denying grep and glob", () => {
|
||||
test("#when config is created #then explicit user denies are preserved", () => {
|
||||
// given
|
||||
const agentOverrides: AgentOverrides = {
|
||||
hephaestus: {
|
||||
model: "openai/gpt-5.4",
|
||||
permission: {
|
||||
grep: "deny",
|
||||
glob: "deny",
|
||||
} as Record<string, "deny">,
|
||||
},
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateHephaestusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "openai/gpt-5.4",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#given non-frontier model with legacy user tools denying grep and glob", () => {
|
||||
test("#when config is created #then explicit legacy denies are preserved", () => {
|
||||
// given
|
||||
const legacyOverride = {
|
||||
model: "openai/gpt-5.4",
|
||||
tools: {
|
||||
grep: false,
|
||||
glob: false,
|
||||
},
|
||||
};
|
||||
const agentOverrides: AgentOverrides = {
|
||||
hephaestus: legacyOverride,
|
||||
};
|
||||
const mergedCategories: Record<string, CategoryConfig> = {};
|
||||
|
||||
// when
|
||||
const config = maybeCreateHephaestusConfig({
|
||||
disabledAgents: [],
|
||||
agentOverrides,
|
||||
availableModels: new Set(["openai/gpt-5.4"]),
|
||||
systemDefaultModel: "openai/gpt-5.4",
|
||||
isFirstRunNoCache: false,
|
||||
availableAgents: [],
|
||||
availableSkills: [],
|
||||
availableCategories: [],
|
||||
mergedCategories,
|
||||
useTaskSystem: false,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(config?.permission).toHaveProperty("grep", "deny");
|
||||
expect(config?.permission).toHaveProperty("glob", "deny");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "../dynamic-agent-prompt-builder";
|
||||
import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder";
|
||||
import { getGptApplyPatchPermission } from "../gpt-apply-patch-guard";
|
||||
import { getFrontierToolSchemaPermission } from "../frontier-tool-schema-guard";
|
||||
|
||||
import { buildHephaestusPrompt as buildGptPrompt } from "./gpt";
|
||||
import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex";
|
||||
@@ -139,6 +140,7 @@ export function createHephaestusAgent(
|
||||
permission: {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
...getFrontierToolSchemaPermission(model),
|
||||
...getGptApplyPatchPermission(model),
|
||||
} as AgentConfig["permission"],
|
||||
reasoningEffort: "medium",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4";
|
||||
import { buildGpt55SisyphusPrompt } from "./sisyphus/gpt-5-5";
|
||||
import { buildTaskManagementSection } from "./sisyphus/default";
|
||||
import { getGptApplyPatchPermission } from "./gpt-apply-patch-guard";
|
||||
import { getFrontierToolSchemaPermission } from "./frontier-tool-schema-guard";
|
||||
|
||||
const MODE: AgentMode = "primary";
|
||||
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
@@ -501,6 +502,7 @@ export function createSisyphusAgent(
|
||||
permission: {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
...getFrontierToolSchemaPermission(model),
|
||||
...getGptApplyPatchPermission(model),
|
||||
} as AgentConfig["permission"],
|
||||
reasoningEffort: "medium",
|
||||
@@ -527,6 +529,7 @@ export function createSisyphusAgent(
|
||||
permission: {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
...getFrontierToolSchemaPermission(model),
|
||||
...getGptApplyPatchPermission(model),
|
||||
} as AgentConfig["permission"],
|
||||
reasoningEffort: "medium",
|
||||
@@ -567,6 +570,7 @@ export function createSisyphusAgent(
|
||||
const permission = {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
...getFrontierToolSchemaPermission(model),
|
||||
...getGptApplyPatchPermission(model),
|
||||
} as AgentConfig["permission"];
|
||||
const base = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { createOracleAgent } from "./oracle"
|
||||
import { createLibrarianAgent } from "./librarian"
|
||||
@@ -6,6 +8,7 @@ import { createMomusAgent } from "./momus"
|
||||
import { createMetisAgent } from "./metis"
|
||||
import { createAtlasAgent } from "./atlas"
|
||||
import { createSisyphusAgent } from "./sisyphus"
|
||||
import { createHephaestusAgent } from "./hephaestus"
|
||||
|
||||
const TEST_MODEL = "anthropic/claude-sonnet-4-5"
|
||||
|
||||
@@ -131,4 +134,47 @@ describe("read-only agent tool restrictions", () => {
|
||||
expect(claudePermission["apply_patch"]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sisyphus and Hephaestus frontier tool schema restrictions", () => {
|
||||
test("deny grep and glob for Opus 4.7 and GPT 5.5 models", () => {
|
||||
// given
|
||||
const frontierAgents = [
|
||||
createSisyphusAgent("anthropic/claude-opus-4-7"),
|
||||
createSisyphusAgent("openai/gpt-5.5"),
|
||||
createHephaestusAgent("anthropic/claude-opus-4-7"),
|
||||
createHephaestusAgent("openai/gpt-5.5"),
|
||||
]
|
||||
|
||||
// when
|
||||
const permissions = frontierAgents.map(
|
||||
(agent) => (agent.permission ?? {}) as Record<string, string>,
|
||||
)
|
||||
|
||||
// then
|
||||
for (const permission of permissions) {
|
||||
expect(permission.grep).toBe("deny")
|
||||
expect(permission.glob).toBe("deny")
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps grep and glob available for other models", () => {
|
||||
// given
|
||||
const otherAgents = [
|
||||
createSisyphusAgent("anthropic/claude-sonnet-4-5"),
|
||||
createSisyphusAgent("openai/gpt-5.4"),
|
||||
createHephaestusAgent("openai/gpt-5.4"),
|
||||
]
|
||||
|
||||
// when
|
||||
const permissions = otherAgents.map(
|
||||
(agent) => (agent.permission ?? {}) as Record<string, string>,
|
||||
)
|
||||
|
||||
// then
|
||||
for (const permission of permissions) {
|
||||
expect(permission.grep).toBeUndefined()
|
||||
expect(permission.glob).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:t
|
||||
import type { TmuxConfig } from '../../config/schema'
|
||||
import type { WindowState, PaneAction } from './types'
|
||||
import type { ActionResult, ExecuteContext } from './action-executor'
|
||||
import type { TmuxUtilDeps } from './manager'
|
||||
import type { TmuxSessionManager as TmuxSessionManagerType, TmuxUtilDeps } from './manager'
|
||||
import * as sharedModule from '../../shared'
|
||||
|
||||
type ExecuteActionsResult = {
|
||||
@@ -287,20 +287,95 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
test('falls back to default port when serverUrl has port 0', async () => {
|
||||
// given
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
delete process.env.OPENCODE_PORT
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
})
|
||||
|
||||
test('falls back to configured OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
// given
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
process.env.OPENCODE_PORT = '5678'
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:5678')
|
||||
})
|
||||
|
||||
test('ignores invalid OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
// given
|
||||
const previousOpenCodePort = process.env.OPENCODE_PORT
|
||||
process.env.OPENCODE_PORT = 'not-a-port'
|
||||
let manager: TmuxSessionManagerType | undefined
|
||||
try {
|
||||
mockIsInsideTmux.mockReturnValue(true)
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = {
|
||||
...createMockContext(),
|
||||
serverUrl: new URL('http://127.0.0.1:0/'),
|
||||
}
|
||||
const config = createTmuxConfig({ enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40, })
|
||||
|
||||
// when
|
||||
manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
} finally {
|
||||
if (previousOpenCodePort === undefined) {
|
||||
delete process.env.OPENCODE_PORT
|
||||
} else {
|
||||
process.env.OPENCODE_PORT = previousOpenCodePort
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
@@ -1989,7 +2064,7 @@ describe('TmuxSessionManager', () => {
|
||||
const cleanupPromise = manager.cleanup()
|
||||
|
||||
// then
|
||||
await expect(cleanupPromise).resolves.toBeUndefined()
|
||||
expect(await cleanupPromise).toBeUndefined()
|
||||
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,7 +72,11 @@ export class TmuxSessionManager {
|
||||
this.client = ctx.client
|
||||
this.tmuxConfig = tmuxConfig
|
||||
this.deps = deps
|
||||
const defaultPort = process.env.OPENCODE_PORT ?? "4096"
|
||||
const configuredPort = process.env.OPENCODE_PORT
|
||||
const parsedPort = configuredPort ? Number(configuredPort) : 4096
|
||||
const defaultPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
||||
? String(parsedPort)
|
||||
: "4096"
|
||||
const fallbackUrl = `http://localhost:${defaultPort}`
|
||||
const rawServerUrl = ctx.serverUrl?.toString()
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, it, spyOn } from "bun:test"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import * as shared from "../../shared"
|
||||
import * as slashcommand from "../../tools/slashcommand"
|
||||
import { executeSlashCommand } from "./executor"
|
||||
import * as slashcommand from "../../tools/slashcommand/command-discovery"
|
||||
|
||||
let resolveCommandsInTextSpy: { mockRestore: () => void } | undefined
|
||||
let resolveFileReferencesInTextSpy: { mockRestore: () => void } | undefined
|
||||
@@ -38,6 +39,11 @@ function restoreExecutorSpies(): void {
|
||||
discoverCommandsSyncSpy = undefined
|
||||
}
|
||||
|
||||
async function executeSlashCommand(...args: Parameters<typeof import("./executor").executeSlashCommand>): ReturnType<typeof import("./executor").executeSlashCommand> {
|
||||
const module = await import(`./executor?test=${Date.now()}-${Math.random()}`)
|
||||
return module.executeSlashCommand(...args)
|
||||
}
|
||||
|
||||
afterEach(restoreExecutorSpies)
|
||||
|
||||
function createRestrictedSkill(): LoadedSkill {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { dirname } from "path"
|
||||
import {
|
||||
resolveCommandsInText,
|
||||
resolveFileReferencesInText,
|
||||
} from "../../shared"
|
||||
import { resolveCommandsInText } from "../../shared/command-executor/resolve-commands-in-text"
|
||||
import { resolveFileReferencesInText } from "../../shared/file-reference-resolver"
|
||||
import { discoverAllSkills, type LoadedSkill, type LazyContentLoader } from "../../features/opencode-skill-loader"
|
||||
import { discoverCommandsSync } from "../../tools/slashcommand"
|
||||
import * as commandDiscovery from "../../tools/slashcommand/command-discovery"
|
||||
import type { CommandInfo as DiscoveredCommandInfo, CommandMetadata } from "../../tools/slashcommand/types"
|
||||
import type { ParsedSlashCommand } from "./types"
|
||||
|
||||
@@ -47,7 +45,7 @@ export interface ExecutorOptions {
|
||||
|
||||
|
||||
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
|
||||
const discoveredCommands = discoverCommandsSync(options?.directory ?? process.cwd(), {
|
||||
const discoveredCommands = commandDiscovery.discoverCommandsSync(options?.directory ?? process.cwd(), {
|
||||
pluginsEnabled: options?.pluginsEnabled,
|
||||
enabledPluginsOverride: options?.enabledPluginsOverride,
|
||||
})
|
||||
|
||||
@@ -4,13 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
||||
import * as skillContent from "../../features/opencode-skill-loader/skill-content"
|
||||
import * as commandDiscovery from "../slashcommand/command-discovery"
|
||||
|
||||
const discoverCommandsSync = mock(() => [])
|
||||
|
||||
mock.module("../slashcommand/command-discovery", () => ({
|
||||
discoverCommandsSync,
|
||||
}))
|
||||
|
||||
function createMockSkill(name: string): LoadedSkill {
|
||||
return {
|
||||
name,
|
||||
@@ -49,7 +46,13 @@ function createMockContext(sessionID: string): ToolContext {
|
||||
}
|
||||
}
|
||||
|
||||
async function createSkillTool(...args: Parameters<typeof import("./tools").createSkillTool>): ReturnType<typeof import("./tools").createSkillTool> {
|
||||
const module = await import(`./tools?test=${Date.now()}-${Math.random()}`)
|
||||
return module.createSkillTool(...args)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(commandDiscovery, "discoverCommandsSync").mockImplementation(discoverCommandsSync)
|
||||
spyOn(skillContent, "getAllSkills").mockImplementation(getAllSkills)
|
||||
spyOn(skillContent, "clearSkillCache").mockImplementation(clearSkillCache)
|
||||
})
|
||||
@@ -65,8 +68,7 @@ describe("createSkillTool", () => {
|
||||
const baselineDiscoverCommandsSyncCalls = discoverCommandsSync.mock.calls.length
|
||||
|
||||
// when
|
||||
const { createSkillTool } = await import("./tools")
|
||||
const skillTool = createSkillTool({})
|
||||
const skillTool = await createSkillTool({})
|
||||
|
||||
// then
|
||||
expect(discoverCommandsSync.mock.calls.length).toBe(baselineDiscoverCommandsSyncCalls)
|
||||
@@ -82,8 +84,7 @@ describe("createSkillTool", () => {
|
||||
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
|
||||
|
||||
// when
|
||||
const { createSkillTool } = await import("./tools")
|
||||
const skillTool = createSkillTool({})
|
||||
const skillTool = await createSkillTool({})
|
||||
|
||||
// then
|
||||
expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls)
|
||||
@@ -99,8 +100,7 @@ describe("createSkillTool", () => {
|
||||
const sessionContext = createMockContext("session-clear-once")
|
||||
|
||||
// when
|
||||
const { createSkillTool } = await import("./tools")
|
||||
const skillTool = createSkillTool({})
|
||||
const skillTool = await createSkillTool({})
|
||||
void skillTool.description
|
||||
await flushMicrotasks()
|
||||
await skillTool.execute({ name: "lazy-skill" }, sessionContext)
|
||||
@@ -116,8 +116,7 @@ describe("createSkillTool", () => {
|
||||
const baselineGetAllSkillsCalls = getAllSkills.mock.calls.length
|
||||
const sessionAContext = createMockContext("session-a")
|
||||
const sessionBContext = createMockContext("session-b")
|
||||
const { createSkillTool } = await import("./tools")
|
||||
const skillTool = createSkillTool({})
|
||||
const skillTool = await createSkillTool({})
|
||||
|
||||
// when
|
||||
await skillTool.execute({ name: "lazy-skill" }, sessionAContext)
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { SkillArgs, SkillLoadOptions } from "./types"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import { clearSkillCache, getAllSkills } from "../../features/opencode-skill-loader/skill-content"
|
||||
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
|
||||
import { discoverCommandsSync } from "../slashcommand/command-discovery"
|
||||
import * as commandDiscovery from "../slashcommand/command-discovery"
|
||||
import type { CommandInfo } from "../slashcommand/types"
|
||||
import { formatLoadedCommand } from "../slashcommand/command-output-formatter"
|
||||
import { formatCombinedDescription } from "./description-formatter"
|
||||
@@ -37,14 +37,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
||||
disabledSkills: options?.disabledSkills,
|
||||
browserProvider: options?.browserProvider,
|
||||
})) ?? []
|
||||
const allSkills = !options.skills
|
||||
? discovered
|
||||
: [
|
||||
...discovered,
|
||||
...options.skills.filter(
|
||||
(skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name)
|
||||
),
|
||||
]
|
||||
const allSkills = options.skills ? [...options.skills] : discovered
|
||||
|
||||
if (options.nativeSkills) {
|
||||
try {
|
||||
@@ -58,7 +51,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
||||
}
|
||||
|
||||
const getCommands = (): CommandInfo[] => {
|
||||
return discoverCommandsSync(undefined, {
|
||||
return commandDiscovery.discoverCommandsSync(undefined, {
|
||||
pluginsEnabled: options.pluginsEnabled,
|
||||
enabledPluginsOverride: options.enabledPluginsOverride,
|
||||
}) ?? []
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export { EXCLUDED_DIRS } from "../../shared/excluded-dirs"
|
||||
export { parseFrontmatter } from "../../shared/frontmatter"
|
||||
export { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||
export { getOpenCodeCommandDirs } from "../../shared/opencode-command-dirs"
|
||||
export { discoverPluginCommandDefinitions } from "../../shared/plugin-command-discovery"
|
||||
export { findProjectOpencodeCommandDirs } from "../../shared/project-discovery-dirs"
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
getOpenCodeCommandDirs,
|
||||
discoverPluginCommandDefinitions,
|
||||
EXCLUDED_DIRS,
|
||||
} from "../../shared"
|
||||
} from "./command-discovery-deps"
|
||||
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir, log } from "../../shared"
|
||||
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
||||
import { getClaudeConfigDir } from "../../shared/claude-config-dir"
|
||||
import { log } from "../../shared/logger"
|
||||
import { loadBuiltinCommands } from "../../features/builtin-commands/commands"
|
||||
import type { CommandInfo, CommandMetadata, CommandScope } from "./types"
|
||||
|
||||
export interface CommandDiscoveryOptions {
|
||||
|
||||
Reference in New Issue
Block a user