@kilocode/openclaw-security-advisor
Advanced tools
+8
-0
@@ -10,2 +10,10 @@ # Changelog | ||
| ### Added | ||
| - Plugin now forwards the active chat surface to the server as `source.channel` on every checkup request. The slash-command path reads `PluginCommandContext.channel` and the tool/natural-language path reads `OpenClawPluginToolContext.messageChannel` (tool registration converted to factory form so the ctx is accessible at tool-instantiation and closed over by `execute()`). Server uses this hint to pick a channel-appropriate format (e.g. collapsible `<details>` blocks on capable UIs, flat markdown on Telegram/Slack). Backward-compatible with older servers: the field is optional in the client payload and servers that don't declare it in their zod schema silently drop it at parse time (no coordinated release required). | ||
| ### Removed | ||
| - `maybeAppendUpdateReminder()` and the plugin-side update-reminder footer introduced in 0.1.3. The footer was presentation logic in the wrong layer — it forced a plugin release to change cadence, copy, or enablement, and only the plugin could decide when to show it. The reminder moves to the server (owner of all report rendering), where it can key off the reported `source.pluginVersion` to show a reminder only when the client is actually behind, and where admins can edit copy/cadence via the content catalog without a plugin release. | ||
| ### Fixed | ||
@@ -12,0 +20,0 @@ |
+73
-37
@@ -21,25 +21,2 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; | ||
| // Roughly 1-in-5 successful checkups append an update-check footer. This is | ||
| // intentionally path-agnostic — applied at the markdown layer in doCheckup — | ||
| // so both the LLM-driven `kilocode_security_advisor` tool and the | ||
| // LLM-bypassing `/security-checkup` slash command surface the reminder at the | ||
| // same cadence. Random rather than stateful because the plugin has no | ||
| // cross-invocation counter to key off. | ||
| const UPDATE_REMINDER_PROBABILITY = 0.2; | ||
| function maybeAppendUpdateReminder(reportMarkdown: string): string { | ||
| if (Math.random() >= UPDATE_REMINDER_PROBABILITY) { | ||
| return reportMarkdown; | ||
| } | ||
| return ( | ||
| reportMarkdown + | ||
| "\n\n---\n\n" + | ||
| "**Tip — stay current:** check the latest plugin version with " + | ||
| "`npm view @kilocode/openclaw-security-advisor version` and compare " + | ||
| "against the `pluginVersion` shown above. If you're behind, upgrade " + | ||
| "with `openclaw plugins install @kilocode/openclaw-security-advisor` " + | ||
| "followed by `openclaw gateway restart`." | ||
| ); | ||
| } | ||
| type ToolResult = { | ||
@@ -60,2 +37,24 @@ content: Array<{ type: "text"; text: string }>; | ||
| /** | ||
| * Minimal shape of the SDK's OpenClawPluginToolContext that we actually | ||
| * read. The full type lives in the SDK and is not re-exported to plugins; | ||
| * we only need the active chat surface (if any) to forward to the server | ||
| * for channel-aware report formatting. Declared structurally so we stay | ||
| * decoupled from internal SDK type evolution. | ||
| */ | ||
| type PluginToolContext = { | ||
| messageChannel?: string; | ||
| }; | ||
| type ToolFactory = (ctx: PluginToolContext) => ToolRegistration; | ||
| /** | ||
| * Minimal shape of the SDK's PluginCommandContext that we actually read. | ||
| * Same rationale as PluginToolContext — we only need the chat surface | ||
| * for the server-side formatter hint. | ||
| */ | ||
| type PluginCommandContext = { | ||
| channel?: string; | ||
| }; | ||
| type CommandRegistration = { | ||
@@ -65,3 +64,3 @@ name: string; | ||
| acceptsArgs: boolean; | ||
| handler: (ctx: unknown) => Promise<CommandResult>; | ||
| handler: (ctx: PluginCommandContext) => Promise<CommandResult>; | ||
| }; | ||
@@ -82,6 +81,22 @@ | ||
| }; | ||
| registerTool: (tool: ToolRegistration) => void; | ||
| // SDK accepts either a tool object or a factory that returns one. We | ||
| // use the factory form so we can capture `messageChannel` from the | ||
| // runtime-provided tool context at tool-creation time and forward it | ||
| // to the server on every invocation. | ||
| registerTool: (tool: ToolRegistration | ToolFactory) => void; | ||
| registerCommand: (cmd: CommandRegistration) => void; | ||
| }; | ||
| /** | ||
| * Coerce a chat-surface string from the SDK into the value we forward to | ||
| * the server. Trims, and treats empty-after-trim as "no channel known" | ||
| * so we don't send `source.channel: ""` and trigger server-side handling | ||
| * of an ambiguous signal. | ||
| */ | ||
| function normalizeChannel(raw: string | undefined): string | undefined { | ||
| if (typeof raw !== "string") return undefined; | ||
| const trimmed = raw.trim(); | ||
| return trimmed.length > 0 ? trimmed : undefined; | ||
| } | ||
| function resolveEnvToken(): string | null { | ||
@@ -121,5 +136,9 @@ return process.env.KILOCODE_API_KEY ?? process.env.KILO_API_KEY ?? null; | ||
| */ | ||
| async function runFlowSafe(api: PluginApi, apiBase: string): Promise<string> { | ||
| async function runFlowSafe( | ||
| api: PluginApi, | ||
| apiBase: string, | ||
| channel: string | undefined, | ||
| ): Promise<string> { | ||
| try { | ||
| return await runSecurityAdvisorFlow(api, apiBase); | ||
| return await runSecurityAdvisorFlow(api, apiBase, channel); | ||
| } catch (err) { | ||
@@ -146,2 +165,3 @@ const message = err instanceof Error ? err.message : String(err); | ||
| apiBase: string, | ||
| channel: string | undefined, | ||
| ): Promise<string> { | ||
@@ -158,3 +178,3 @@ // Path 0: user explicit config. If `plugins.entries.openclaw-security-advisor.config.authToken` | ||
| try { | ||
| return await doCheckup(api, apiBase, configToken); | ||
| return await doCheckup(api, apiBase, configToken, channel); | ||
| } catch (err) { | ||
@@ -177,3 +197,3 @@ if (err instanceof AuthExpiredError) { | ||
| try { | ||
| return await doCheckup(api, apiBase, envToken); | ||
| return await doCheckup(api, apiBase, envToken, channel); | ||
| } catch (err) { | ||
@@ -198,3 +218,3 @@ if (err instanceof AuthExpiredError) { | ||
| try { | ||
| return await doCheckup(api, apiBase, savedToken); | ||
| return await doCheckup(api, apiBase, savedToken, channel); | ||
| } catch (err) { | ||
@@ -225,3 +245,3 @@ if (!(err instanceof AuthExpiredError)) throw err; | ||
| try { | ||
| return await doCheckup(api, apiBase, pollResult.token); | ||
| return await doCheckup(api, apiBase, pollResult.token, channel); | ||
| } catch (err) { | ||
@@ -309,2 +329,3 @@ if (err instanceof AuthExpiredError) { | ||
| token: string, | ||
| channel: string | undefined, | ||
| ): Promise<string> { | ||
@@ -325,5 +346,9 @@ const auditResult = await runAudit(); | ||
| pluginVersion: PLUGIN_VERSION, | ||
| // Only include `channel` when we actually know it. Sending an empty | ||
| // string would force the server to special-case unknown-vs-absent; | ||
| // absent + zod's unknown-key strip on older servers are both safe. | ||
| ...(channel !== undefined ? { channel } : {}), | ||
| }, | ||
| }); | ||
| return maybeAppendUpdateReminder(response.report.markdown); | ||
| return response.report.markdown; | ||
| } | ||
@@ -371,3 +396,12 @@ | ||
| // below is deterministic. | ||
| api.registerTool({ | ||
| // | ||
| // Registered as a factory (`(ctx) => toolDef`) rather than a bare | ||
| // tool object so the SDK's OpenClawPluginToolContext is available. | ||
| // We read `ctx.messageChannel` once at tool-instantiation and close | ||
| // over it; every subsequent `execute()` forwards the same channel to | ||
| // the server for channel-aware report formatting. The factory is | ||
| // re-invoked per tool-collection event (session start, agent spawn), | ||
| // so long-running sessions that outlive a channel switch get the | ||
| // refreshed channel automatically. | ||
| api.registerTool((toolCtx: PluginToolContext) => ({ | ||
| name: "kilocode_security_advisor", | ||
@@ -395,6 +429,7 @@ description: | ||
| const apiBase = resolveApiBase(pluginConfig); | ||
| const markdown = await runFlowSafe(api, apiBase); | ||
| const channel = normalizeChannel(toolCtx.messageChannel); | ||
| const markdown = await runFlowSafe(api, apiBase, channel); | ||
| return toolResult(markdown); | ||
| }, | ||
| }); | ||
| })); | ||
@@ -411,5 +446,6 @@ // Entry point 2: slash command for deterministic invocation that | ||
| acceptsArgs: false, | ||
| handler: async (_ctx: unknown) => { | ||
| handler: async (ctx: PluginCommandContext) => { | ||
| const apiBase = resolveApiBase(pluginConfig); | ||
| const markdown = await runFlowSafe(api, apiBase); | ||
| const channel = normalizeChannel(ctx.channel); | ||
| const markdown = await runFlowSafe(api, apiBase, channel); | ||
| return { text: markdown }; | ||
@@ -416,0 +452,0 @@ }, |
+1
-1
| { | ||
| "name": "@kilocode/openclaw-security-advisor", | ||
| "version": "0.1.3", | ||
| "version": "0.1.4", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+11
-0
@@ -37,2 +37,13 @@ import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime"; | ||
| openclawVersion?: string; | ||
| /** | ||
| * Chat surface that invoked the plugin (e.g. "control-ui", "telegram", | ||
| * "slack", "discord", "kilocode-chat"). Sent when the plugin SDK exposes | ||
| * it — from `PluginCommandContext.channel` on the slash-command path and | ||
| * `OpenClawPluginToolContext.messageChannel` on the tool/natural-language | ||
| * path. The server uses this to pick a channel-appropriate format (e.g. | ||
| * collapsible `<details>` blocks on capable surfaces, flat markdown on | ||
| * Telegram/Slack). Older servers that don't know this field just drop | ||
| * it during zod parse — no coordinated release needed. | ||
| */ | ||
| channel?: string; | ||
| }; | ||
@@ -39,0 +50,0 @@ } |
64984
5.71%1171
4%