Skip to content

Commit c44b437

Browse files
committed
Add path-based permissions for agents
1 parent f125df4 commit c44b437

File tree

10 files changed

+209
-6
lines changed

10 files changed

+209
-6
lines changed

assets/tool-approval-modal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,4 @@ <h4>Arguments</h4>
285285
Approve <span class="keyboard-hint">(Enter)</span>
286286
</button>
287287
</div>
288-
</div>
288+
</div>

docs/Agents.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,28 @@ aiagent:
107107

108108
This agent restricts tools to only the built-in read-only tools, preventing any page modifications.
109109

110+
### Sandboxed Agent with Path Restrictions
111+
112+
An agent that can only operate on pages within a specific folder:
113+
114+
```yaml
115+
---
116+
tags: meta/template/aiAgent
117+
aiagent:
118+
name: "Sandbox Agent"
119+
description: "Can only access pages under Sandbox/"
120+
systemPrompt: |
121+
You help the user with notes in the Sandbox folder.
122+
You cannot access or modify pages outside this area.
123+
allowedReadPaths: ["Sandbox/"]
124+
allowedWritePaths: ["Sandbox/"]
125+
---
126+
```
127+
128+
This agent can read and write pages under `Sandbox/` but will get an error if it tries to access other pages via tools that support path permissions.
129+
130+
> **Note:** Path permissions only apply to tools that declare `readPathParam` or `writePathParam`. Tools like `eval_lua` can bypass these restrictions. For a true sandbox, combine path permissions with a tool whitelist.
131+
110132
### Writing Assistant with Context
111133

112134
An agent with additional context embedded from wiki-links:
@@ -186,6 +208,8 @@ aiagent:
186208
| `tools` | string[] | Whitelist - only these tools are available |
187209
| `toolsExclude` | string[] | Blacklist - these tools are removed |
188210
| `inheritBasePrompt` | boolean | Prepend base system prompt (default: true) |
211+
| `allowedReadPaths` | string[] | Path prefixes tools can read from (e.g., `["Journal/", "Notes/"]`) |
212+
| `allowedWritePaths` | string[] | Path prefixes tools can write to (e.g., `["Journal/"]`) |
189213

190214
### Base Prompt Inheritance
191215

@@ -199,6 +223,38 @@ By default, agents inherit the base system prompt which includes SilverBullet ma
199223

200224
**Tip:** Use `tools` (whitelist) for restrictive agents that should only have specific capabilities. Use `toolsExclude` (blacklist) when you want most tools but need to block a few dangerous ones like `eval_lua`.
201225

226+
### Path Permissions
227+
228+
Restrict which pages an agent can read from or write to using path prefixes:
229+
230+
```yaml
231+
---
232+
tags: meta/template/aiAgent
233+
aiagent:
234+
name: "Journal Assistant"
235+
description: "Helps with journal entries only"
236+
allowedReadPaths: ["Journal/", "Daily/"]
237+
allowedWritePaths: ["Journal/"]
238+
---
239+
```
240+
241+
Or in Lua:
242+
243+
```lua
244+
ai.agents.journal = {
245+
name = "Journal Assistant",
246+
allowedReadPaths = {"Journal/", "Daily/"},
247+
allowedWritePaths = {"Journal/"}
248+
}
249+
```
250+
251+
**How it works:**
252+
- If `allowedReadPaths` is set, tools with `readPathParam` can only read pages starting with those prefixes
253+
- If `allowedWritePaths` is set, tools with `writePathParam` can only write to pages starting with those prefixes
254+
- If not set, no path restrictions apply
255+
256+
This is useful for creating restricted agents that can only operate on specific areas of your space.
257+
202258
## Usage
203259

204260
1. **Select Agent**: Run `AI: Select Agent` command

docs/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This page is a brief overview of each version.
77
- Embeddings now include page title and section headers
88
- Benchmark command now shows progress
99
- Reuse SB's theming where possible so that the UI is more consistent
10+
- Add path-based permissions for agents (`allowedReadPaths`, `allowedWritePaths`) to restrict tool access to specific folders
1011

1112
## 0.6.2 (2025-01-05)
1213

