Files
fusion/plugins/fusion-plugin-omp-runtime/src/acp/sanitize.ts
gsxdsm b563b12662 feat: add Oh My Pi (omp) ACP runtime plugin (#2083)
## Summary

- Add `fusion-plugin-omp-runtime` so Fusion agents can run through
operator-installed **Oh My Pi (`omp`)** over the [Agent Client
Protocol](https://omp.sh/docs/acp) (`omp acp`).
- Wire staged/bundled install, Settings → Authentication card (enable +
binary path), model discovery (`omp models` → `omp-cli/*`), and MCP
eligibility for runtime id `omp`.
- Forward Fusion `systemPrompt` via ACP `session/new`
`_meta.systemPromptOverride`.

## How operators use it

1. Install/auth `omp` (credentials under `~/.omp`).
2. Enable **Oh My Pi — via omp ACP** in Settings → Authentication
(optional binary path).
3. Set agent **Runtime Source → OMP Runtime** (`runtimeHint: "omp"`), or
pick an `omp-cli/*` model when enabled.

## Known v1 gaps

- No Grok-style Fusion `fn_*` loopback tool bridge yet (operator MCP is
forwarded; in-process custom tools are not).
- Model is fixed at spawn (`omp --model … acp`); no mid-session Fusion
model switch.

## Test plan

- [x] `pnpm --filter @fusion-plugin-examples/omp-runtime test` (unit +
live ACP when `omp` is on PATH)
- [x] Auth routes: `POST /api/auth/omp-cli`, `GET
/api/providers/omp-cli/status`
- [x] Engine `runtimeSupportsMcp("omp")`
- [ ] Manual: enable card in dashboard, select OMP runtime on an agent,
run a short chat turn

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Oh My Pi (OMP) CLI support as an ACP-backed runtime and model
provider, including model discovery and probing.
* Added dashboard auth/status controls to enable OMP, check readiness,
and configure the local binary path (with validation).
* Exposed OMP custom `fn_*` tools via an MCP loopback bridge, plus
optional filesystem capabilities and stricter tool permission gating.
* **Documentation**
* Added/expanded OMP runtime contract and integration docs (including
the ACP session/handshake flow).
* **Tests**
* Added Vitest coverage for settings wiring, provider status, model
discovery, runtime sessions, permissions, MCP bridging, and live
connectivity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 08:18:52 -07:00

82 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Vendored ACP client from fusion-plugin-acp-runtime — see ./VENDORED.md (FNXC:GrokAcp 2026-07-11-16:00). */
// Untrusted-input sanitization helpers (U6 / Risk S7).
//
// Every string an ACP agent emits — text/thinking deltas, tool `title`, plan
// text, `sessionId`, `toolCallId` — is untrusted input. Before any such string
// reaches a Fusion callback, a log, the UI, or (worst) a filesystem path, it must
// be neutralized:
//
// - `stripControlSequences` removes ANSI/OSC escapes and C0/C1 control chars so
// a crafted string cannot inject terminal escapes / rewrite log lines.
// - `boundString` truncates oversized content (Risk S5) with a visible marker.
// - `boundIdentifier` bounds an agent-supplied id and strips path separators /
// NUL bytes so the id can never be interpolated into a filesystem path
// unsanitized.
/** Default cap for an agent-supplied identifier (sessionId, toolCallId). */
export const DEFAULT_IDENTIFIER_MAX = 256;
/** Marker appended when `boundString` truncates its input. */
export const TRUNCATION_MARKER = "…[truncated]";
// ANSI escape sequences:
// CSI / SGR: ESC [ ... <final byte>
// OSC: ESC ] ... (BEL | ST)
// other ESC-prefixed two-char sequences (e.g. ESC ( B)
const ANSI_PATTERN =
// eslint-disable-next-line no-control-regex
/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[ -/]*[0-~]/g;
// Non-printable control chars to drop. C0 = \x00–\x1F, DEL = \x7F, C1 = \x80–\x9F.
// We KEEP \n (\x0A) and \t (\x09) — they are legitimate whitespace in agent text.
// eslint-disable-next-line no-control-regex
const CONTROL_CHARS_PATTERN = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
/**
* Remove ANSI escape sequences (CSI/SGR/OSC) and non-printable C0/C1 control
* characters from an untrusted string. Preserves `\n` and `\t`. Never throws —
* a non-string input yields an empty string.
*/
export function stripControlSequences(text: string): string {
if (typeof text !== "string" || text === "") return "";
return text.replace(ANSI_PATTERN, "").replace(CONTROL_CHARS_PATTERN, "");
}
/**
* Truncate `text` to at most `max` characters, appending a short truncation
* marker when the input is cut. A non-positive `max` yields an empty string; a
* non-string input yields an empty string. The returned string is never longer
* than `max` (the marker replaces the tail of the budget, it is not added on
* top).
*/
export function boundString(text: string, max: number): string {
if (typeof text !== "string" || text === "") return "";
if (!Number.isFinite(max) || max <= 0) return "";
if (text.length <= max) return text;
if (max <= TRUNCATION_MARKER.length) {
return text.slice(0, max);
}
return text.slice(0, max - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
}
/**
* Bound an agent-supplied identifier to a sane length and strip anything that
* could let it escape into a filesystem path: path separators (`/`, `\`), NUL
* bytes, control chars, and `..` traversal segments are removed. The result is
* a flat, length-bounded token safe to use as a Map key or a single path
* component. A non-string / empty input yields `""`.
*/
export function boundIdentifier(id: string, max: number = DEFAULT_IDENTIFIER_MAX): string {
if (typeof id !== "string" || id === "") return "";
const cap = Number.isFinite(max) && max > 0 ? max : DEFAULT_IDENTIFIER_MAX;
// Drop ANSI/control first, then path-dangerous characters, then traversal.
let cleaned = stripControlSequences(id)
// eslint-disable-next-line no-control-regex
.replace(/\x00/g, "")
.replace(/[/\\]/g, "_");
// Collapse any remaining `..` traversal tokens (after separators were removed
// a `..` cannot point anywhere, but normalize it away for defense in depth).
cleaned = cleaned.replace(/\.\.+/g, "_");
return cleaned.slice(0, cap);
}