feat: file system access, behind a path policy and an approval (#25)

This commit was merged in pull request #25.
This commit is contained in:
2026-10-07 09:04:05 +00:00
parent 34c4383428
commit 2adc6d74b5
18 changed files with 5481 additions and 38 deletions
+25 -1
View File
@@ -1,6 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { environment } from "@vicinae/api";
import { runAgent } from "../agent";
import { confirmWithAlert, createApprovalLedger } from "../approval";
import { toStoredAttachment } from "../attachments";
import { createPolicy, displayPath } from "../files";
import { ipPlace } from "../geo";
import { buildMessages, composeSystemPrompt } from "../history";
import { isSelfHosted } from "../ollama";
@@ -205,6 +208,18 @@ export function useChat(options: UseChatOptions): UseChat {
const controller = new AbortController();
abortRef.current = controller;
// Built per turn rather than per session: the ledger inside it is the record of
// what the user approved while answering this one question, and the next question
// starts by asking again. The policy is cheap to rebuild and holds no state.
const policy = createPolicy({
supportPath: environment.supportPath,
readRoots: preferences.extraReadRoots,
writeRoots: preferences.extraWriteRoots,
});
const fileAccess = preferences.fileEnabled
? { policy, approve: createApprovalLedger(confirmWithAlert).approve }
: undefined;
const messages = buildMessages(
composeSystemPrompt(preferences.systemPrompt, {
model: preferences.model,
@@ -212,6 +227,11 @@ export function useChat(options: UseChatOptions): UseChat {
selfHosted: isSelfHosted(preferences.host),
webTools: preferences.webEnabled,
vision: visionRef.current,
fileTools: preferences.fileEnabled,
fileRoots: {
read: policy.readRoots.map((root) => displayPath(root, policy.home)),
write: policy.writeRoots.map((root) => displayPath(root, policy.home)),
},
}),
base.turns,
prompt,
@@ -233,7 +253,10 @@ export function useChat(options: UseChatOptions): UseChat {
// cap, so the two conventions meet here.
maxToolRounds: preferences.maxToolRounds ?? null,
messages,
tools: toolsFor({ webEnabled: preferences.webEnabled }),
tools: toolsFor({
webEnabled: preferences.webEnabled,
fileEnabled: preferences.fileEnabled,
}),
toolContext: {
host: preferences.host,
apiKey: preferences.apiKey,
@@ -241,6 +264,7 @@ export function useChat(options: UseChatOptions): UseChat {
// Only wired up when the user switched it on, so an off setting
// cannot reach the network for a location at all.
ipPlace: preferences.ipGeolocation ? ipPlace : undefined,
files: fileAccess,
},
signal: controller.signal,
events: {