docs/Tools.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ Each tool needs:
8585
- `parameters` - JSON Schema defining input parameters
8686
- `handler` - Function that receives `args` and returns a string result
8787
- `requiresApproval` - (optional) If `true`, user must confirm before the tool executes
88+
- `readPathParam` - (optional) Parameter name(s) containing page paths for read operations. Can be a string or array of strings. (used with agent path permissions)
89+
- `writePathParam` - (optional) Parameter name(s) containing page paths for write operations. Can be a string or array of strings. (used with agent path permissions)
8890

8991
## Requiring Approval
9092

@@ -137,4 +139,53 @@ The `ai.writePage` function:
137139

138140
All built-in editing tools (`update_note`, `update_frontmatter`, `create_note`, etc.) use `ai.writePage` internally to provide diff previews.
139141

140-
There's nothing stopping you from bypassing this, so please be careful when making custom tools.
142+
There's nothing stopping you from bypassing this, so please be careful when making custom tools.
143+
144+
## Path Permissions
145+
146+
Tools can declare which parameter contains a page path for permission validation. When an agent has `allowedReadPaths` or `allowedWritePaths` configured, tools will be blocked from accessing pages outside those paths.
147+
148+
### Declaring Path Parameters
149+
150+
```lua
151+
ai.tools.my_reader = {
152+
description = "Read data from a page",
153+
readPathParam = "page", -- This param will be validated against allowedReadPaths
154+
parameters = {
155+
type = "object",
156+
properties = {
157+
page = {type = "string", description = "The page to read"}
158+
},
159+
required = {"page"}
160+
},
161+
handler = function(args)
162+
return space.readPage(args.page)
163+
end
164+
}
165+
166+
ai.tools.my_writer = {
167+
description = "Write data to a page",
168+
writePathParam = "page", -- This param will be validated against allowedWritePaths
169+
requiresApproval = true,
170+
parameters = {
171+
type = "object",
172+
properties = {
173+
page = {type = "string", description = "The page to write"}
174+
},
175+
required = {"page"}
176+
},
177+
handler = function(args)
178+
ai.writePage(args.page, "content")
179+
return "Written"
180+
end
181+
}
182+
```
183+
184+
### How It Works
185+
186+
1. Agent defines `allowedReadPaths` and/or `allowedWritePaths` (see [[Agents]])
187+
2. When a tool is called, the validation checks if the path parameter starts with any allowed prefix
188+
3. If the path is not allowed, the tool returns an error instead of executing
189+
4. Write operations require **both** read and write access (since tools typically read content before modifying it)
190+
191+
All built-in tools declare their path parameters, so they work with agent path permissions automatically.

