Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
72b2ac4
refactor(rules): simplify glob rule matching logic
senamakel Oct 9, 2026
745e127
refactor(rules): move rule types into a dedicated module
senamakel Oct 9, 2026
dd41ca5
refactor(rules): extract subject validation into its own module
senamakel Oct 9, 2026
d46d4f2
refactor(rules): simplify eval rule handling
senamakel Oct 9, 2026
af99504
chore(rules): register the rules module
senamakel Oct 9, 2026
02bc40c
feat(tinytools): add tool type definitions
senamakel Oct 9, 2026
1a615b4
chore(rules): add tests for module rule matching
senamakel Oct 9, 2026
6d766fc
style: apply rustfmt formatting to rules and tool types
senamakel Oct 9, 2026
4242698
refactor(rules): build denial message from parts
senamakel Oct 9, 2026
6191e93
test(tinytools): update tool trait impl to return static str
senamakel Oct 9, 2026
59e1cd3
docs: document the tool rules module and its spec
senamakel Oct 9, 2026
7ca7243
feat(rules): add eval rule support
senamakel Oct 9, 2026
e669ba2
refactor(rules): extract rule tests into a dedicated module
senamakel Oct 9, 2026
9399683
docs(specs): link tool rules spec to its plan
senamakel Oct 9, 2026
26b8ce0
test(tinytools): cover default tool-rule metadata
senamakel Oct 9, 2026
e7d83ec
test(rules): cover category wire names and arg pointer normalisation
senamakel Oct 9, 2026
42b892a
feat(rules): add subject rule for commit message validation
senamakel Oct 9, 2026
5abb92f
docs(rules): document indirect call arguments
senamakel Oct 9, 2026
138920e
fix(rules): evaluate an indirect target against its own arguments
senamakel Oct 9, 2026
6f69b33
test(rules): pin that a deny cannot be sidestepped by name case
senamakel Oct 9, 2026
fabda18
fix(rules): honour legacy external effect declarations
senamakel Oct 9, 2026
738ce29
fix(rules): let per-call external effect supersede the conservative d…
senamakel Oct 9, 2026
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ crates/
├── context/ # `ToolRunContext`
├── progress/ # `ToolProgress`, `ProgressSink`
├── naming/ # rendering a call for a human
├── rules/ # `ToolRules`: declarative allow/deny/hide/approval rules
└── shared/ # `SharedTool`: an `Arc<dyn Tool>` as an owned belt entry
# each: mod.rs / types.rs / mod_tests.rs
docs/
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ compiles neither the harness nor the host.
| `naming` | `humanize_tool_name`, `context_detail_from_args` — rendering a call for a human |
| `shared` | `SharedTool`, `share_belt`, `owned_belt` — one built `Arc<dyn Tool>` handed out as many owned `Box<dyn Tool>` belts, forwarding every trait method |
| `rank` | `ToolRanker`, `RankCandidate`, `RankHit`, `Bm25Ranker` — ranking a catalogue of tools against an intent, with the lexical ranker built in |
| `rules` | `ToolRules`, `ToolRuleSet`, `ToolRule`, `ToolSubject`, `RuleDecision` — declarative allow / deny / hide / approval rules over tools, evaluated by a harness on the catalogue, search and call surfaces |

The workspace also contains `tinytools-agent`, a separate crate for
model-facing tool-call parsing, dialects, catalogue/result rendering, and
Expand Down
6 changes: 6 additions & 0 deletions crates/tinytools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub mod policy;
pub mod progress;
pub mod rank;
pub mod result;
pub mod rules;
pub mod shared;
pub mod spec;
pub mod tool;
Expand Down Expand Up @@ -148,6 +149,11 @@ pub use rank::{
Bm25Index, Bm25Ranker, RankCandidate, RankContext, RankError, RankHit, ToolRanker, tokenize,
};
pub use result::{FileData, ImageData, ToolContent, ToolControl, ToolErrorKind, ToolResult};
pub use rules::{
ApprovalDirective, ArgMatcher, DefaultEffect, IndirectCall, Patterns, RuleContext,
RuleDecision, RuleEffect, RuleRef, SideEffect, Surface, ToolMatcher, ToolRule, ToolRuleSet,
ToolRules, ToolSubject, glob_matches,
};
pub use shared::{SharedTool, owned_belt, share_belt};
pub use spec::ToolSpec;
pub use tool::{Tool, ToolExposure};
Expand Down
50 changes: 50 additions & 0 deletions crates/tinytools/src/rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `rules`: declarative tool rules

Allow, deny, hide and approval rules over tools, written by a host and
evaluated by a harness on every surface a tool reaches the model on.
Specification: [`docs/specs/tool-rules.md`](../../../../docs/specs/tool-rules.md).

## Design

A host restricts tools from many places, such as an agent's allowlist, a
channel's permission ceiling, an MCP server filter or a connector's curated
actions. This module gives all of them one vocabulary. It holds **no policy**:
the rules are data the host writes, and evaluation is mechanical. That is the
same line `deferral` draws.

| File | Holds |
|---|---|
| `glob.rs` | `glob_matches`: `*` and `?`, ASCII case-insensitive, with no dependency |
| `types.rs` | `ToolRule`, `ToolMatcher`, `ArgMatcher`, `ToolRules`, `ToolRuleSet`, `RuleContext`, `RuleDecision`, `RuleRef`, `ApprovalDirective` |
| `subject.rs` | `ToolSubject`: what is evaluated, built from a live tool or by hand |
| `eval.rs` | Evaluation of a rule, a layer, a set, and a concrete call |

## Public surface

- **`ToolRule`** has an effect (`allow`, `deny`, `hide`, `require_approval`,
`auto_approve`), the surfaces it applies on (`catalog`, `search`, `call`), a
`match`, an optional `except` carve-out, `when` context conditions, and an
`id` and `reason` for refusals.
- **`ToolRules`** is one layer with a default. Inside a layer, effects combine
without regard to order:
- `deny` wins.
- With a `deny` default, a tool needs a matching `allow`.
- `hide` only clears visibility on the listing surfaces.
- `require_approval` beats `auto_approve`.
- **`ToolRuleSet`** stacks layers, and every layer must admit a tool. Adding a
layer can only narrow, so two allowlists intersect.
- **`ToolRuleSet::evaluate_call`** evaluates a call with the tool's
argument-aware permission. When the tool reports an `indirect_target`, an
`IndirectCall { target, arguments }`, it evaluates that target too. It uses
the target's own arguments when the dispatcher wraps them in an envelope, so
an argument-scoped rule cannot be sidestepped through the dispatcher.

## Operational constraints

- A matcher field naming an attribute the subject does not know does not
match. That fails open for `deny` and closed for `allow`, so evaluate
against the live tool wherever you have one.
- Off the call surface an `arg` condition cannot be decided. An `allow` reads
it optimistically and every other effect does not apply.
- A wrapper tool must forward `tags` and `indirect_target`, as `SharedTool`
does. Otherwise tag rules miss and a dispatcher's target escapes its rules.
Loading
Loading