@kilocode/openclaw-security-advisor
Advanced tools
+1
-0
@@ -12,2 +12,3 @@ # Changelog | ||
| - KiloClaw platform detection now uses four independent signals instead of relying on a single env var, so detection holds across KiloClaw deployments of varying age. `detectPlatform()` now walks (in order, short-circuiting on the first hit): (1) `plugins.entries.kiloclaw-customizer.enabled` in `openclaw.json`, (2) `plugins.load.paths` containing the kiloclaw customizer install path, (3) `process.env.KILOCLAW_SANDBOX_ID`, (4) `process.env.KILOCODE_FEATURE === "kiloclaw"`. The two config-side signals are written by the KiloClaw controller at boot and are present on every KiloClaw instance since the customizer plugin was introduced, so they catch older deployments that predate the env-var signals. Internal signature change: `detectPlatform()` now takes the loaded openclaw config so it can inspect the config-side signals. | ||
| - First-time device auth no longer triggers a brief gateway restart after the token is captured. The plugin now registers `reload.noopPrefixes` for `plugins.entries.openclaw-security-advisor.config.authToken`, so the SecretRef patch written to `openclaw.json` after device auth is classified as a noop by the gateway reload planner instead of falling through to the default `plugins.* → restart` rule. The security checkup report is returned in the same response with no connection interruption. Scope is intentionally limited to the `authToken` field — `apiBaseUrl` and other config changes still take effect via the normal restart path. | ||
@@ -14,0 +15,0 @@ - Release workflow: consolidated post-publish git/GitHub operations into a single atomic step with retries, eliminating a race condition where the version bump commit and tag could be pushed separately. Registry verification is now informational-only and never blocks tag/release steps. |
+10
-6
@@ -129,3 +129,3 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; | ||
| try { | ||
| return await doCheckup(apiBase, configToken); | ||
| return await doCheckup(api, apiBase, configToken); | ||
| } catch (err) { | ||
@@ -148,3 +148,3 @@ if (err instanceof AuthExpiredError) { | ||
| try { | ||
| return await doCheckup(apiBase, envToken); | ||
| return await doCheckup(api, apiBase, envToken); | ||
| } catch (err) { | ||
@@ -169,3 +169,3 @@ if (err instanceof AuthExpiredError) { | ||
| try { | ||
| return await doCheckup(apiBase, savedToken); | ||
| return await doCheckup(api, apiBase, savedToken); | ||
| } catch (err) { | ||
@@ -196,3 +196,3 @@ if (!(err instanceof AuthExpiredError)) throw err; | ||
| try { | ||
| return await doCheckup(apiBase, pollResult.token); | ||
| return await doCheckup(api, apiBase, pollResult.token); | ||
| } catch (err) { | ||
@@ -276,3 +276,7 @@ if (err instanceof AuthExpiredError) { | ||
| async function doCheckup(apiBase: string, token: string): Promise<string> { | ||
| async function doCheckup( | ||
| api: PluginApi, | ||
| apiBase: string, | ||
| token: string, | ||
| ): Promise<string> { | ||
| const auditResult = await runAudit(); | ||
@@ -289,3 +293,3 @@ if (!auditResult.ok) { | ||
| source: { | ||
| platform: detectPlatform(), | ||
| platform: detectPlatform(api.runtime.config.loadConfig()), | ||
| method: "plugin", | ||
@@ -292,0 +296,0 @@ pluginVersion: PLUGIN_VERSION, |
+1
-1
| { | ||
| "name": "@kilocode/openclaw-security-advisor", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+68
-2
@@ -8,5 +8,71 @@ /** | ||
| * side of that check. | ||
| * | ||
| * Detection walks multiple independent signals in order of decreasing | ||
| * reliability across deployment age. The goal is that at least one | ||
| * signal fires on every KiloClaw instance ever deployed, regardless | ||
| * of whether the instance predates a given env var. Any hit short- | ||
| * circuits to "kiloclaw". | ||
| * | ||
| * Ordering (stopping at the first hit): | ||
| * 2. openclaw.json has `plugins.entries["kiloclaw-customizer"].enabled` | ||
| * truthy — the kiloclaw controller writes this at boot for every | ||
| * kiloclaw instance, predating any of the env-var signals. Most | ||
| * durable universal signal today. | ||
| * 3. openclaw.json `plugins.load.paths` contains the kiloclaw | ||
| * customizer install path — same writer, redundant cross-check. | ||
| * 4. `process.env.KILOCLAW_SANDBOX_ID` is set — present on every | ||
| * kiloclaw instance since 2026-03-22. | ||
| * 5. `process.env.KILOCODE_FEATURE === "kiloclaw"` — the original | ||
| * env-var signal, present on kiloclaw since 2026-02-17. | ||
| * | ||
| * We intentionally do NOT add a loose `KILOCLAW_*`-prefix heuristic; | ||
| * the four signals above are precise and one of them will hit on any | ||
| * real kiloclaw deployment. | ||
| */ | ||
| export function detectPlatform(): "kiloclaw" | "openclaw" { | ||
| return process.env.KILOCODE_FEATURE === "kiloclaw" ? "kiloclaw" : "openclaw"; | ||
| export type Platform = "kiloclaw" | "openclaw"; | ||
| const CUSTOMIZER_ID = "kiloclaw-customizer"; | ||
| const CUSTOMIZER_LOAD_PATH = | ||
| "/usr/local/lib/node_modules/@kiloclaw/kiloclaw-customizer"; | ||
| export function detectPlatform( | ||
| config: unknown, | ||
| env: NodeJS.ProcessEnv = process.env, | ||
| ): Platform { | ||
| if (hasKiloclawCustomizerEntry(config)) return "kiloclaw"; | ||
| if (hasKiloclawCustomizerLoadPath(config)) return "kiloclaw"; | ||
| if (hasKiloclawSandboxIdEnv(env)) return "kiloclaw"; | ||
| if (hasKilocodeFeatureEnv(env)) return "kiloclaw"; | ||
| return "openclaw"; | ||
| } | ||
| function hasKiloclawCustomizerEntry(config: unknown): boolean { | ||
| const entry = getPath(config, ["plugins", "entries", CUSTOMIZER_ID]); | ||
| if (!entry || typeof entry !== "object") return false; | ||
| const enabled = (entry as Record<string, unknown>).enabled; | ||
| return enabled === true; | ||
| } | ||
| function hasKiloclawCustomizerLoadPath(config: unknown): boolean { | ||
| const paths = getPath(config, ["plugins", "load", "paths"]); | ||
| return Array.isArray(paths) && paths.includes(CUSTOMIZER_LOAD_PATH); | ||
| } | ||
| function hasKiloclawSandboxIdEnv(env: NodeJS.ProcessEnv): boolean { | ||
| const v = env.KILOCLAW_SANDBOX_ID; | ||
| return typeof v === "string" && v.length > 0; | ||
| } | ||
| function hasKilocodeFeatureEnv(env: NodeJS.ProcessEnv): boolean { | ||
| return env.KILOCODE_FEATURE === "kiloclaw"; | ||
| } | ||
| function getPath(root: unknown, path: string[]): unknown { | ||
| let cur: unknown = root; | ||
| for (const key of path) { | ||
| if (!cur || typeof cur !== "object") return undefined; | ||
| cur = (cur as Record<string, unknown>)[key]; | ||
| } | ||
| return cur; | ||
| } |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
55764
6.66%1085
6.06%8
14.29%