silverbullet-ai/Space Lua/AI Tools.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ end
8888
8989
ai.tools.read_note = {
9090
description = "Read the content of a note. Optionally read only a specific section.",
91+
readPathParam = "page",
9192
parameters = {
9293
type = "object",
9394
properties = {
@@ -152,6 +153,7 @@ ai.tools.read_note = {
152153
153154
ai.tools.list_pages = {
154155
description = "List pages in the space. Pages ending with / have subpages (e.g., 'Projects/' has children).",
156+
readPathParam = "path",
155157
parameters = {
156158
type = "object",
157159
properties = {
@@ -232,6 +234,7 @@ ai.tools.list_pages = {
232234
233235
ai.tools.get_page_info = {
234236
description = "Get metadata about a page including tags, last modified date, size, and subpage count",
237+
readPathParam = "page",
235238
parameters = {
236239
type = "object",
237240
properties = {
@@ -271,6 +274,7 @@ ai.tools.get_page_info = {
271274
272275
ai.tools.update_note = {
273276
description = "Update a note's content. Can update the whole page, a specific section, or append/prepend to a section.",
277+
writePathParam = "page",
274278
parameters = {
275279
type = "object",
276280
properties = {
@@ -353,6 +357,7 @@ ai.tools.update_note = {
353357
354358
ai.tools.search_replace = {
355359
description = "Find and replace text in a note. Use for targeted edits when you know the exact text to change.",
360+
writePathParam = "page",
356361
parameters = {
357362
type = "object",
358363
properties = {
@@ -451,6 +456,7 @@ ai.tools.search_replace = {
451456
452457
ai.tools.create_note = {
453458
description = "Create a new note. Fails if the page already exists.",
459+
writePathParam = "page",
454460
parameters = {
455461
type = "object",
456462
properties = {
@@ -470,6 +476,7 @@ ai.tools.create_note = {
470476
471477
ai.tools.navigate = {
472478
description = "Navigate to a page or position. Use to open pages for the user or jump to specific locations.",
479+
readPathParam = "ref",
473480
parameters = {
474481
type = "object",
475482
properties = {
@@ -512,6 +519,7 @@ ai.tools.eval_lua = {
512519
ai.tools.update_frontmatter = {
513520
description = "Update frontmatter (YAML metadata) on a page. Can set or delete individual keys without affecting the rest of the page content.",
514521
requiresApproval = true,
522+
writePathParam = "page",
515523
parameters = {
516524
type = "object",
517525
properties = {
@@ -576,6 +584,7 @@ ai.tools.update_frontmatter = {
576584
ai.tools.rename_note = {
577585
description = "Rename a page to a new name. This also updates all backlinks across the space to point to the new name.",
578586
requiresApproval = true,
587+
writePathParam = {"oldPage", "newPage"},
579588
parameters = {
580589
type = "object",
581590
properties = {

src/agents.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export async function discoverAgents(): Promise<AIAgentTemplate[]> {
2222
tools?: string[];
2323
toolsExclude?: string[];
2424
inheritBasePrompt?: boolean;
25+
allowedReadPaths?: string[];
26+
allowedWritePaths?: string[];
2527
}
2628
>;
2729

@@ -36,6 +38,8 @@ export async function discoverAgents(): Promise<AIAgentTemplate[]> {
3638
tools: agent.tools,
3739
toolsExclude: agent.toolsExclude,
3840
inheritBasePrompt: agent.inheritBasePrompt,
41+
allowedReadPaths: agent.allowedReadPaths,
42+
allowedWritePaths: agent.allowedWritePaths,
3943
},
4044
});
4145
}

src/benchmark.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { aiSettings, configureSelectedModel, initializeOpenAI, parseDefaultModel
33
import { getAllAvailableModels } from "./model-discovery.ts";
44
import { convertToOpenAITools, discoverTools, runAgenticChat } from "./tools.ts";
55
import type { ToolExecutionResult } from "./tools.ts";
6-
import type { ModelConfig, Tool } from "./types.ts";
6+
import type { ModelConfig, PathPermissions, Tool } from "./types.ts";
77
import type { ProviderInterface } from "./interfaces/Provider.ts";
88
import { showProgressModal } from "./utils.ts";
99

@@ -19,6 +19,10 @@ const BENCHMARK_ALLOWED_TOOLS = [
1919
"search_replace",
2020
"create_note",
2121
];
22+
const BENCHMARK_PERMISSIONS: PathPermissions = {
23+
allowedReadPaths: [`${BENCHMARK_PAGE}/`],
24+
allowedWritePaths: [`${BENCHMARK_PAGE}/`],
25+
};
2226

2327
function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
2428
return new Promise((resolve, reject) => {
@@ -389,6 +393,7 @@ async function runExecutionTest(
389393
onToolCall: (name, args, execResult) => {
390394
toolCalls.push({ name, args, result: execResult });
391395
},
396+
permissions: BENCHMARK_PERMISSIONS,
392397
});
393398

394399
const passed = await test.verify(toolCalls, result.finalResponse);

src/chat-panel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ async function runToolLoop(
298298
onToolCall: (_toolName, _args, _result) => {
299299
// Tool calls are already formatted in toolCallsText and used below
300300
},
301+
permissions: currentChatAgent?.aiagent
302+
? {
303+
allowedReadPaths: currentChatAgent.aiagent.allowedReadPaths,
304+
allowedWritePaths: currentChatAgent.aiagent.allowedWritePaths,
305+
}
306+
: undefined,
301307
});
302308

303309
// Update usage to reflect current context size (not cumulative)

0 commit comments

Comments
 (0)