fix(skills): register security skills at runtime
This commit is contained in:
@@ -32,6 +32,13 @@ const mockCreateManagers = mock(() => ({
|
||||
skillMcpManager: { disconnectAll: async () => {} },
|
||||
configHandler: async () => {},
|
||||
}))
|
||||
const mockRuntimeSkillSourceStop = mock(() => {})
|
||||
const mockCreateRuntimeSkillSourceServer = mock(
|
||||
(options: { readonly skills: readonly { readonly name: string }[] }) => ({
|
||||
url: `http://127.0.0.1:49152/${options.skills.map((skill) => skill.name).join(",")}`,
|
||||
stop: mockRuntimeSkillSourceStop,
|
||||
}),
|
||||
)
|
||||
const mockCreateTools = mock(async () => ({
|
||||
mergedSkills: [],
|
||||
availableSkills: [],
|
||||
@@ -71,6 +78,7 @@ function createTestPluginModule(): ReturnType<typeof createPluginModule> {
|
||||
isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled as never,
|
||||
createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig as never,
|
||||
createManagers: mockCreateManagers as never,
|
||||
createRuntimeSkillSourceServer: mockCreateRuntimeSkillSourceServer as never,
|
||||
createTools: mockCreateTools as never,
|
||||
createHooks: mockCreateHooks as never,
|
||||
createPluginInterface: mockCreatePluginInterface as never,
|
||||
@@ -91,6 +99,8 @@ describe("createPluginModule()", () => {
|
||||
mockInjectServerAuthIntoClient.mockClear()
|
||||
mockLoadPluginConfig.mockClear()
|
||||
mockCreateManagers.mockClear()
|
||||
mockRuntimeSkillSourceStop.mockClear()
|
||||
mockCreateRuntimeSkillSourceServer.mockClear()
|
||||
mockCreateTools.mockClear()
|
||||
mockCreateHooks.mockClear()
|
||||
mockCreatePluginInterface.mockClear()
|
||||
@@ -123,6 +133,71 @@ describe("createPluginModule()", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given bundled security skills are enabled", () => {
|
||||
it("#then startup exposes them through a runtime skill source URL", async () => {
|
||||
// given
|
||||
const pluginModule = createTestPluginModule()
|
||||
mockLoadPluginConfig.mockReturnValue({})
|
||||
|
||||
// when
|
||||
await pluginModule.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof pluginModule.server>[0])
|
||||
|
||||
// then
|
||||
const sourceArgs = mockCreateRuntimeSkillSourceServer.mock.calls.at(0)?.[0]
|
||||
expect(sourceArgs?.skills.map((skill) => skill.name)).toEqual([
|
||||
"security-research",
|
||||
"security-review",
|
||||
])
|
||||
expect(mockCreateManagers.mock.calls.at(0)?.[0]).toMatchObject({
|
||||
runtimeSkillSourceUrl: "http://127.0.0.1:49152/security-research,security-review",
|
||||
})
|
||||
})
|
||||
|
||||
it("#then dispose stops the runtime skill source", async () => {
|
||||
// given
|
||||
const pluginModule = createTestPluginModule()
|
||||
mockLoadPluginConfig.mockReturnValue({})
|
||||
|
||||
// when
|
||||
const hooks: Awaited<ReturnType<typeof pluginModule.server>> & {
|
||||
dispose?: () => Promise<void>
|
||||
} = await pluginModule.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof pluginModule.server>[0])
|
||||
await hooks.dispose?.()
|
||||
|
||||
// then
|
||||
expect(mockRuntimeSkillSourceStop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given security-research is disabled", () => {
|
||||
it("#then startup still exposes security-review through the runtime skill source", async () => {
|
||||
// given
|
||||
const pluginModule = createTestPluginModule()
|
||||
mockLoadPluginConfig.mockReturnValue({
|
||||
disabled_skills: ["security-research"],
|
||||
})
|
||||
|
||||
// when
|
||||
await pluginModule.server({
|
||||
directory: "/tmp/project",
|
||||
client: {},
|
||||
} as Parameters<typeof pluginModule.server>[0])
|
||||
|
||||
// then
|
||||
const sourceArgs = mockCreateRuntimeSkillSourceServer.mock.calls.at(0)?.[0]
|
||||
expect(sourceArgs?.skills.map((skill) => skill.name)).toEqual(["security-review"])
|
||||
expect(mockCreateManagers.mock.calls.at(0)?.[0]).toMatchObject({
|
||||
runtimeSkillSourceUrl: "http://127.0.0.1:49152/security-review",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given duplicate OMO plugin entries are configured", () => {
|
||||
it("#then startup warns and returns no prompt-producing hooks", async () => {
|
||||
// given
|
||||
|
||||
@@ -6,7 +6,9 @@ import { createHooks } from "../create-hooks"
|
||||
import { createManagers } from "../create-managers"
|
||||
import { createRuntimeTmuxConfig, isTmuxIntegrationEnabled } from "../create-runtime-tmux-config"
|
||||
import { createTools } from "../create-tools"
|
||||
import { createRuntimeSkillSourceServer, selectRuntimeSecuritySkills } from "../features/opencode-runtime-skills"
|
||||
import { initializeOpenClaw } from "../openclaw"
|
||||
import { createPluginDispose } from "../plugin-dispose"
|
||||
import { createPluginInterface } from "../plugin-interface"
|
||||
import { loadPluginConfig } from "../plugin-config"
|
||||
import { createModelCacheState } from "../plugin-state"
|
||||
@@ -30,8 +32,9 @@ import { migrateLegacyWorkspaceDirectory } from "../shared/legacy-workspace-migr
|
||||
import { injectServerAuthIntoClient } from "../shared/opencode-server-auth"
|
||||
import { startBackgroundCheck as startTmuxCheck } from "../tools/interactive-bash"
|
||||
|
||||
type HooksWithCompactionAutocontinue = Hooks & {
|
||||
type HooksWithRuntimeLifecycle = Hooks & {
|
||||
"experimental.compaction.autocontinue"?: CompactionAutocontinueHook
|
||||
dispose?: () => Promise<void>
|
||||
}
|
||||
|
||||
export type PluginModuleDeps = {
|
||||
@@ -56,6 +59,7 @@ export type PluginModuleDeps = {
|
||||
createModelCacheState: typeof createModelCacheState
|
||||
createManagers: typeof createManagers
|
||||
createTools: typeof createTools
|
||||
createRuntimeSkillSourceServer: typeof createRuntimeSkillSourceServer
|
||||
createHooks: typeof createHooks
|
||||
createPluginInterface: typeof createPluginInterface
|
||||
}
|
||||
@@ -82,6 +86,7 @@ const defaultPluginModuleDeps: PluginModuleDeps = {
|
||||
createModelCacheState,
|
||||
createManagers,
|
||||
createTools,
|
||||
createRuntimeSkillSourceServer,
|
||||
createHooks,
|
||||
createPluginInterface,
|
||||
}
|
||||
@@ -111,6 +116,11 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
||||
deps.injectServerAuthIntoClient(input.client)
|
||||
|
||||
const pluginConfig = deps.loadPluginConfig(input.directory, input)
|
||||
const runtimeSecuritySkills = selectRuntimeSecuritySkills(pluginConfig)
|
||||
const runtimeSkillSource =
|
||||
runtimeSecuritySkills.length > 0
|
||||
? deps.createRuntimeSkillSourceServer({ skills: runtimeSecuritySkills })
|
||||
: undefined
|
||||
deps.initI18n(pluginConfig.i18n?.locale ? { locale: pluginConfig.i18n.locale } : undefined)
|
||||
deps.setAgentSortOrder(pluginConfig.agent_order)
|
||||
|
||||
@@ -129,8 +139,12 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
||||
"[team-mode] enabled=true but team-mode skill is disabled; skill docs hidden but tools still registered (D-29)",
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[team-mode] init failed:", err)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.warn("[team-mode] init failed:", error)
|
||||
} else {
|
||||
console.warn("[team-mode] init failed:", String(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
const tmuxIntegrationEnabled = deps.isTmuxIntegrationEnabled(pluginConfig)
|
||||
@@ -154,6 +168,7 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
||||
tmuxConfig,
|
||||
modelCacheState,
|
||||
backgroundNotificationHookEnabled: isHookEnabled("background-notification"),
|
||||
runtimeSkillSourceUrl: runtimeSkillSource?.url,
|
||||
})
|
||||
|
||||
const toolsResult = await deps.createTools({
|
||||
@@ -183,12 +198,23 @@ export function createPluginModule(overrides: Partial<PluginModuleDeps> = {}): P
|
||||
tools: toolsResult.filteredTools,
|
||||
})
|
||||
|
||||
const pluginHooks: HooksWithCompactionAutocontinue = {
|
||||
const dispose = createPluginDispose({
|
||||
backgroundManager: managers.backgroundManager,
|
||||
skillMcpManager: managers.skillMcpManager,
|
||||
disposeHooks: hooks.disposeHooks,
|
||||
})
|
||||
|
||||
const pluginHooks: HooksWithRuntimeLifecycle = {
|
||||
...pluginInterface,
|
||||
|
||||
"experimental.session.compacting": createSessionCompactingHandler(hooks),
|
||||
|
||||
"experimental.compaction.autocontinue": createCompactionAutocontinueHandler(hooks),
|
||||
|
||||
dispose: async (): Promise<void> => {
|
||||
runtimeSkillSource?.stop()
|
||||
await dispose()
|
||||
},
|
||||
}
|
||||
|
||||
return pluginHooks
|
||||
|
||||
Reference in New Issue
Block a user