/** * Path D — per-sender workspace sandboxing. * * Cross-session leaks happen at the STORAGE layer, not the * outbound layer. If the agent writes user A's secret to a * shared MEMORY.md and later user B asks the agent, the LLM * reads the same file and recites the secret — no firewall on * the *output* side can prevent this without playing whack-a- * mole with hooks, channels, and SDK versions. * * The architecturally correct fix: sandbox file-system tool * calls per-sender. User A's writes go to * ``{workspaceDir}/_lumin/by-sender/{safe-A}/...`` and user B's * reads resolve relative to ``{workspaceDir}/_lumin/by-sender/{safe-B}/``. * The bot literally has no path to user A's data while * serving user B — the leak is impossible by construction. * * Why plugin-side (not server-side rule): * - Path resolution is channel-agnostic and protocol-agnostic. * - ``before_tool_call`` IS awaited and CAN return rewritten * params. We modify the path in place before the tool runs. * - No server round-trip per fs call → zero added latency. * * What this does NOT cover (yet — separate rules): * - Tools that read from outside the workspace dir (e.g. * web_fetch, http_get). Those leaks are different and need * their own egress policy (see TENANT_ISOLATION_SPEC.md L1.5). * - Subagent / nested-session sharing. Subagents inherit the * parent's workspaceDir; sandboxing applies recursively. * - Race between concurrent senders writing to the SAME * sandbox directory — handled by per-call mkdirSync (atomic). */ /** * Read-only fs tools. Shared-path matches are allowed for these * (the operator opted them in via ``shared_paths`` config). */ export declare const FS_READONLY_TOOL_NAMES: Set; /** * Write-capable fs tools. Targeting a shared path with one of * these is blocked — shared paths are read-only by default per * TENANT_ISOLATION_SPEC §2.1. A future ``shared_writable`` flag * can opt specific paths into writability. */ export declare const FS_WRITE_TOOL_NAMES: Set; /** * Every fs tool name we sandbox. Sourced from OpenClaw's * pi-coding-agent (read/edit/write/search/ls) plus the standard * shell-style readers (grep/find/cat/head/tail) operators * commonly expose to agents. Exact-match check — an attacker * can't bypass by aliasing because Lumin also blocks * unrecognised fs tools via a separate ``unknown_fs_tool`` rule. */ export declare const FS_TOOL_NAMES: Set; /** * Make a senderId safe for use as a directory name. Replaces * EVERY char outside ``[A-Za-z0-9_-]`` with ``-`` — including * dots, so an attacker can't sneak a ``..`` parent-traversal * sequence into the sender component. Real-world senderIds use * colons (``slack:U09…``) or are pure digits (Telegram chat * IDs); none of them legitimately contain ``..``. Empty / * undefined senders fall back to ``_anonymous`` so the firewall * still isolates anonymous traffic from authenticated users. */ export declare function sanitizeSenderForFs(senderId: string | undefined): string; /** * Compute the sandbox root for a given workspace + sender. * Always under ``{workspaceDir}/_lumin/by-sender/{safe}/`` — * the ``_lumin`` prefix is reserved so operators can grep / git- * ignore Lumin-managed state without colliding with the user's * own files. */ export declare function senderSandboxRoot(workspaceDir: string, senderId: string | undefined): string; /** * Decide what to do with a requested path under the per-sender * sandbox model: * * - ``inside-sandbox`` — already inside the right sandbox, allow as-is. * - ``rewrite`` — would touch user-data outside this sender's * sandbox; rewrite to the sandbox-equivalent path. * - ``outside-workspace`` — path is outside the workspace dir * entirely (e.g. ``/etc/passwd``, ``/tmp/foo``). The * workspace-isolation rule doesn't manage these; let other * firewall rules decide. * * Returns the resolved absolute path along with the verdict. */ export type SandboxVerdict = { kind: "inside-sandbox"; resolved: string; } | { kind: "rewrite"; original: string; rewritten: string; } | { kind: "outside-workspace"; resolved: string; }; export declare function classifyPath(requestedPath: string, workspaceDir: string, senderId: string | undefined): SandboxVerdict; /** * Test whether a requested path matches any of the operator's * declared shared_paths entries. Patterns are interpreted * relative to ``workspaceDir`` if relative, or as-absolute * otherwise. Examples: * * - ``"AGENTS.md"`` matches ``${ws}/AGENTS.md`` exactly. * - ``"docs/**"`` matches anything under ``${ws}/docs/``. * - ``"/etc/openclaw/*"`` matches one level under * ``/etc/openclaw/`` (absolute). * * Returns the matched pattern + the resolved absolute path of * the request (for the caller to use as the rewritten param). */ export declare function matchesSharedPath(requestedPath: string, sharedPatterns: string[], workspaceDir: string): { matched: string; resolved: string; } | undefined; /** * Outcome of applying tenant-isolation policy to a tool call. * * - undefined → no change (not an fs tool, no path field, or * the path is already inside the right sandbox). * - ``{ kind: "rewrite" }`` → params were rewritten to the * per-sender sandbox; caller should pass these to the tool. * - ``{ kind: "shared" }`` → path matched an operator-declared * shared_paths entry. Params still need to be replaced with * the resolved absolute path so the tool reads the global * file (not the per-sender copy that would otherwise apply). * - ``{ kind: "block" }`` → fail-closed. Caller should refuse * the tool call and surface ``reason`` to the audit row. */ export type SandboxOutcome = { kind: "rewrite"; params: Record; rewrittenPaths: Array<{ original: string; rewritten: string; }>; } | { kind: "shared"; params: Record; sharedMatches: Array<{ pattern: string; resolved: string; }>; } | { kind: "block"; reason: string; }; /** * Apply sandboxing to a tool call's params. * * @param sharedPaths - Operator-declared paths that bypass the * per-sender sandbox (read-only). Glob patterns relative to * ``workspaceDir`` or absolute. Defaults to none. * @param failClosed - When true, missing ``workspaceDir`` for an * fs tool returns a ``block`` outcome instead of silently * passing through. Production should set this to true; the * default (false) preserves backward-compatible fail-warn * behaviour. * * Side effect: ensures the destination directory exists on disk * for rewrite outcomes (mkdirSync recursive — idempotent and * concurrency-safe). */ export declare function sandboxToolParams(args: { toolName: string; params: Record; senderId: string | undefined; workspaceDir: string | undefined; sharedPaths?: string[]; failClosed?: boolean; }): SandboxOutcome | undefined; //# sourceMappingURL=sender-sandbox.d.ts.map