feat(policies): backend-driven policy enforcement (frontend) (#6598)

## Summary
Adds the **Policies** feature (proprietary, behind the
`POLICIES_ENABLED` flag): backend-driven enforcement that runs a fixed
tool pipeline on documents, docked in the right tool sidebar alongside
Tools.

## Highlights
- **Policy catalog** — 5 categories; **Security** is wired (redact PII +
sanitize), the others are marked "Coming soon".
- **Backend as source of truth** — policies persist via the Policies
engine (`/api/v1/policies`), one policy per category, with a local cache
+ offline fallback.
- **Auto-run** — enabled policies run on every uploaded file: dispatch →
poll → import outputs into the workspace.
- **Security redact config** — PII preset dropdown + custom word/regex
entry + advanced options; tool params map to the backend endpoint
fields.
- **Activity feed** with retry on failures; **file badges** showing
which policies ran on a file (sidebar + files page), tinted to the
policy colour.
- Reuses the **Watched Folders** engine for each policy's backing
folder; policy-owned folders are filtered out of the Watched Folders UI.

## Notes
- Gated by `POLICIES_ENABLED` (true in proprietary, false in core) —
unreachable in the open-source build.
- Frontend-only diff; depends on the backend Policies engine and the
merged Watched Folders feature.
This commit is contained in:
Reece Browne
2026-06-10 15:57:08 +01:00
committed by GitHub
parent ebc28b0a14
commit 8dde4262ec
91 changed files with 7250 additions and 99 deletions
@@ -0,0 +1,96 @@
import { Suspense } from "react";
import { Loader } from "@mantine/core";
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
import { Card } from "@shared/components/Card";
import { PolicyRedactConfig } from "@app/components/policies/PolicyRedactConfig";
import type { ToolRegistry } from "@app/data/toolsTaxonomy";
import type { ToolId } from "@app/types/toolId";
/** One tool in a policy's fixed chain: whether it runs + its configured params. */
export interface PolicyToolState {
/** Frontend tool-registry id (also the registry key + the thing we map to an endpoint). */
operation: string;
/** Whether this tool runs as part of the policy (the per-tool on/off). */
enabled: boolean;
/** Tool-specific parameters (the shape its endpoint accepts). */
parameters: Record<string, unknown>;
}
interface PolicyToolConfigProps {
/** The policy's fixed tool chain — locked (no add/remove), only configurable. */
tools: PolicyToolState[];
toolRegistry: Partial<ToolRegistry>;
onChange: (tools: PolicyToolState[]) => void;
/** Read-only when the policy is managed / the user can't configure. */
editable?: boolean;
}
/**
* Locked, configure-only tool panel for a policy. The chain is fixed (you can't
* add or remove tools); each tool is a section that renders its OWN settings form
* from the tool registry (`automationSettings`) — the same forms the automation
* builder uses — so the config is generated from the tools in the workflow, not
* hardcoded per policy. The parameters produced here are exactly what the backend
* engine POSTs to each tool's endpoint.
*/
export function PolicyToolConfig({
tools,
toolRegistry,
onChange,
editable = true,
}: PolicyToolConfigProps) {
const patchTool = (index: number, patch: Partial<PolicyToolState>) =>
onChange(tools.map((t, i) => (i === index ? { ...t, ...patch } : t)));
return (
<div className="pol-tool-config">
{tools.map((tool, index) => {
const entry = toolRegistry[tool.operation as ToolId];
const Settings = entry?.automationSettings ?? null;
return (
<Card key={tool.operation} padding="none">
<div className="pol-tool-head">
<span className="pol-tool-icon">{entry?.icon}</span>
<span className="pol-tool-name">
{entry?.name ?? tool.operation}
</span>
<ToggleSwitch
size="sm"
checked={tool.enabled}
disabled={!editable}
onChange={(checked) => patchTool(index, { enabled: checked })}
aria-label={`Enable ${entry?.name ?? tool.operation}`}
/>
</div>
{tool.enabled &&
(tool.operation === "redact" ? (
// Redact has a bespoke config: PII preset dropdown + a custom
// word/regex field + advanced options, mode + regex locked on.
<div className="pol-tool-body">
<PolicyRedactConfig
parameters={tool.parameters}
onChange={(parameters) => patchTool(index, { parameters })}
disabled={!editable}
/>
</div>
) : Settings ? (
<div className="pol-tool-body">
<Suspense fallback={<Loader size="sm" />}>
<Settings
parameters={tool.parameters}
onParameterChange={(key: string, value: unknown) =>
patchTool(index, {
parameters: { ...tool.parameters, [key]: value },
})
}
disabled={!editable}
/>
</Suspense>
</div>
) : null)}
</Card>
);
})}
</div>
);
}