diff --git a/docs.json b/docs.json index 931424e4..d47147d8 100644 --- a/docs.json +++ b/docs.json @@ -155,7 +155,15 @@ "product/admin/tools-and-toolsets", "product/admin/tool-call-hooks", "product/admin/ai-clients", - "product/admin/audit-ai-tool-usage" + "product/admin/audit-ai-tool-usage", + { + "group": "Agent guardrails", + "pages": [ + "product/admin/agent-guardrails", + "product/admin/agent-guardrails-configure", + "product/admin/agent-guardrails-reference" + ] + } ] }, { diff --git a/product/admin/agent-guardrails-configure.mdx b/product/admin/agent-guardrails-configure.mdx new file mode 100644 index 00000000..dd9d796b --- /dev/null +++ b/product/admin/agent-guardrails-configure.mdx @@ -0,0 +1,281 @@ +--- +title: "Configure agent guardrails" +og:title: "Configure agent guardrails - C1 docs" +description: "Create a guardrail, bind it to a surface, author rules, attach hooks and tool gates, and promote rules from Observe to Enforce." +og:description: "Create a guardrail, bind it to a surface, author rules, attach hooks and tool gates, and promote rules from Observe to Enforce." +sidebarTitle: "Configure agent guardrails" +--- + +{/* Editor Refresh: 2026-07-30 */} + + +**Early access.** This feature is in early access, which means it's undergoing ongoing testing and development while we gather feedback, validate functionality, and improve outputs. Contact the C1 Support team if you'd like to try it out or share feedback. + + +Everything on this page lives under **AI > Guardrails**. The **General** tab holds guardrails, hooks, and tool gates; the **Settings** tab holds the untrusted content judge. + +The order below is deliberate: create the guardrail, bind it, then author rules in **Observe** mode and promote them to **Enforce** once you've seen what they catch on real traffic. + +## Step 1: Create a guardrail + +Start from **AI > Guardrails**, where every guardrail you create is listed alongside its bindings, rules, hooks, and tool gates. + + + +Go to **AI > Guardrails** and click **Create guardrail**. + + +Enter a **Name** (required, up to 128 characters) and optionally a **Description** (up to 1024 characters). + + +Set **Applies to** — **Agent** or **Gateway**. Choose deliberately: it determines which risk signals your rules can read, and which selector the guardrail appears in when you bind it. + + +Click **Create guardrail**. C1 creates it and opens its detail page. + + + +A new guardrail has no rules, so it matches nothing yet. Bind it first (Step 2), then add rules (Step 3) — that way the first rule you author is live in Observe mode immediately and starts producing signal. + + +To rename a guardrail or change its description or surface later, click **Edit** on the **Details** card on its detail page. + + +## Step 2: Bind the guardrail + + +A guardrail does nothing until it's bound. This is the step people skip — a carefully authored cascade that was never bound is evaluated on exactly zero calls. + + +Where you bind depends on the surface the guardrail applies to. + +### Agent surface + +For a tenant-wide default across your C1AI agents: + + + +Go to **AI > Guardrails**. + + +Under **Default C1AI Guardrail policy**, open the dropdown — it reads **No default guardrail selected** until you set one. + + +Pick your guardrail. + + + +To override the default for one agent, open that agent from **AI > Agents**, go to its **Guardrails** tab, and select a guardrail there. Choosing **Use the default agent guardrail** clears the override. + + +Only guardrails whose **Applies to** is **Agent** appear in these dropdowns. The match is exact — a guardrail left at the legacy **Any surface** value appears in neither selector. + + +### Gateway surface + +For external AI clients connecting through the C1 Gateway: + + + +Go to **AI > C1 Gateway > Settings**. + + +Under **Default C1 Gateway Guardrail policy**, select your guardrail. + + + +The guardrail is now bound and evaluated on every call to that surface — but with no rules yet, it matches nothing. + +## Step 3: Author a rule + +A guardrail's cascade is empty until you add rules to it. Each rule you create is appended to the bottom of the cascade, so order them deliberately (see [Order the cascade](#order-the-cascade) below). + + + +On the guardrail's detail page, click **New guardrail rule**. + + +Enter a **Name** and optional **Description**. Name it for the behavior it prevents — this string is what you'll be reading in audit logs later. + + +Enter a **Condition (CEL)**. Leave it empty to match every tool call. Available variables: + +| Variable | Values | +| :--- | :--- | +| `ctx.private_data` | `LOW`, `MEDIUM`, `HIGH` | +| `ctx.untrusted_content` | `LOW`, `MEDIUM`, `HIGH` — Agent surface only | +| `ctx.exfiltration` | `LOW`, `MEDIUM`, `HIGH` | +| `ctx.tool_name` | string | + +For example, to match the untrusted-content exfiltration pattern: + +``` +ctx.untrusted_content == HIGH + && ctx.exfiltration == HIGH +``` + + +Choose an **Action**: + +- **Block** — deny the call immediately. Gates and hooks are skipped, and the hook/gate pickers disappear from the form. +- **Evaluate hooks** — run the gates and hooks you select below, and let their outcome decide. + + +If you chose **Evaluate hooks**, select what this rule dispatches: + +| Picker | When it runs | +| :--- | :--- | +| **Pre-tool hooks** | Before the tool call | +| **Post-tool hooks** | After the tool call | +| **Pre-output hooks** | Before the assistant's response is delivered | +| **Tool gates** | Must grant before the tool call executes | + +Only hooks and gates with **Managed by guardrails** turned on appear in these lists, and each picker only offers hooks whose event matches it. If something you expect is missing, that's why — see Step 5. + +**Pre-output hooks** behave differently from the other three: a rule only takes effect on the outgoing response if it uses **Evaluate hooks** *and* selects at least one pre-output hook. A **Block** rule never applies to outgoing responses. See [Pre-output hooks](/product/admin/agent-guardrails-reference#pre-output-hooks). + + +Set **Mode** to **Observe** for now. (**Enforce** applies the outcome; **Disabled** skips the rule.) + + +Optionally enter a **Deny reason** (up to 256 characters). It's shown to the caller when this rule denies a call, so write it for whoever hits it — a bare "policy violation" leaves the agent's user with nothing to act on. + + +Click **Create**. + + + +The rule is appended to the bottom of the cascade in **Observe** mode, where it logs matches without blocking anything. + +### Order the cascade + +Rules evaluate **top to bottom, first match wins**. Click **Manage guardrail rules** to reorder them with the move controls, or to edit and delete individual rules. + + +Put narrow rules above broad ones. A catch-all rule with an empty condition matches every call, so anything below it is unreachable. + + +Each rule renders as an **IF** / **THEN** card showing its condition and its action and mode chips, so you can read the cascade top-down without opening each rule. + +## Step 4: Add a tool gate for approval + +A tool gate turns a matching call into an approval request instead of a failure. + + + +In the **Tool gates** section on **AI > Guardrails**, click **New tool gate**. + + +Enter a **Name** (1–100 characters) and optional **Description**. + + +Pick a **Grant policy** — this determines who approves and how. Required. + + +Enter a **Filter (CEL expression)**. Gate filters read a different variable set than guardrail rules — the call's identity and target rather than the risk axes. The field's helper text names `ctx.tool_name` and `ctx.tool_input`; see the [full list](/product/admin/agent-guardrails-reference#gate-filter-variables). + + +Set **Priority** (0–1000, lower matches first) and leave **Enabled** on. + + +Turn **Managed by guardrails** **on** so this gate only fires when a rule selects it, then click **Create**. + + +Go back to your rule and select the gate under **Tool gates**. + + + + +If a rule selects more than one tool gate, they don't all apply — **only the first gate whose filter matches runs**, and its grant policy decides the call. Order is enabled-first, then ascending **Priority**, then oldest first, across the gates your rule selected *plus* every gate with **Managed by guardrails** off. Give the gate whose approver should win the lower priority number. See [Only the first matching gate runs](/product/admin/agent-guardrails-reference#only-the-first-matching-gate-runs). + + + +Leaving **Managed by guardrails** **off** means the gate evaluates on every matching call independently of any guardrail. That's sometimes what you want — a blanket approval requirement — but it makes the gate invisible to your rule cascade. + + +## Step 5: Attach hooks + +Hooks do the inspection and rewriting. Create them in the **Hooks** section of **AI > Guardrails** — see [Tool call hooks](/product/admin/tool-call-hooks) for the full field reference and the built-in patterns. + +To make a hook selectable by a guardrail rule, turn on its **Managed by guardrails** toggle, either in the hook form or directly from the toggle column in the hooks table. + + +Hooks are **fail-closed**: if a hook errors, times out, or its filter expression fails to evaluate, the call is **denied**. A hook selected by an Enforce-mode rule can therefore block traffic because it's broken, not because it matched. Check the recorded status rather than assuming a denial means the guardrail worked. + + +## Step 6: Promote from Observe to Enforce + +Until you do this, a rule in **Observe** is a reporting mechanism, not a control. + + + +Leave the rule in **Observe** and run real traffic through it. + + +Review what it would have caught — see [Where to find Observe results](#where-to-find-observe-results) below. + + +Check both directions: rules that would have fired on calls you consider legitimate (too broad), and risky calls that matched nothing (too narrow). + + +Edit the rule and set **Mode** to **Enforce**. + + + +Because an Observe match falls through to the next rule, you can safely stage a whole cascade in Observe and promote it rule by rule. + +### Where to find Observe results + +Every rule that matches a call — enforcing or observing — writes an event to the [C1 system log](/product/admin/system-log). There is no per-guardrail activity view in C1, so the system log is where you read Observe results. + + +C1 does not surface these events in the admin UI, and nothing notifies you when an Observe rule would have fired. To read them you need a [system log exporter](/product/admin/system-log#sync-c1-system-logs-into-your-siem) configured to your SIEM or data source. Set that up **before** you start an Observe rollout, or the traffic you were staging against passes unrecorded from your point of view. + + +In the exported OCSF events, guardrail decisions carry: + +| Field | Value | +| :--- | :--- | +| `activity_name` | `guardrail_decision` | +| `api.operation` | `aigov.guardrail.decision` | +| `would_have_fired` | `true` for an Observe match; absent or `false` when a rule actually acted | +| `policy_rule_id` | Which rule matched — the rule's ID, not its name | +| `guardrail_id` | Which guardrail the rule belongs to | +| `mode`, `action` | The rule's mode and action at evaluation time | +| `tool_name`, `session_id` | What was called, and in which session | +| `a1_private`, `a2_untrusted`, `a3_exfiltration` | The three risk scores as evaluated | + +To count what a rule would have caught, filter on `activity_name = "guardrail_decision"` plus `would_have_fired = true` and your `policy_rule_id`. + + +Observe records are written at **informational** severity — only an actual block is raised above that. A severity filter tuned to warnings and above will exclude exactly the records you're staging against. + + +System log events are retained for 365 days by default. + +## Step 7: Configure the untrusted content judge + +The judge produces the `ctx.untrusted_content` signal. It's on by default and needs no setup — but you can turn it off. + + + +Go to **AI > Guardrails** and open the **Settings** tab. + + +Under **Untrusted content judge**, click **Edit** and toggle it. + + + + +Turning the judge off doesn't just stop the scoring — `ctx.untrusted_content` then **always evaluates as low risk**. Any rule that gates on it silently stops matching, so a cascade that looks correct is no longer protecting anything. Leave it on unless you have a specific reason, and if you do turn it off, disable the rules that depend on it rather than leaving them in place. + + +## Delete a guardrail + +Open the guardrail and click **Delete** in the top bar. The confirmation dialog names the guardrail and warns the deletion can't be undone. Any binding that pointed at it stops applying. + +## Next steps + +- Rule fields, CEL variables, and evaluation order: [Agent guardrails reference](/product/admin/agent-guardrails-reference). +- Hook configuration and built-in patterns: [Tool call hooks](/product/admin/tool-call-hooks). +- Controlling which tools exist to be gated at all: [Govern tools and toolsets](/product/admin/tools-and-toolsets). diff --git a/product/admin/agent-guardrails-reference.mdx b/product/admin/agent-guardrails-reference.mdx new file mode 100644 index 00000000..858bcb4a --- /dev/null +++ b/product/admin/agent-guardrails-reference.mdx @@ -0,0 +1,327 @@ +--- +title: "Agent guardrails reference" +og:title: "Agent guardrails reference - C1 docs" +description: "Technical reference for agent guardrails: object model, rule and tool gate fields, CEL variables, enforcement surfaces, evaluation order, constraints, and troubleshooting." +og:description: "Technical reference for agent guardrails: object model, rule and tool gate fields, CEL variables, enforcement surfaces, evaluation order, constraints, and troubleshooting." +sidebarTitle: "Agent guardrails reference" +--- + +{/* Editor Refresh: 2026-07-30 */} + + +**Early access.** This feature is in early access, which means it's undergoing ongoing testing and development while we gather feedback, validate functionality, and improve outputs. Contact the C1 Support team if you'd like to try it out or share feedback. + + +## Object model + +| Object | What it is | Scope | +| :--- | :--- | :--- | +| **Guardrail** | A named, ordered cascade of rules, plus the surface it may bind to | Tenant; many per tenant | +| **Guardrail rule** | One entry in a cascade. Stored inline on the guardrail, not separately reusable | Per guardrail | +| **Guardrail binding** | Attaches one guardrail to one enforcement target. The only object that puts a guardrail into effect | One per target | +| **Tool gate** | Standalone approval requirement: a filter plus a grant policy | Tenant; reusable across rules | +| **Hook** | Standalone interceptor that inspects, rewrites, or denies a payload | Tenant; reusable across rules | + +How they relate: a binding points a `(surface, target)` pair at one guardrail. The guardrail holds the cascade, and each rule in it references the gates and hooks it dispatches. + +``` +Guardrail + └─ rules[] (ordered) + ├─ tool gate ids + ├─ pre-tool hook ids + ├─ post-tool hook ids + └─ pre-output hook ids +``` + +Rules live inline on the guardrail because an ordered cascade is configuration, not a reusable unit. Tool gates and hooks are the reusable primitives — a rule references them by ID, and the same gate or hook can be selected by rules in different guardrails. + + +A binding is the only thing that activates a guardrail. Creating a guardrail, authoring rules, and wiring up gates and hooks changes nothing about live traffic until a binding points at it. + + +## Enforcement surfaces + +| Surface | Governs | Bind at | Available CEL variables | +| :--- | :--- | :--- | :--- | +| **Agent** | C1AI agents | **AI > Guardrails** → **Default C1AI Guardrail policy**, or an agent's **Guardrails** tab | `ctx.private_data`, `ctx.untrusted_content`, `ctx.exfiltration`, `ctx.tool_name` | +| **Gateway** | External AI clients through the C1 Gateway | **AI > C1 Gateway > Settings** → **Default C1 Gateway Guardrail policy** | `ctx.private_data`, `ctx.exfiltration`, `ctx.tool_name` | + +**Applies to** is a two-way choice — **Agent** or **Gateway**. There is no option to target both. + + +You may see a guardrail whose surface displays as **Any surface**. That's the unset value, kept for guardrails carried over from the older single-policy model; it isn't offered when you create or edit one. A guardrail in that state appears in **neither** binding selector, so it can't be bound from the admin UI at all — edit it and set **Applies to** to **Agent** or **Gateway** first. + + +Binding resolution on the agent surface tries the specific agent's binding first, then falls back to the tenant default agent binding. The gateway surface resolves a single tenant-wide binding — there is no per-client or per-connection gateway granularity. + + +`ctx.untrusted_content` is **not available on the Gateway surface**. The untrusted-content score comes from the judge, which runs inside a C1AI session; there is no equivalent for an external client. A gateway rule written against `ctx.untrusted_content` will not behave as intended. + + +If no binding resolves for a surface — none configured, or it points at a deleted guardrail — that surface falls back to an empty policy with no rules. It does **not** fall back to another surface's guardrail. + +## Guardrail fields + +| Field | Notes | +| :--- | :--- | +| **Name** | Required. Up to 128 characters. | +| **Description** | Optional. Up to 1024 characters. | +| **Applies to** | **Agent** or **Gateway**. Determines which variables rules can read, and which binding selector the guardrail appears in. | +| **Managed by** | **Tenant** for guardrails you author — freely editable. **System** marks a C1-curated guardrail, which is read-only. | + +## Guardrail rule fields + +| Field | Notes | +| :--- | :--- | +| **Name** | Required. Up to 128 characters. | +| **Description** | Optional. Up to 1024 characters. | +| **Condition (CEL)** | Optional. Empty matches **every** tool call. Up to 32,768 bytes. | +| **Action** | **Block** or **Evaluate hooks**. | +| **Pre-tool hooks** | Hooks evaluated before the tool call. Hidden when Action is Block. | +| **Post-tool hooks** | Hooks evaluated after the tool call. Hidden when Action is Block. | +| **Pre-output hooks** | Hooks evaluated before the assistant's response is delivered. Hidden when Action is Block. | +| **Tool gates** | Gates that must grant before the call executes. Hidden when Action is Block. | +| **Mode** | **Enforce**, **Observe**, or **Disabled**. | +| **Deny reason** | Optional. Up to 256 characters. Shown to the caller on denial. | + +Only hooks and tool gates with **Managed by guardrails** enabled are selectable in the hook and gate pickers, and each picker only offers hooks whose event matches it. + + +Audit records identify a matched rule by its **ID**, not its name. Renaming a rule doesn't change what earlier records point at — but it also means a rule's name isn't what you search on when tracing a decision. + + +### Actions + +| Action | Behavior | +| :--- | :--- | +| **Block** | Deny the call immediately. Tool gates and hooks are skipped entirely. | +| **Evaluate hooks** | Run the selected tool gates and hooks; their outcome decides the call. | + +### Modes + +| Mode | On a match | +| :--- | :--- | +| **Enforce** | The outcome applies and cascade evaluation stops. | +| **Observe** | Logs that the rule *would have fired*, then **falls through to the next rule**. Nothing is blocked. The first Observe match is recorded as an informational audit event marked *would have fired* if no later Enforce rule fires. | +| **Disabled** | Skipped entirely. Unset behaves as disabled. | + +## CEL variables + +| Variable | Type | Surface | Notes | +| :--- | :--- | :--- | :--- | +| `ctx.private_data` | level | Both | Sensitivity of data in play | +| `ctx.untrusted_content` | level | Agent only | Prompt-injection risk, from the judge | +| `ctx.exfiltration` | level | Both | Capability of this call to move data out | +| `ctx.tool_name` | string | Both | The tool being called | + +Levels are `LOW`, `MEDIUM`, and `HIGH`, ordered — they compare as 0, 1, and 2 respectively, so `>=` works as you'd expect and the numeric form is equivalent. + +Examples: + +``` +ctx.untrusted_content == HIGH + && ctx.exfiltration == HIGH + +ctx.private_data >= MEDIUM + && ctx.exfiltration >= MEDIUM + +ctx.private_data == HIGH + && ctx.tool_name.startsWith("salesforce_") +``` + + +A rule whose CEL expression fails to evaluate is **skipped**, and the cascade continues to the next rule. It does not abort evaluation and does not deny the call — so a catch-all rule lower in the cascade still fires. This is the opposite of hook filter behavior, where a failed filter denies the call. + + +## Evaluation order on a tool call + +1. **Resolve the binding** for the surface, and walk the guardrail's rules top to bottom, first match wins. +2. If the matched rule's action is **Block** → deny. **Gates and hooks never run.** +3. Otherwise run **tool gates**: the gates the rule selected, plus every gate with **Managed by guardrails** off. Only the [first matching gate](#only-the-first-matching-gate-runs) runs. A gate that requests approval or denies short-circuits here — **hooks and the tool never run.** +4. Run **pre-tool hooks**: those the rule selected, plus every pre-tool hook with **Managed by guardrails** off. A denial blocks the call. +5. **The tool executes.** +6. Run **post-tool hooks**, same union rule. A denial blocks the result from reaching the caller. + +If no rule matches, the call proceeds with no guardrail-curated gates or hooks — unmanaged gates and hooks still apply. + +A guardrail with no rules, or one whose every rule is **Disabled**, matches nothing. At call time it is indistinguishable from an unbound surface: bound-but-empty and not-bound-at-all behave identically. + + +**A call that matches no rule writes no `guardrail_decision` event.** Only a matched rule produces a durable record. So in the system log you cannot tell *"traffic reached this surface and matched nothing"* apart from *"no traffic reached this surface at all"* — both look like silence. Keep that in mind when a rule you expected to fire shows no records. + + +Pre-output hooks are not part of this walk. See [Pre-output hooks](#pre-output-hooks). + +## Pre-output hooks + +Pre-output hooks run when a chunk of the assistant's generated response is about to be delivered. No tool call is involved, and the stage differs from the tool-call stages in four ways that each have a distinct failure mode. + +### It is a separate cascade walk with an eligibility test + +The cascade is walked again for the outgoing response, but a rule is only **eligible** to win if both of these hold: + +- its action is **Evaluate hooks**, and +- its **Pre-output hooks** list is non-empty. + +An ineligible rule is skipped and the walk **continues to the next rule**, even if its condition matched. Two consequences: + +- A **Block**-action rule can never win on this path. It curates no hook lists, so it is structurally excluded — a Block rule does not block outgoing responses. +- An **Evaluate hooks** rule that curates only tool gates or pre/post-tool hooks is also skipped here, so a later rule can win the response even though the earlier rule won the tool call. + +Mode works exactly as it does on the tool-call path: **Disabled** and unset are skipped, **Observe** logs a would-have-fired and falls through, **Enforce** can win. If no rule is eligible, the response is delivered unfiltered. + +### Only two built-in patterns run here + +**Block output** and **Link filter**. Every other built-in pattern is restricted to the tool-call events, and C1 rejects an incompatible event when you save the hook. + + +**Custom function hooks cannot run on this stage.** Saving a function hook with the Pre-output event is rejected. The stage is also fail-closed, so a function hook that did reach it would withhold every matching response rather than being ignored. + + +### The filter variables are different, and one name is a trap + +| Variable | Values | +| :--- | :--- | +| `ctx.untrusted_class` | The strings `"LOW"`, `"MEDIUM"`, `"HIGH"` | +| `ctx.surface` | `"slack"` or `"web"` | + +`ctx.tool_name` is **unset** here. + + +`ctx.untrusted_class` (pre-output hook filter) and `ctx.untrusted_content` (guardrail rule) are the same underlying risk score exposed under different names *and* different types — an uppercase string versus an ordered level. A hook filter needs `ctx.untrusted_class == "HIGH"`; a rule needs `ctx.untrusted_content == HIGH`. Writing the rule form in a hook filter fails to evaluate, and a failed hook filter **denies** — so the mistake withholds responses rather than erroring visibly. + + +### A Block output hook with no surfaces is inert + +An empty surface list means no surface at all, not all of them. Select **Slack**, **Web**, or both, or the hook never fires. See [Tool call hooks](/product/admin/tool-call-hooks#built-in-patterns). + +## Tool gate fields + +| Field | Notes | +| :--- | :--- | +| **Name** | Required. 1–100 characters. | +| **Description** | Optional. Up to 2048 characters. | +| **Grant policy** | Required. Determines who approves and how. | +| **Filter (CEL expression)** | Optional. See the variables below. | +| **Priority** | 0–1000. Lower matches first. | +| **Enabled** | Default on. | +| **Managed by guardrails** | Default **off** — the gate always evaluates. On — only when a rule selects it. | + +### Only the first matching gate runs + +A rule can select several tool gates, but they are not all applied. The gates are evaluated in order and **the first one whose filter matches wins** — its grant policy is the one that decides the call, and no further gate is consulted. + +The order is: + +1. Enabled gates first +2. Then ascending **Priority** (lower first) +3. Then oldest first + +The set evaluated is the gates the matched rule selected **plus** every gate with **Managed by guardrails** off, so an unmanaged gate with a lower priority can win ahead of one your rule explicitly selected. + + +This makes **Priority** load-bearing whenever more than one gate could match the same call. Two gates with different grant policies and overlapping filters will not both apply — the call is approved under whichever gate sorts first, and the other is silently skipped. If you need a stricter approver to take precedence, give that gate the lower priority number. + + +### Gate filter variables + +A tool gate filter is evaluated against a different variable set than a guardrail rule — it sees the call's identity and target rather than the risk axes: + +| Variable | Notes | +| :--- | :--- | +| `ctx.tool_name` | The tool being called | +| `ctx.tool_kind` | What kind of tool it is — see below | +| `ctx.tool_input` | The call's input arguments | +| `ctx.app_id` | Target app | +| `ctx.connector_id` | Target connector | +| `ctx.app_entitlement_id` | Target entitlement | +| `ctx.caller_kind` | What kind of caller made the call — see below | +| `ctx.principal_user_id` | The acting user | +| `ctx.agent_id` | The acting agent | + +`ctx.tool_kind` and `ctx.caller_kind` are **separate vocabularies** — the values are not interchangeable: + +| Variable | Values | +| :--- | :--- | +| `ctx.tool_kind` | `builtin`, `connector`, `c1_derived`, `unspecified` | +| `ctx.caller_kind` | `c1aw`, `claw_builtin`, `code_mode`, `connector_manager`, `mcp_gateway`, `unspecified` | + +Both fall back to `unspecified` for a value C1 doesn't recognize, so a filter written as an equality test against a specific kind won't match an unrecognized one. + + +The in-product helper text on the filter field mentions only `ctx.tool_name` and `ctx.tool_input` — the two you'll use most. The rest are available. + + +## The untrusted content judge + +| Property | Detail | +| :--- | :--- | +| What it does | Scores agent input and tool output for prompt-injection risk on every turn | +| Produces | `ctx.untrusted_content`, as `LOW`, `MEDIUM`, or `HIGH` | +| Default | On | +| Configuration | A single on/off toggle at **AI > Guardrails > Settings** | +| When off | The dimension **always scores as low risk** and the judge never runs | +| When it can't be reached at the start of a run | C1 seeds a cautious **MEDIUM** untrusted-content floor rather than treating the turn as low risk | +| When a mid-run rescore fails | The existing floor is kept; no new score is synthesized | + +There is no model selection, threshold, or per-agent judge configuration. + + +Disabling the judge does not disable rules that read `ctx.untrusted_content` — it makes them silently stop matching, because the value is pinned to low risk. Disable those rules explicitly rather than relying on the judge being off. + + +## Constraints + +| Constraint | Detail | +| :--- | :--- | +| Rule evaluation | First match wins, top to bottom | +| Rule with empty condition | Matches every call | +| Rule with a failing CEL expression | Skipped; cascade continues | +| Hook failure mode | Fail-closed — error, timeout, or filter failure denies the call | +| Custom function hook timeout | 60 seconds | +| Gate filter variables | Nine — see [Gate filter variables](#gate-filter-variables). Distinct from the rule variable set. | +| Gate ordering | Enabled first, then priority ascending, then oldest first | +| Priority range | 0–1000 for gates and hooks; lower first | +| Tenant defaults | No guardrail, binding, or rule is created for a new tenant | + +## Troubleshooting + +### My guardrail isn't doing anything + +Check, in order: + +1. **Is it bound?** An unbound guardrail is evaluated on zero calls. Check **Default C1AI Guardrail policy** (agent) or **Default C1 Gateway Guardrail policy** (gateway), and any per-agent override that might be shadowing the default. +2. **Does the surface match?** An Agent guardrail won't appear in the gateway selector, and vice versa. +3. **Are the rules in Observe?** Observe logs and falls through by design. +4. **Are the rules Disabled?** An unset mode behaves as disabled. +5. **Is an earlier rule matching first?** A broad rule above a narrow one makes the narrow one unreachable. + +### A rule that reads `ctx.untrusted_content` never matches + +Either the guardrail is bound to the **Gateway** surface, where that variable isn't available, or the **untrusted content judge** is turned off, which pins the value to low risk. + +### A hook or tool gate I created isn't selectable in a rule + +Its **Managed by guardrails** toggle is off. Turn it on from the hook or gate form, or from the toggle column in the table. + +### A hook fires even though no rule selects it + +That's the documented behavior for **Managed by guardrails** off: the hook always evaluates. Turn the toggle on to bring it under rule control. + +### Calls are being denied and I can't tell which layer did it + +Distinguish the three denial shapes: + +- A **Block**-action rule denies with your configured **Deny reason**. +- A **tool gate** doesn't deny — it pauses the call behind an approval request. A denial only follows if the request is denied. +- A **hook** denies with its own reason, or fail-closed because it errored, timed out, or its filter failed to evaluate. Check the recorded hook status: `ERROR`, `TIMEOUT`, and `FILTER_ERROR` mean the hook is broken, not that it matched. + +### An Observe rule shows nothing in the logs + +Observe records a match, and only a match. A call that matches no rule writes no event at all — so "traffic arrived and matched nothing" and "no traffic arrived" produce identical silence, and you can't tell them apart from the log. Check both: widen the condition, and independently confirm traffic is reaching the bound surface. + +Also confirm the guardrail is bound, and that your severity filter isn't excluding informational records. + +### A rule matches calls I expected it to ignore + +An empty **Condition (CEL)** matches every tool call. Check that the condition saved, and that a broader rule above this one isn't the one actually matching — evaluation stops at the first match in **Enforce** mode. diff --git a/product/admin/agent-guardrails.mdx b/product/admin/agent-guardrails.mdx new file mode 100644 index 00000000..11fab921 --- /dev/null +++ b/product/admin/agent-guardrails.mdx @@ -0,0 +1,150 @@ +--- +title: "Agent guardrails" +og:title: "Agent guardrails - C1 docs" +description: "Guardrails are named, reusable rule cascades that decide what an AI agent may do on each tool call, based on how much private data, untrusted content, and exfiltration capability is in play." +og:description: "Guardrails are named, reusable rule cascades that decide what an AI agent may do on each tool call, based on how much private data, untrusted content, and exfiltration capability is in play." +sidebarTitle: "Agent guardrails" +--- + +{/* Editor Refresh: 2026-07-30 */} + + +**Early access.** This feature is in early access, which means it's undergoing ongoing testing and development while we gather feedback, validate functionality, and improve outputs. Contact the C1 Support team if you'd like to try it out or share feedback. + + +A **guardrail** is a named, reusable cascade of rules that C1 evaluates on every governed tool call. Each rule asks a question about the *risk shape* of the call — how sensitive the data in play is, whether the agent has been exposed to untrusted content, whether the call could exfiltrate — and decides what happens: allow it, run a set of hooks over it, require human approval, or block it outright. + +Guardrails exist because the risk of an agent action often isn't a property of the tool. `send_email` is harmless until the agent has just read a support ticket written by a stranger and is holding customer records. Access profiles can't express that; they only know whether the tool is reachable. A guardrail can, because it evaluates the combination at call time. + +## What you can do with guardrails + +- **Gate on risk combinations, not just tool identity**: write rules over private-data sensitivity, untrusted-content exposure, and exfiltration capability +- **Require approval mid-call**: attach tool gates so a matching call pauses for a human decision instead of failing +- **Redact and inspect payloads**: attach hooks that rewrite inputs, outputs, or the assistant's outgoing response +- **Roll out safely**: run any rule in **Observe** mode to log what it *would* have done before it denies anything +- **Bind per surface**: apply one guardrail to your C1AI agents and a different one to external clients on the gateway + +## Key concepts + +| Concept | What it is | +| :--- | :--- | +| **Guardrail** | A named, ordered cascade of rules, plus which surface it may bind to. The reusable unit you create and manage. | +| **Guardrail rule** | One entry in the cascade: an optional CEL condition, an action, a mode, and — for hook-evaluating rules — the hooks and tool gates it selects. | +| **Guardrail binding** | What actually puts a guardrail into effect: it attaches one guardrail to one enforcement target. Nothing is enforced until a binding exists. | +| **Tool gate** | Requires approval before a matching tool call executes. The call pauses behind a request under a grant policy rather than being denied. | +| **Hook** | Runs synchronously around a call to inspect, rewrite, or deny its input, output, or the assistant's outgoing response. See [Tool call hooks](/product/admin/tool-call-hooks). | +| **Untrusted content judge** | The classifier that scores agent input and tool output for prompt-injection risk on every turn, producing the untrusted-content signal rules read. | + +## The three risk signals + +Every guardrail rule is a CEL expression over the same small set of variables. Three of them are the risk axes C1 scores per call, each `LOW`, `MEDIUM`, or `HIGH`: + +| Variable | What it measures | +| :--- | :--- | +| `ctx.private_data` | How sensitive the data the agent is holding or requesting is | +| `ctx.untrusted_content` | How much untrusted, potentially injected content the agent has been exposed to. Scored by the untrusted content judge. **Agent surface only** | +| `ctx.exfiltration` | How capable this call is of moving data outside the trust boundary | + +Plus `ctx.tool_name`, for name-based matching. + +The reason this framing exists is that each axis alone is usually fine and the *combination* is what's dangerous — an agent holding private data, exposed to untrusted content, and able to exfiltrate is the "lethal trifecta." Writing rules on the combination is the point of guardrails. + + +`ctx.untrusted_content` is only populated on the **Agent** surface, because the judge runs inside a C1AI session. A rule written against it on a **Gateway** guardrail won't behave as you expect. See [Enforcement surfaces](/product/admin/agent-guardrails-reference#enforcement-surfaces). + + +## How a rule decides + +Rules are evaluated **top to bottom, first match wins**. A rule with no condition matches every call, which makes it useful as a catch-all at the bottom of a cascade. + +Each matching rule has an **action**: + +- **Block** — deny the call immediately. No gates or hooks run. +- **Evaluate hooks** — run the tool gates and hooks this rule selects, and let their outcome decide the call. + +...and a **mode**, which is what makes guardrails safe to roll out: + +| Mode | What happens on a match | +| :--- | :--- | +| **Enforce** | The rule's outcome applies and the cascade stops. | +| **Observe** | The rule logs that it *would* have fired and evaluation **falls through to the next rule**. Nothing is blocked. | +| **Disabled** | The rule is skipped entirely. | + + +Author every new rule in **Observe** mode. You get a durable record of what it would have caught against real traffic, with no risk of blocking a legitimate call, and you promote it to **Enforce** once the logs match your expectation. This is a first-class mode — you don't need to fake it with a permissive rule. + + +## Where guardrails apply + +A guardrail declares which surface it **applies to**, and a binding attaches it to a target on that surface. + +| Surface | Governs | Where you bind it | Signals available | +| :--- | :--- | :--- | :--- | +| **Agent** | C1AI agents acting in your tenant | **AI > Guardrails** → **Default C1AI Guardrail policy**, or per-agent on an agent's **Guardrails** tab | All three risk signals | +| **Gateway** | External AI clients connecting through the C1 Gateway | **AI > C1 Gateway > Settings** → **Default C1 Gateway Guardrail policy** | `ctx.private_data`, `ctx.exfiltration`, `ctx.tool_name` — no `ctx.untrusted_content` | + +A guardrail targets one surface or the other — you pick **Agent** or **Gateway** when you create it, and it only appears in that surface's binding selector. To cover both, create one guardrail per surface. + + +Creating a guardrail does nothing on its own. Until you **bind** it, no call is evaluated against it. A tenant starts with no guardrails and no bindings — there is no default cascade to inherit. + + +## Guardrails, tool gates, and hooks + +These three are easy to conflate. The distinction is what each one does to a call: + +| | Decides | Effect on a matching call | +| :--- | :--- | :--- | +| **Guardrail rule** | Which gates and hooks apply to this call, or whether to block it outright | Dispatches, or blocks | +| **Tool gate** | Whether a human must approve first | **Pauses** the call behind an approval request under a grant policy | +| **Hook** | Whether the payload is acceptable, and what it should look like | Allows, **rewrites**, or denies — synchronously, in-line | + +A guardrail rule invokes hooks and gates; never the reverse. On a single tool call the order is fixed: + +**guardrail rule match → tool gate → pre-tool hooks → tool executes → post-tool hooks** + +A `Block` rule short-circuits before any gate or hook runs. A gate that requests approval short-circuits before any hook runs. + +**Pre-output hooks** are a separate stage on a separate path. They run when the assistant's own response is about to be delivered, with no tool call involved — the last point at which you can stop something on its way out. They behave differently enough from the tool-call stages that they have [their own section](/product/admin/agent-guardrails-reference#pre-output-hooks) in the reference. + +Both hooks and tool gates carry a **Managed by guardrails** toggle that decides whether they participate in this cascade at all: + +- **Off** (default) — the hook or gate always evaluates, on every matching call, regardless of guardrails. +- **On** — it only evaluates when a guardrail rule explicitly selects it. + + +This is the single most common source of surprise. A hook you created before adopting guardrails has **Managed by guardrails** off, so it keeps firing on every call even when no guardrail selects it. Turning it on hands control of that hook to your rule cascade. + + +## Key use cases for guardrails + +### Freeze exfiltration after the agent reads untrusted content + +The indirect prompt-injection pattern behind published attacks like EchoLeak and CamoLeak: an agent reads attacker-controlled text, then is steered into sending data out. A rule matching elevated `ctx.untrusted_content` **and** elevated `ctx.exfiltration` blocks that combination while leaving each capability independently available. + +### Require approval for the lethal trifecta + +Rather than blocking, route the risky combination to a human: a rule that matches all three axes being elevated and selects a tool gate, so the call pauses for approval under a grant policy you choose. + +### Keep your crown jewels inside the boundary + +A rule matching the highest `ctx.private_data` level against a high-exfiltration call, blocking regardless of which tool is involved. Because it's written on the data-sensitivity axis rather than a tool allowlist, it covers tools you haven't onboarded yet. + +### Strip secrets and PII from tool output + +Attach the **Secrets masking** and **PII field redaction** hooks to a rule so matching values are removed from output before the agent's context ever sees them. + +### Approve destructive actions without blocking the agent + +A tool gate filtered to destructive operations turns `delete`/`drop`/`terminate`-shaped calls into approval requests instead of failures, so the agent stays useful and a human owns the irreversible step. + +### Catch a leak on the way out, after every tool call has passed + +The tool-call stages check each call in isolation. But an agent can assemble something from several individually-innocuous calls and put it in its reply — and by then no tool is involved, so no pre-tool or post-tool hook will see it. A **pre-output** rule inspects the outgoing response itself: withhold it entirely on a matching risk class with **Block output**, or strip non-allowlisted URLs and markdown images with **Link filter** to close the markdown-image exfiltration channel. This is the natural companion to the untrusted-content rules above — it's the last checkpoint before the response reaches a person. + +## Where to go from here + +- Ready to set one up? See [Configure agent guardrails](/product/admin/agent-guardrails-configure). +- Need the rule fields, CEL variables, and evaluation order? See the [Agent guardrails reference](/product/admin/agent-guardrails-reference). +- Configuring the hooks a rule selects? See [Tool call hooks](/product/admin/tool-call-hooks). +- Governing which tools are reachable in the first place? See [Govern tools and toolsets](/product/admin/tools-and-toolsets). diff --git a/product/admin/ai-clients.mdx b/product/admin/ai-clients.mdx index efdf4c8e..1251b878 100644 --- a/product/admin/ai-clients.mdx +++ b/product/admin/ai-clients.mdx @@ -5,7 +5,7 @@ og:title: Manage AI clients - C1 docs og:description: Understand how AI clients register with C1, the lifecycle states they move through, and the controls available to admins. --- -{/* Editor Refresh: 2026-05-08 */} +{/* Editor Refresh: 2026-07-30 */} **Activation required.** AI access management must be enabled for your tenant before you can use it. To get started, [contact the C1 support team](mailto:support@c1.ai) for a walkthrough. @@ -47,14 +47,14 @@ You can filter by any column. Click a client to open its detail panel. ## Client lifecycle states -C1 transitions clients through four states based on inactivity. Thresholds are set at the tenant level but can be overridden per client. +C1 transitions clients through four states. Thresholds are set at the tenant level (**Hide inactive clients after**: 1 day, **Close credentials after**: 1 month, **Delete closed clients after**: 3 months, each settable to zero to disable) but can be overridden per client. | State | What it means | What the user sees | | :--- | :--- | :--- | | **Active** | Recently used; tokens valid | Normal operation | -| **Hidden** | Inactive past the hidden threshold | Client is hidden from the user's connected-clients list, but tokens still work | -| **Closed** | Inactive past the closed threshold | Tokens revoked; user must re-authenticate to use the client again | -| **Deleted** | Inactive past the deleted threshold | Client registration is removed; user must re-register from scratch | +| **Hidden** | Inactive past the **Hide inactive clients after** threshold | Client is hidden from the user's connected-clients list, but tokens still work | +| **Closed** | Inactive past the **Close credentials after** threshold | Tokens revoked; user must re-authenticate to use the client again | +| **Deleted** | Past the **Delete closed clients after** threshold, measured from when the client entered **Closed** — not from its last activity | Client registration is removed; user must re-register from scratch | A client moves back to **Active** as soon as it makes a successful tool call (assuming its tokens are still valid). diff --git a/product/admin/aiam-overview.mdx b/product/admin/aiam-overview.mdx index 7999bab0..476dde43 100644 --- a/product/admin/aiam-overview.mdx +++ b/product/admin/aiam-overview.mdx @@ -37,7 +37,7 @@ C1's AI Connections feature has two sides. This page covers AI access management | **MCP server** | A downstream service that exposes one or more tools via the Model Context Protocol. C1 hosts a curated catalog. | | **C1 MCP** | The proxy your AI clients connect to. It accepts connections from AI clients, routes requests to the appropriate downstream servers, and enforces authorization on every tool call. AI clients connect to one URL — C1 MCP — not to each downstream server directly. | | **Tool** | A single capability exposed by an MCP server (for example, `github_create_issue` or `salesforce_query`). C1 discovers tools automatically when an MCP server is registered. | -| **Toolset** | A named bundle of approved tools. Two kinds: C1-maintained (for example, "All approved tools" and "All read tools") and custom (admin-curated). | +| **Toolset** | A named bundle of approved tools. Two kinds: C1-maintained per connector ("All approved tools" and "Read tools", suffixed with the server name) and custom (admin-curated). | | **Access profile** | The same mechanism C1 already uses for traditional app access. A toolset is bound to an access profile, which carries the approval policy, expiry, and approvers. | | **AI client** | A specific registered AI client instance (for example, "Jess's Claude Desktop" or "the team's shared Cursor agent"). Registered via Dynamic Client Registration (DCR) or Client ID Metadata Document (CIMD). | | **AI connection** | The authenticated link between a user's AI client and C1. When a user connects an AI client to C1, C1 creates an AI connection record that ties the client instance to the user's identity. Admins can view, manage, and revoke AI connections across the tenant. | diff --git a/product/admin/enable-ai-access-management.mdx b/product/admin/enable-ai-access-management.mdx index 2281bb9a..514c9a3a 100644 --- a/product/admin/enable-ai-access-management.mdx +++ b/product/admin/enable-ai-access-management.mdx @@ -5,7 +5,7 @@ og:title: Enable AI access management - C1 docs og:description: Turn on AIAM for your tenant and configure tenant-wide defaults for MCP servers, tools, and AI clients. --- -{/* Editor Refresh: 2026-05-08 */} +{/* Editor Refresh: 2026-07-30 */} **Activation required.** AI access management must be enabled for your tenant before you can use it. To get started, [contact the C1 support team](mailto:support@c1.ai) for a walkthrough. @@ -50,9 +50,11 @@ Controls which categories of AI client are allowed to register against your tena | Type | What it is | Default | | :--- | :--- | :--- | | **Personal** | Tied to a single human user (for example, Claude Desktop on their laptop) | Allowed | -| **Shared** | Used by multiple humans behind a single registration (for example, a team workspace agent) | Disallowed | -| **Service** | Machine-to-machine, no human in the loop (CI/CD, batch agents) | Disallowed | -| **Ephemeral** | Short-lived, single-session (one-off scripts, sandbox runs) | Disallowed | +| **Shared** | Used by multiple humans behind a single registration (for example, a team workspace agent) | Allowed | +| **Service** | Machine-to-machine, no human in the loop (CI/CD, batch agents) | Allowed | +| **Ephemeral** | Short-lived, single-session (one-off scripts, sandbox runs) | Allowed | + +The underlying setting is a list of allowed client types. By default that list is **empty**, which means every client type is allowed to register. **Recommendation:** restrict this list to the client types your organization actually uses (typically Personal and Shared) — this is a hardening step you opt into, not the out-of-the-box behavior. To change which types are allowed: @@ -77,7 +79,7 @@ Any in-flight client of a now-disallowed type continues to function until its ex When C1 discovers a new tool on a registered MCP server, it assigns the tool this initial state. Until an admin reviews and approves the tool, it cannot be added to a toolset and end users cannot request it. - **State**: Pending Review / Unset (recommended — keeps every newly-discovered tool out of end-user reach until you've reviewed it) -- **Classification**: Unclassified (recommended) +- **Classification**: Sensitive (default) To change the defaults: @@ -104,24 +106,26 @@ When on, every newly-discovered tool starts in **Pending Review** and must be ap Turning this off does not bypass access profile approval — end users still go through the access profile's approval policy when they request a toolset. -### Client lifecycle inactivity policy +### Client lifecycle policy -C1 tracks how long it's been since each registered AI client made a tool call. After configurable thresholds, the client transitions through three states: +C1 tracks how long it's been since each registered AI client made a tool call, and how long it's been since a client's credentials were closed. Three thresholds control what happens to an inactive client: -| State | What changes for the user | Default threshold | +| Field label | Helper text | Default | | :--- | :--- | :--- | -| **Hidden** | Client is hidden from the end user's connected-clients list, but tokens still work if presented | 1 day | -| **Closed** | Tokens are revoked; client must re-authenticate to be used again | 7 days | -| **Deleted** | Client registration is removed; user must register again from scratch | 90 days | +| **Hide inactive clients after** | How long after last activity before the client is hidden from the default list. Set to zero to disable. | 1 day | +| **Close credentials after** | How long after last activity before OAuth credentials are closed. Set to zero to disable. | 1 month | +| **Delete closed clients after** | How long after close before the client registration is deleted. Set to zero to disable. | 3 months | + +**Delete closed clients after** is measured from when the client's credentials were closed, **not** from the client's last activity. To change the thresholds: -In **AI > C1 Gateway > Settings**, find **Client lifecycle**. +In **AI > C1 Gateway > Settings**, find **Client governance**. -Set the inactivity threshold for each state. +Set each threshold. Click **Save**. @@ -148,3 +152,21 @@ Toggle **Disable MCP traffic**. +### Per-server kill switch + +Each registered MCP server also has its own kill switch, independent of the tenant-wide one. Flipping it blocks all MCP requests for that connector only — other servers keep working. + + + +Open the MCP server's configuration tab. + + +Toggle the kill switch. + + +Click **Save**. + + + +Combined with the per-tool kill switch ([Govern tools and toolsets](/product/admin/tools-and-toolsets)) and the per-client kill switch ([Manage AI clients](/product/admin/ai-clients)), C1 offers kill switches at four scopes: tenant, server, tool, and client. + diff --git a/product/admin/tool-call-hooks.mdx b/product/admin/tool-call-hooks.mdx index 92c70550..91a6a53b 100644 --- a/product/admin/tool-call-hooks.mdx +++ b/product/admin/tool-call-hooks.mdx @@ -5,7 +5,7 @@ og:title: Tool call hooks - C1 docs og:description: Intercept MCP tool calls with built-in patterns or custom functions to redact, modify, or block calls at runtime. --- -{/* Editor Refresh: 2026-05-08 */} +{/* Editor Refresh: 2026-07-30 */} **Activation required.** AI access management must be enabled for your tenant before you can use it. To get started, [contact the C1 support team](mailto:support@c1.ai) for a walkthrough. @@ -15,12 +15,13 @@ Tool call hooks are interception points that run on every governed MCP tool call ## How hooks work -Each hook fires on one of two events: +Each hook fires on one of three events: | Event | When it runs | What it can do | | :--- | :--- | :--- | | **Pre-tool use** | Before C1 forwards the call to the MCP server | Inspect or rewrite the input, or deny the call | | **Post-tool use** | After the MCP server returns | Inspect or rewrite the output, or deny the response from reaching the client | +| **Pre-output** | Before a chunk of the assistant's generated response leaves the process (Slack or web chat). No tool call is involved. | Inspect or rewrite the outgoing response chunk, or withhold it | Hooks run in **priority order** (lower priority numbers run first). Each hook is independently enabled or disabled and can be scoped to specific tools with a [CEL](/product/admin/expressions) filter expression. Multiple hooks can stack on the same call — input or output modifications from earlier hooks are passed to later ones, and any hook can short-circuit the chain by denying. @@ -30,9 +31,11 @@ Every hook execution is recorded in the [audit log](/product/admin/audit-ai-tool ## Configure a hook +Create hooks from the **Hooks** section under **AI > Guardrails**. + -Go to **AI > C1 Gateway > Settings** and find the **Hooks** section. +Go to **AI > Guardrails**, open the **General** tab, and find the **Hooks** section. Click **New hook**. @@ -45,9 +48,10 @@ Fill out the form: | **Name** | Required. 1–100 characters. | | **Description** | Optional. Up to 2048 characters. | | **Hook type** | **Built-in pattern** for one of the patterns below, or **Custom function** to invoke a [function](/product/admin/functions). | -| **Event** | **Pre-tool use** or **Post-tool use**. Some built-in patterns only support one event. | -| **Filter** | Optional CEL expression. Available variable: `ctx.tool_name`. Empty matches all tools. Example: `ctx.tool_name.startsWith("github_")`. | +| **Event** | **Pre-tool use**, **Post-tool use**, or **Pre-output**. Some built-in patterns only support one event. | +| **Filter** | Optional CEL expression. Empty matches all calls for the event. Available variables depend on the event — see [Filter variables by event](#filter-variables-by-event). | | **Priority** | 0–1000. Lower runs first. | +| **Managed by guardrails** | Off by default. See [Managed by guardrails](#managed-by-guardrails) below. | | **Enabled** | Toggle on to activate the hook immediately on save. | @@ -58,17 +62,77 @@ Click **Save**. +### Filter variables by event + +A filter expression only sees the variables available for its event. Referencing one that isn't set for that event makes the expression fail to evaluate, which **denies the call** — see the fail-closed note above. + +| Event | Variables | +| :--- | :--- | +| **Pre-tool use**, **Post-tool use** | `ctx.tool_name` — for example, `ctx.tool_name.startsWith("github_")` | +| **Pre-output** | `ctx.untrusted_class`, one of the strings `"LOW"`, `"MEDIUM"`, `"HIGH"`; and `ctx.surface`, `"slack"` or `"web"`. `ctx.tool_name` is **unset** — no tool call is involved. | + + +`ctx.untrusted_class` (a hook filter, comparing **strings**) and `ctx.untrusted_content` (a [guardrail rule](/product/admin/agent-guardrails-reference#cel-variables) variable, comparing **ordered levels**) carry the same underlying risk score under two different names and two different types. In a pre-output hook filter write `ctx.untrusted_class == "HIGH"`; in a guardrail rule write `ctx.untrusted_content == HIGH`. Using the rule form in a hook filter fails to evaluate, and the call is denied. + + +### Event and pattern must be compatible + +Each built-in pattern is only valid for certain events, and C1 rejects a mismatch when you save the hook. The **Event** column in [Built-in patterns](#built-in-patterns) is the authoritative list. + + +**Custom function hooks cannot use the Pre-output event.** Saving one is rejected. Only the **Block output** and **Link filter** built-in patterns run on that stage. + + +## Managed by guardrails + +Each hook has a **Managed by guardrails** toggle: + +- **Off (default)** — the hook always evaluates on every matching call. +- **On** — the hook only evaluates when a [guardrail](/product/admin/agent-guardrails) rule explicitly selects it. + +See [Agent guardrails](/product/admin/agent-guardrails) for how guardrail rules select hooks. + ## Built-in patterns -C1 ships five pre-built hook patterns. Each one is a self-contained handler with its own configuration; no function code is required. +C1 ships eleven pre-built hook patterns. Each one is a self-contained handler with its own configuration; no function code is required. | Pattern | Event | What it does | | :--- | :--- | :--- | -| **PII field redaction** | Post | Replaces values in JSON output fields whose names match a configurable list (defaults: `ssn`, `social_security_number`, `date_of_birth`, `salary`, `bank_account`) with a placeholder string. | -| **Credit card blocking** | Post | Scans tool output for Luhn-valid credit card numbers and denies the response if any are found. | -| **Query scope limit** | Pre | Caps numeric input fields (defaults: `limit`, `page_size`, `count`, `max_results`) at a configured maximum to prevent oversized queries. | -| **Write authorization** | Pre | Denies tool calls whose classification is in a blocked list, optionally only outside a configured business-hours window (timezone, start/end times, days of the week). | -| **Sensitive file guard** | Pre | Denies tool calls that reference sensitive file paths or directories (defaults: `.env`, `*.pem`, `*.key`, `id_rsa`, `.ssh/`, `.aws/`, and similar). | +| **PII field redaction** | Post | Replaces values in output fields whose names match a configurable list with a placeholder string | +| **Credit card blocking** | Post | Denies the response if output contains a Luhn-valid credit card number | +| **Query scope limit** | Pre | Caps numeric input fields at a maximum, to prevent oversized queries | +| **Write authorization** | Pre | Denies calls whose classification is in a blocked list, optionally only outside business hours | +| **Sensitive file guard** | Pre | Denies calls that reference sensitive file paths or directories | +| **Tool output size guard** | Post | Denies output exceeding a byte cap | +| **Secrets masking** | Post | Redacts secret-shaped values from output — AWS keys, bearer tokens, JWTs, private keys, Slack/GitHub tokens | +| **Link filter** | Post, Pre-output | Redacts or annotates URLs and markdown images whose host is not allowlisted, mitigating markdown-image exfiltration | +| **Encoded content guard** | Pre | Denies input containing long base64 blobs, long hex runs, or invisible unicode used to smuggle instructions | +| **Prompt injection scan** | Post | Scores output for prompt injection using the AI-governance judge and acts at or above a threshold | +| **Block output** | Pre-output | Withholds the in-flight response chunk | + +### Pattern configuration and defaults + + +Where a pattern takes a list, a value you configure **replaces** the default list — it does not extend it. If you add one field name to **PII field redaction**, the built-in field names stop applying unless you re-include them. + + +| Pattern | Configuration | Defaults | +| :--- | :--- | :--- | +| **PII field redaction** | Fields to redact; replacement text | `ssn`, `social_security_number`, `date_of_birth`, `dob`, `salary`, `bank_account`, `routing_number`; replacement `***REDACTED***` | +| **Credit card blocking** | None | — | +| **Query scope limit** | Fields to cap; maximum value | `limit`, `page_size`, `count`, `max_results`; maximum `100` | +| **Write authorization** | Blocked classifications (at least one); optional business-hours window with timezone, start/end times, and days | Read is not offered as a blockable classification | +| **Sensitive file guard** | Two separate lists: blocked file patterns, and blocked directories | Patterns `.env`, `.env.*`, `credentials.json`, `*.pem`, `*.key`, `id_rsa`, `*.pfx`; directories `.ssh`, `.aws`, `.config` | +| **Tool output size guard** | Maximum output size in bytes | `131072` (128 KB) | +| **Secrets masking** | None | — | +| **Link filter** | Allowed hosts; action (Redact or Annotate); whether to also act on images | Action defaults to Redact | +| **Encoded content guard** | None | — | +| **Prompt injection scan** | Risk threshold at which to act | — | +| **Block output** | Message; output surfaces | Surfaces default to **none** — see the warning below | + + +**A Block output hook with no surfaces selected never fires.** An empty surface list means *no* surface, not all of them — deliberately, so that forgetting to choose can't silently apply the hook everywhere. The failure mode is a hook that looks configured and enabled but withholds nothing. Select **Slack**, **Web**, or both. + ## Custom function hooks @@ -87,11 +151,21 @@ The function receives: "context": { "tool_source": "connector", "classification": "WRITE" + }, + "caller": { + "id": "", + "token_id": "...", + "mcp_client_id": "...", + "mcp_client_type": "..." } } ``` -`tool_source` is either `builtin` or `connector`. `classification` is the tool's configured action class (`READ`, `WRITE`, `DESTRUCTIVE`, `SENSITIVE`, or `DANGEROUS`). +`tool_source` is `builtin`, `connector`, or `claw`. `classification` is the tool's configured action class (`READ`, `WRITE`, `DESTRUCTIVE`, `SENSITIVE`, or `DANGEROUS`). `caller` identifies who made the call; `token_id`, `mcp_client_id`, and `mcp_client_type` are omitted when empty. + + +`caller` is for logging and call-shape decisions only — it is **never an authorization input**, because the grant decision is made before the hook runs. Identity-based scope belongs in [access profiles](/product/admin/tools-and-toolsets), not hooks. + ### Post-tool-use payload @@ -103,7 +177,8 @@ The function receives the same fields plus the call result: "input": { /* original input */ }, "output": { /* whatever the tool returned */ }, "error": "", - "context": { /* same as pre */ } + "context": { /* same as pre */ }, + "caller": { /* same as pre */ } } ``` diff --git a/product/admin/tools-and-toolsets.mdx b/product/admin/tools-and-toolsets.mdx index de324870..8bf53e64 100644 --- a/product/admin/tools-and-toolsets.mdx +++ b/product/admin/tools-and-toolsets.mdx @@ -5,7 +5,7 @@ og:title: Govern tools and toolsets - C1 docs og:description: Review discovered tools, approve and classify them, bundle them into toolsets, and bind toolsets to access profiles. --- -{/* Editor Refresh: 2026-05-08 */} +{/* Editor Refresh: 2026-07-30 */} **Activation required.** AI access management must be enabled for your tenant before you can use it. To get started, [contact the C1 support team](mailto:support@c1.ai) for a walkthrough. @@ -19,6 +19,8 @@ Tools must be reviewed and approved before end users can request them. Use the s ### View discovered tools +Discovered tools live under each registered MCP server's own **Tools** tab. + In **AI > MCP**, open a registered server. @@ -32,12 +34,15 @@ The list shows every tool C1 has discovered, its current state, classification, ### Classify a tool -C1 captures two classification axes per tool. Both are **metadata only** — there is no enforcement layer that uses them. They exist so admins can filter and reason about tools, and so enforcement can be built on top later. +C1 captures a single **Classification** field per tool. It isn't enforced at call time on its own, but it isn't purely decorative either: the auto-maintained **Read tools** toolset (see [C1-maintained toolsets](#c1-maintained-toolsets) below) selects its members by `Classification = Read`, so classifying a tool as Read widens what that toolset grants. Classification is also readable by [tool call hooks](/product/admin/tool-call-hooks) as `context.classification`, and it's the field the Write authorization hook pattern blocks against. -| Axis | Values | Meaning | -| :--- | :--- | :--- | -| **Action** | Read / Write / Delete | Whether the tool can mutate downstream state | -| **Risk** | Sensitive / Dangerous | Severity of the worst outcome if misused | +| Value | Meaning | +| :--- | :--- | +| **Read** | Doesn't mutate downstream state | +| **Write** | Mutates downstream state | +| **Destructive** | Deletes or irreversibly overwrites downstream state | +| **Sensitive** | Exposes or handles sensitive data | +| **Dangerous** | Severe potential impact if misused | To classify a tool: @@ -46,7 +51,7 @@ To classify a tool: Open the tool's detail panel from the **Tools** tab. -Set **Action** and **Risk**. +Set **Classification**. Click **Save**. @@ -99,10 +104,12 @@ A **toolset** is a named bundle of approved tools. ### C1-maintained toolsets -C1 ships and auto-maintains two toolsets per tenant: +C1 ships and auto-maintains two toolsets per connector: + +- **All approved tools** — every tool in **Approved** state on that connector. +- **Read tools** — every **Approved** tool on that connector with Classification = Read. -- **All approved** — every tool in **Approved** state across the server. -- **Read-only** — every **Approved** tool with Action = Read. +Their display names are suffixed with the connector's server name — for example, `All approved tools - Slack` and `Read tools - Slack`. Both update automatically as tools are approved or disabled. They are read-only — admins can bind them to access profiles but cannot edit their contents. @@ -158,7 +165,7 @@ You can also start from the access profile side and add toolsets as entitlements Go to **Access profiles** and either create a new profile or open an existing one. -Add the toolset as an entitlement — both C1-maintained toolsets (**All approved**, **Read-only**) and custom toolsets appear as options. +Add the toolset as an entitlement — both C1-maintained toolsets (**All approved tools**, **Read tools**) and custom toolsets appear as options. Set the access policy on the profile (auto-approve, JIT with expiry, or approval required).