Skip to content

add: a project's work folders, granted to every turn up front - #516

Merged
Yiannis128 merged 3 commits into
masterfrom
feat/work-folders
Nov 10, 2026
Merged

Yiannis128 merged 3 commits into
masterfrom
feat/work-folders

Conversation

@Yiannis128

@Yiannis128 Yiannis128 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

A long chat that worked in a repository outside Entoli's workspace left the sandbox 105 times, needed 72 approval prompts, and had 12 commands refused while the user was away. A project can now declare its work folders up front. Every turn in the project can then write to them inside the sandbox, with nothing to approve and no one needing to be there.

What it is

  • Most of the setting already existed. The project tier's writableDirs in project.json is where an add_directory "always for this project" answer lands, and it already reached the sandbox ruleset (Seatbelt and Landlock), the gate's "leaves the sandbox" decision, the file tools and the classifier's alsoWritable. This PR builds the UI over that same path rather than a parallel one. A folder granted on a banner shows in the list, and one removed from the list is gone at the next call.

  • Settings UI. The project settings page has a "Work folders" section (work_folders_section.dart, replacing the typed-text writable_dirs_section.dart that sat on the Permissions sub-page). "Add folder" opens the platform folder picker (a new FolderPicker port over file_selector's getDirectoryPath), and each row is the shared EditableRow (its onTap and subtitle are now optional) with a remove control.

  • One check for what a grant may name. refuseFolder in lib/domain/work_folders.dart returns a FolderRefusal, and both doors use it: the settings page and AddDirectoryTool.run. It refuses:

    • anything that is missing or not a directory;
    • the root;
    • the home, or anything holding it;
    • the app's data tree (the data directory, anything under it, anything holding it);
    • anything holding the workspaces.

    The settings page shows the refusal's message in a snack bar, and a row shows its row wording.

  • Behaviour change in add_directory: it now refuses ~, / and any folder holding the home or the workspaces, all of which it used to accept. Its tool description is unchanged; a sibling PR owns it.

  • Missing folders. The repository merges the tiers' lists once, without duplicates. The agent hands its gate that policy minus what grantsNow drops (anything refuseFolder refuses), checking each path once per turn rather than per tool call. A folder gone from this machine therefore grants nothing, and a command cannot recreate it under the grant. Its row says "Missing on this device". A ~ path passes through for the gate to resolve; isTildePath in shell.dart is now the one check for that, shared with the gate.

  • Telling the model. The turn's second system message gets a "## Work folders" section (turnPrompt's workFolders) naming the granted folders as writable, telling the model not to use unsandboxed or ask for access there. It reads the same filtered policy as the gate, and appears only where the new ApprovalGate.confines says commands run confined (the condition _sandboxFor uses).

  • Platforms. The section shows only for a local project whose picked driver's Confinement.enforcesSandbox is true, which means macOS and Linux on the host. It is hidden on Windows (following "What is absent, and honestly so"), on phones, under a Linux guest, for a built-in with rootfs isolation, and for a paired device's project, whose paths a picker here cannot name.

Storage: the folders stay in project.json

I did not follow the instruction_files.json precedent, and the user chose this. The field already lives in project.json and already reaches the sandbox and the gate. Moving it would cost a migration, and the only unreleased migration entry (7.5.0) has already run on real data directories, so the move would need a second entry that breaks the rule of naming entries by release version. A path that reaches another machine through a backup or sync names a folder that is not there, and that grants nothing. No migration is needed and no file is added.

Not touched: the bash_tool.dart and directory_tools.dart tool descriptions. Only AddDirectoryTool.run's checks changed.

Tests

  • sidebar_test: a picked folder is added as a row and its cross removes it; the data directory is refused; a vanished folder's row says it is missing; no section where the host confines nothing (none, appSandbox).
  • directory_tools_test: add_directory refuses ~, a folder holding the home, /, and a folder holding the workspaces, all before any banner.
  • permission_gate_test: the turn's writable folders reach the sandbox while they are folders on disk, and a missing folder or a file does not. The model is told about the work folders under the sandbox, and not with the sandbox off. Tests that granted made-up paths now use real temp folders.
  • chat_repository_test: a folder granted at two tiers reaches the turn once.
  • templates_test: the section is named only when there are folders.

Notes updated: lib/ui/settings/AGENTS.md (new "Work folders"), lib/data/builtin/AGENTS.md (sandbox, approvals, add_directory), and the root AGENTS.md (project.json contents and the index).

A project's settings page lists the folders outside the workspaces that
every turn in it may write to, added through the platform's folder picker
and removed per row. They are the project's existing writableDirs in
project.json, the list an add_directory "always for this project" answer
extends, so the sandbox (Seatbelt and Landlock), the approval gate and the
classifier already read them; what is new is the UI, the refusals, a folder
gone from the machine granting nothing, and the turn's context naming them.

Shown only for a local project where the picked driver enforces the
sandbox: not on Windows, phones, a guest, a built-in with rootfs isolation,
or a paired device's project.
refuseFolder in lib/domain/work_folders.dart is now the predicate both the
work folders and add_directory take, with the stricter set: not a
directory, the root, the home or anything holding it, the app's data tree,
and anything holding the workspaces. add_directory now refuses ~ and /,
which it used to accept.

The agent filters the persisted grants once per turn rather than the
repository on every call, the repository merges the tiers without
duplicates, the prompt asks the gate whether it confines a command, the
rows are EditableRow and read their reason from the refusal, and the
tilde check is one predicate.
# Conflicts:
#	lib/data/builtin/approval_gate.dart
#	test/data/builtin/permission_gate_test.dart
@Yiannis128
Yiannis128 merged commit ae2ffaa into master Nov 10, 2026
@Yiannis128
Yiannis128 deleted the feat/work-folders branch November 10, 2026 16:19
Yiannis128 added a commit that referenced this pull request Nov 10, 2026
Merge master (#512, #515, #516) and read the nudge's grantability from
refuseFolder, the work-folders check add_directory answers with.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant