Permission system¶
The core policy has four owners. mevedel-permission-mode.el owns mode
normalization and session-scoped transitions; mevedel-permission-rules.el
owns rule matching, precedence buckets, protected-path policy, and resource
grants; mevedel-permission-persistence.el owns authority-store validation and
target-aware I/O. mevedel-permissions.el is the decision facade that combines
those facts with tool policy. mevedel-permission-review.el owns optional
automatic exception review before human queue admission.
mevedel-implementation-policy.el owns accepted implementation-policy data and
exact model resolution. The mode owner projects its request-local permission
override through this same decision facade; implementation policy is not a
separate rule store or a session-mode transition.
Decision flow¶
flowchart TD
Input[Operation and resource facts] --> Absolute{Absolute deny or workflow restriction?}
Absolute -->|Yes| Deny[Deny]
Absolute -->|No| Tool[Resolve tool or command authority]
Tool --> Resource[Resolve independent resource authority]
Resource --> Result{Combined decision}
Result -->|Denied| Deny
Result -->|Missing authority| Ask[Permission hooks and user decision]
Result -->|Authorized| Allow[Continue within approved confinement]
Ask -->|Denied| Deny
Ask -->|Approved| Allow
The diagram shows authority composition; the ordered resolver below supplies the exact precedence. Bash and batch Eval also resolve their child-confinement capabilities before execution. An approval applies only within the requested and approved scope; it cannot override an absolute denial or workflow ceiling.
Single decision function mevedel-check-permission. Decision chain:
- Extract specifier values via
get-path/get-pattern/get-domain/get-nameslots - Deny rules (across all buckets — see bucket precedence below)
- Workflow restrictions: standalone/sticky Plan denies native edit tools and
Eval, except ApplyPatch whose every operand is a session-owned
work://descendant. Directive Planning denies all native edits and Eval. These restrictions apply regardless of allow rules or permission mode. An agent turn started under Plan keeps them for its lifetime through its Plan read-only stamp. - Full Access bypasses ordinary asks, resource defaults and review; explicit
tool denials still apply. Otherwise the tool's own
check-permissionslot decides command authority - Allow/ask rules (innermost-bucket-first — see bucket precedence below)
- For a path not directly covered by a native path rule, resolve an allowed root, exact allowed path, or covering resource grant
- A protected or outside-root path without that authority → ask
- Permission-mode fallback when no earlier policy decides; in Edits, ordinary native reads cover the OS-readable filesystem except inaccessible protected paths, and Bash/batch Eval run with required confinement
The permission mode itself never denies: modes decide between automatic
allowance and a prompt, and every hard denial is a step-2 or step-3 policy.
ApplyPatch is the deliberate exception to step 8. Its reviewed-edit capability
is allowed because ask mode already supplies mandatory per-file/hunk review
before any write. Allowed-root patches therefore proceed directly to that
review. Protected or outside-root paths still require resource authority before
the review appears, and explicit allow/ask/deny rules retain their precedence.
For a tool with a command checker, command authority and filesystem resource
authority are layered: both must allow. A plain command allow cannot authorize its
path, and a resource grant cannot authorize its command. A matching direct
command rule with :file-system supplies both operation and scoped filesystem
authority for that execution. Native :path rules remain direct tool
authorization, but cannot bypass a protected path's
resource-grant requirement.
Resource authority normally follows the tool's mutability: a read-only tool's
paths need read authority and any other tool's paths need write authority. A
path-or-resource argument that a mutating tool declares :access read
is the exception. Each such path is authorized separately as a read: in Edits an
unprotected path needs no prompt, a protected path needs a covering read
grant, and an Ask prompt offers a read grant, never a write grant. A path the
tool also writes keeps write authority. The tool's own mode and workflow
decisions are unchanged, so SharedEdit is still refused in Plan whatever it
reads.
Hook integration sits around this chain:
PreToolUseruns before the chain. A hookdenyis final. A hookaskcan tighten an allow into a prompt in Ask or Edits. A hookallowcan only skip a prompt when the normal resolver would have returnedask; explicit denies still win. The resultingaskstill crossesPermissionRequestonce before that earlier allow suppresses queue admission. Full Access ignores hook asks but retains hook denies.PermissionRequestruns whenever generic, Bash, Eval, or sandbox authority resolution reachesask, before the corresponding entry enters the shared queue. It can allow, deny, or leave the prompt in place. Queue display and rule-driven re-evaluation do not rerun it. Every approval source, including hook and fallback approvals, rechecks current policy at settlement in every mode. Hard denies still apply before queue admission, even without a mode transition. A cancelled request cannot admit a new card or automatic review, or resume execution through a late approval. Prepared session-owned patch classification is retained through prompts and automatic review, so valid Plan-file edits remain distinguishable from forbidden ordinary or shared writes.PermissionDeniedruns after any final denial. It can adjust the reason/context shown to the model, but it cannot turn the denial into an allow. Its payload identifies the original policy, user, reviewer,PreToolUse, orPermissionRequestprovenance.
mevedel-tool-permission-decide-now decides a call that a running tool makes
on the model's behalf without dispatching it, such as WebFetch following a
redirect to another host. It runs the same decision chain under a given
buffer's session, request, agent invocation and buffer-local policy, but never
prompts and never runs hooks: an allow becomes ask when a PreToolUse
handler would run for the tool, since only a dispatched call runs it. A dead
buffer or one without a session yields ask. The caller treats ask as "make
this a fresh tool call", which then receives the full chain, hooks and prompt
included. Each decision is written to the permission log with its reason.
Permission invocation context is normalized in the decision facade before callers enter the decision chain. That context centralizes specifier extraction, rule buckets, mode, allowed roots, resource grants, missing-session fallback warnings, and the prompt rule shape used for outside-root approvals.
Resource-address authority¶
A resource address identifies a target; it is not a permission grant. After repair and final validation, resource preparation resolves an opaque attempt and only the logical authority facts needed here, without reading content. Permission then applies the ordinary operation policy before the authorized handler consumes that attempt. Malformed or unsupported addresses, traversal, and containment failures stop before permission and post-use hooks; a valid but missing, disconnected, stale, or unreadable resource follows ordinary handler failure handling.
Read-only artifact://, skill://, agent://, history://, memory://, and memory://journal/
resources keep their intrinsic read capability and current freshness rules.
work:// and explicit memory-file mutation use ordinary ApplyPatch permission
and patch review. Shared and memory operands use their real backing paths for
protected-path and workspace-boundary decisions;
recognizing an address does not broaden roots or create a grant. Skill and
memory addresses retain client-local origin, while MCP authority remains with
the current configured connection. Standalone/sticky Plan mode allows session-only
ApplyPatch, including requests from retained agents, but denies any proposal
containing an ordinary, shared, memory, or bare endpoint before local materialization
or ordinary mutation. Mixed local/ordinary and ordinary-only proposals
therefore fail tree-wide; permission modes and allow rules cannot reopen that
boundary. See
address-to-resource.md.
The synchronous and asynchronous decision entry points then share one pure preflight. It normalizes decision facts, resolves absolute deny rules, and records protected-path and resource-boundary facts exactly once. Both paths use the same synchronous tool slot adapter and decision tail; only a tool that supplies an asynchronous permission callback introduces an asynchronous branch.
Bucket precedence¶
Steps 2 and 5 consume rules from multiple buckets, in this order:
invocation skill-permission-rules, request skill-permission-rules,
session rules, persistent rules, defcustom mevedel-permission-rules.
- Step 2 (deny) is absolute — any bucket's
denywins. - Step 5 (allow/ask) is innermost-first — the first bucket yielding any decision wins.
- Directive discussion installs request-scoped denies for every registered mutation-capable tool and for mutation-capable delegation. These denies are a hard capability ceiling: broader session, workspace, or global allows cannot enable mutation during that discussion request.
- Goals use the same tool permission chain as ordinary root conversation turns. An accepted-plan Goal may supply a durable implementation mode for its active root turns, but neither Goal lifecycle nor that policy changes the session's baseline permission mode.
- Standalone Plan approval captures the selected Ask, Edits, or Full Access mode on the Direct implementation request, or on the Goal for active implementation turns. Here and Worktree both leave session baselines unchanged; retries and Goal resume reuse the accepted policy. Ordinary chat with a paused, blocked, budget-limited, or complete Goal uses the baseline. An admitted turn keeps its captured override through settlement.
- Directive implementation selections use the same request-local mode path. Summary, control, unrelated child, and directive requests do not inherit an ambient Goal implementation policy. No policy snapshot is restored at settlement, so concurrent user changes to session defaults are not overwritten.
The common effective-mode resolver checks the owning request's override, then the child invocation or pending submission when no request owns the decision, and otherwise the live session baseline. Asynchronous checks retain their explicit request/invocation identity rather than borrowing a newer request in the same buffer. The override changes mode fallback and confinement selection, not rule ownership: live hard denies, shared rules, resource grants, and workflow restrictions still pass through the ordinary chain.
Rule format¶
Rules live on mevedel-permission-rules with form
(TOOL-NAME &key SPECIFIER VALUE :network BOOL :file-system GRANTS
:sandbox-permissions LEVEL :action ACTION).
One specifier per rule:
| Key | Matches | Used by |
|---|---|---|
:path |
path (glob, ~ exp.) |
Read, ApplyPatch, Glob, Grep, ... |
:pattern |
command/expression glob | Bash; deliberately authored Eval rules |
:expression |
whole expression, literal | Remembered Eval approvals and execution profiles |
:domain |
host name (glob) | WebFetch |
:name |
free-form name (glob) | Agent (role) |
Precedence: specifier rules outrank generic; within a group
deny > ask > allow. Ask and Edits retain explicit ask rules. Full Access
ignores ask rules and default protected paths; explicit denies remain absolute.
:sandbox-permissions is an execution-level qualifier, not a request to raise
authority. A rule carrying require-escalated is considered only after Bash or
batch Eval has explicitly requested that level. Ordinary command allows cannot
authorize full escalation in Ask or Edits. Only direct user-authored session, persistent, and
defcustom rules may allow it; delegated invocation/request rules cannot. A
pattern scopes authority to the matching Bash command or Eval expression, and
omitting the pattern deliberately authorizes every expression for that tool at
that execution level. Qualified and ordinary explicit denies remain final.
:network and :file-system form an execution permission profile on a
matching Bash or batch-Eval allow rule. The profile records the additive child
authority approved with that operation; it is not a workload classifier.
Single-segment Bash approvals may use the recognized safe command pattern.
Compound commands keep generalized operation rules for their segments but
store the profile against the complete compound command, preventing one
segment from inheriting another segment's capability.
Filesystem entries are (:path PATH :access read-or-write [:recursive t])
grants. PATH is either absolute or home-relative with ~/~/; stored
home-relative paths expand in the execution target. Grant paths are literal —
*, **, and ? carry no glob meaning; :recursive t extends an entry from
the exact path to the directory and all its descendants, present and future.
Project and session stores
encode exact paths and absolute path rules without a client-specific TRAMP
prefix, then qualify them through the currently opened matching target. A
foreign target prefix invalidates the stored authority instead of being
reinterpreted. Entries in a direct allow rule authorize their own filesystem
access for matching executions. They need no companion :resource-grants entry
and do not grant native tools or unrelated commands access. Independent resource
grants remain sufficient for explicitly requested access, but are not
automatically attached to every child. Multiple matching profiles
are unioned, with write dominating read for the same path and scope; exact and
recursive entries on the same path stay distinct. Only session, persistent,
and defcustom rules can contribute a reusable profile. Invocation- and
request-scoped delegated rules may still authorize ordinary operations but
never broaden the child sandbox.
mevedel-protected-paths is an alist from glob to read-only or
inaccessible. The default .git glob is read-only, and so is the
artifact store's bookkeeping under
.mevedel/artifacts/.state/ -- metadata, shared item state, discussions and versions -- and
.mevedel/leases/, so a model write cannot bypass an item's lease, versions
and validation. These directory boundaries cover future artifact ids and missing
bookkeeping files too. Authored files under .mevedel/artifacts/ID/ stay writable,
including file creation and atomic replacement. Before a confined launch, missing
protected state directories and their parents are created; only the protected
leaf is mounted read-only. Missing-directory creation and mode changes run
through pinned target-side parents using the writable roots' captured canonical
spellings, so an ancestor replacement during preparation cannot redirect them.
All missing directories share one parent-before-child program; a creation
conflict refuses preparation before changing the conflicting directory's mode.
Existing directories need no creation program. These directories and their parents remain after
execution, since removing an empty directory could detach another running
sandbox's protection and allow it to recreate that path as writable. If a prepared state
directory disappears before launch, confinement refuses to start the command.
The default SSH,
GnuPG, AWS, Azure, Google Cloud, and Kubernetes credential globs are
inaccessible. On
a trailing /**, policy covers both the directory and its descendants. On a
remote session, leading ~ uses the target user's probed home; a
client-absolute custom pattern stays in the client path domain and therefore
does not become a remote protection rule.
String-only entries are invalid by design. Glob discovery walks the
workspace, memory, and additional writable roots before every launch; the
execution target's temporary directory is excluded as a discovery root. A
workspace or additional root beneath it is still searched when independently
listed. Other repositories placed only in temporary scratch do not receive
glob-discovered protection. Local preparation defaults to
temporary-file-directory when no explicit temporary root is supplied, including
one-shot helpers, so it does not plant synthetic Git or workspace metadata in
the shared temporary root.
On local Linux, literal directory globs such as **/.git/** use GNU find
when available. Discovery still runs before each launch: it follows a symlink
only at the supplied root, prunes matched and unreadable directories, and includes
Git worktree pointer files. NUL framing preserves spaces and newlines in names.
Managed Bash and one-shot helpers run native scans asynchronously under their
execution owner. Cancellation, owner teardown or changed launch authority prevents
the pending command from starting. Results are never reused across launches.
Terminal review diffs are settlement work rather than producer commands: they
can finish after request cancellation so an aborted turn still has a review
patch. They retain request identity, owner liveness, target and policy checks;
replacement requests or changed authority still refuse their pending launch.
A failed or truncated native scan refuses preparation; it does not discard missing protection.
A pattern below **/.mevedel/, mevedel's own state directory, which exists
only at a workspace root, protects in the Bash sandbox the whole directory its
literal part names, at each discovery root and without a walk:
**/.mevedel/artifacts/.state/** is one mount however many artifacts there
are. A literal file pattern protects the file. Native
tool checks match the pattern itself, at any depth; a .mevedel nested deeper
in a root is covered by native tool checks only.
Other patterns and targets retain the Lisp walker. Canonical target checks and
final confinement validation remain unchanged.
The three canonical modes are ask, edits, and full-auto:
| Mode | Native tools | Bash and Eval | Confinement |
|---|---|---|---|
ask |
Root/resource checks and patch review | Recognized inspection automatic; uncertain execution asks | Configured sandbox preference |
edits |
Native edits automatic within roots; ordinary reads span OS-readable paths, retaining credential masks | Arbitrary Bash and batch Eval automatic; live Eval and additional authority require approval | Required; an unavailable backend refuses execution |
full-auto |
Full Access | Automatic, including live Eval and explicit escalation | Off; filesystem and network unrestricted under the target OS account |
Full Access bypasses ordinary ask rules, default protections, resource boundaries, hook asks and automatic review. Explicit hard denies, Plan restrictions, validation, and session ownership remain. It does not override OS access controls: other users' files are readable only when the target account can read them. Edits also exposes broad ordinary read access, including outside the workspace; its configured inaccessible credential paths remain protected. Choosing Full Access deliberately removes those default credential masks.
This is Codex's Full Access analogue, not the historical sandboxed --full-auto
CLI preset. Mode changes do not overwrite the stored sandbox preference.
Configuration, interactive commands, and persisted sessions accept only these canonical values.
The prompt offers allow/deny choices for the invocation, session, or persistent
workspace scope. When one Bash or batch-Eval call needs both operation and
additive authority, one card presents the complete request. Session and
workspace approval default to remembering the complete selected profile;
command and network toggles can narrow it before approval. For each path,
p offers With this command, Independent path access, or Do not
remember. Command scope is the default for reusable commands; independent
access is an explicit choice and is stored only in :resource-grants. Commands
that cannot be remembered offer independent path access or no remembering,
with neither path authority nor command authority selected by default.
Selecting command-scoped paths enables command remembering. Turning command
remembering off clears its path and network selections, without converting them
to independent grants. Independent selections remain unchanged. The g extent
selector preserves each path's remembering scope.
The current invocation always receives the complete approved request.
.mevedel/permissions.el stores a plist containing both :rules and
:resource-grants. It lives with the project on its execution target and stores
target-native paths, so reopening the same workspace through an equivalent
TRAMP alias reuses its authority. Paths under the target's home directory are
written abbreviated as ~/... and expanded against the current target at
load, so the file can be committed and shared between machines whose home
directories differ. A target-incarnation change never rewrites this file; only
session filesystem grants, including paths embedded in command profiles, are
revoked. Command and network approvals survive; invalidated frozen filesystem
authority is cleared as well. Both global and project stores belong
to the session's execution target:
| Session | Global store | Project store |
|---|---|---|
| Local | mevedel-user-dir/permissions.el (default ~/.mevedel/permissions.el) |
PROJECT/.mevedel/permissions.el |
| TRAMP | Target user's ~/.mevedel/permissions.el |
Target's PROJECT/.mevedel/permissions.el |
Both stores contribute :rules and :resource-grants. Global and project
rules share the persistent rule bucket and its ordinary matching precedence;
explicit denies still win. Resource grants are additive, retaining their read
or write access and exact or recursive extent. Paths in either store refer to
the execution target, including ~ and filesystem requirements in command
profiles. Durable files use native paths, without client-specific TRAMP prefixes.
Local mevedel-user-dir customization does not relocate remote stores.
A remote session does not inherit the client's global file, even if its target
global file is missing or unavailable. Global authority is read at runtime,
not copied into a project or portable session. Store replacement is atomic
and descriptor-pinned; a symlink in the state path or at permissions.el
fails closed before any linked target is changed.
Global and project stores are validated when a fresh, resumed, or forked
session initializes. Invalid files contribute no authority and produce one
actionable warning per file version. Persistent permission changes refuse to
overwrite an invalid file; fix it to the current
(:rules (...) :resource-grants (...)) shape first. Missing files are valid
empty stores.
Before each permission-controlled tool, mevedel refreshes both stores once and
uses that snapshot for every rule and resource-grant lookup in the invocation.
A remote refresh waits until the transport is idle, so it cannot re-enter a
TRAMP command already in flight. Thus an external edit or revocation, including
one made by another Emacs in a different session for the same project, governs
the next tool admitted by this Emacs. An already admitted tool keeps its prior
decision. Frozen /btw conversations retain their documented creation-time
authority snapshot.
Default allowed roots are the workspace root, the system temporary directory,
configured memory roots, and manually configured additional roots. A native
filesystem write outside those roots prompts for exact write authority in
Ask and Edits. Ask also gates outside reads. Edits permits ordinary OS-readable
paths, retaining inaccessible credential protections. Full Access removes these
default resource restrictions. A session grant is stored in the durable session sidecar and
survives save/resume of that same session; an always grant is stored only in
the workspace permission file. Neither enters an unrelated session. ApplyPatch
authority covers reading the same exact path, but read authority does not cover
writes. An exact grant does not cover siblings or descendants; a grant carrying
:recursive t covers the directory and everything beneath it, present and
future. Prompts and the cockpit label every grant (exact) or (recursive),
and a remembered execution profile's row lists its child grants after the
pattern. Neither kind
adds workspace roots or authorizes Bash/Eval code. Revoking the grant restores
the underlying
workspace/protected-path restriction unless another entry still grants access.
Session-side paths use the same target-native codec as persistent authority;
resume reloads the target's global and project stores. Invocation-only authority is consumed
by the approved call and is not stored.
The permissions cockpit shows session, workspace, and global entries,
including their backing store in the details. Refresh reads current stores.
Revoking a global entry edits the target user's global file and affects every
project using it; revoking a workspace entry edits only that project's file.
Identical entries in different scopes remain separate and independently
revocable. Global entries are authored explicitly: prompt-based remembering
continues to write only session or workspace authority.
Permission entries select an enforceable scope before automatic review and before displaying approval controls. For confined Bash and batch Eval, existing directory writes and reads beneath inaccessible masks default to the requested directory's recursive scope. Files, missing paths, already-readable directory reads, native operations, live Eval, and unconfined execution retain their original scope. Defaults never select an ancestor or change read access to write access. Preparation is idempotent: a reviewer fallback card shows the same scope the reviewer judged. The reviewer also receives the original resources and which ones were prepared. Hooks still receive the unchanged original request.
The card displays the selected authority consistently for the current invocation
and remembering controls, with exact/recursive labels and target-native paths.
Original request facts remain available in the permission audit. Press g to
choose another enforceable scope, including a higher containing directory tree;
unrepresentable exact directory choices are omitted. For several resources,
first choose which resource to change. Selection alone neither creates a grant
nor runs the tool. RET approves the displayed scope for the current invocation,
s remembers session authority, and A remembers workspace authority; one-shot
interactions retain only their permitted lifetime choices. Remembering preserves
the selected command-bound or independent path scope. Approval and launch still
reject unusable grants if filesystem or confinement facts change.
Approving a tree installs its grant before checking queued siblings. Rechecks
retain the originating tool buffer and delegated rules, and a captured hook ask
continues to require its own answer.
Read-only tools also receive mevedel's installed package source directory as an allowed root, so the model can read bundled manuals named by an absolute tool-description path. Mutation-capable tools do not receive this root; an installed package outside the active workspace therefore remains outside their automatic edit boundary.
A conversation or worktree session fork copies the source session's permission mode, sandbox mode, session permission rules, and resource grants at the fork point. Subsequent authority changes are local to the source or fork unless they modify the shared workspace permission store.
An ephemeral /btw side also copies that policy, but every side request carries
an immutable one-shot mutation boundary. Analyzer-proven read-only Bash and
read-only tools remain automatic when the ordinary policy allows them. Other
mutations ask in Ask and Edits even under an inherited allow rule; Full Access
authorizes them without a prompt. When a prompt is needed it
offers only allow-once, deny-once, or feedback; defensive outcome normalization
prevents hooks or non-UI callers from creating reusable authority. ApplyPatch's
mandatory hunk review itself satisfies this boundary, so it does not show a
duplicate generic permission prompt. Explicit denies and protected-resource
checks still precede approval. Oversized side tool results are bounded and
discarded in memory rather than materializing an ephemeral session on disk.
The side publishes only explicit file/search/code/web inspection tools,
ApplyPatch, Bash, and managed-execution controls. Eval, delegation, Ask,
tasks, Goals, skills, introspection tools, deferred tools, and arbitrary
MCP tool schemas are absent.
The side freezes session, workspace-persistent, global-rule, protected-path,
sandbox, and additional-root authority at creation; later configuration changes
cannot broaden or narrow it. Mutation approval warns when the parent request is
still active because both requests affect the same workspace. The side owns a
separate permission queue and execution state, while allowlisted redacted audit
events are attributed to /btw and written through the durable parent session.
Side prompts, command text, paths, patch contents, results, and justifications
are never copied into that parent audit stream.
Files dropped into the view buffer can add exact, session-scoped Read
grants when the next sent prompt still mentions the same path. These
grants are in-memory only, do not grant the containing directory, do not
apply to write tools, and are still lower precedence than explicit deny or
ask rules and protected-path resource checks. A queued steering entry or
follow-up activates its grant before its own mention expansion, which
needs it to read the dropped file, so the grant is provisional until that
entry is delivered: an entry that fails to deliver stays pending and its
grant is given back.
Local slash commands may own deterministic workflows outside the model tool
pipeline. /worktree status and /worktree create run argv-safe Git commands
directly on the session execution target because the user explicitly typed the
command in the mevedel UI. That does not grant the model any Bash permission.
When the model uses the git-worktree skill and falls back to creating a
worktree itself, creation happens through the normal Bash tool and this
permission chain.
Prompt queues¶
The root session owns one authority boundary for its complete agent tree. Every
child uses the root session's sandbox preference, user-authoritative permission
rules, and resource grants. A child turn started from an implementation request
also captures that request's permission-mode override, including through nested
delegation; without an override it uses the live root-session mode. A later
follow-up captures its new caller's override, not an ambient Goal or an earlier
implementation turn's mode. Child model workloads remain independent of this
permission inheritance. A child may exercise that existing
authority but cannot broaden it independently. Permission prompts from the
complete tree are therefore queued on the root session, not displayed as
independent blocking overlays, and carry the requesting agent's canonical path
for attribution. Nested ToolCall asks instead show their envelope and child
tool-use identities, including for root-owned requests.
mevedel-permission-queue.el owns a heterogeneous FIFO with four entry kinds:
genericfor pipeline permission asksbashfor Bash command confirmationevalfor Eval expression confirmationsandboxfor additive network, exact filesystem, or full execution authority
Only the head is visible in the view interaction zone. The permission UI
registers that head with mevedel-view-interaction.el, which owns ordering,
callback overlays, and redraw. Rule-creating outcomes (allow-session,
deny-session, always-allow) can coalesce
queued siblings by re-running the decision chain. Resolved siblings leave the
queue before any callback runs. Execution admission and rechecks share one
pure decision in mevedel-tool-exec-permission.el: it orders operation
policy, full escalation, denied capabilities, command
resources, and missing capabilities, and names the one thing that still
stands in the way. Admission resolves that need through hooks, optional approval
review and then a human card if needed; a recheck reports unresolved asks. Rechecks
therefore cover the operation and its complete requested filesystem/network
authority, including capabilities that were already granted at admission. They retain the originating request,
agent invocation, Plan restrictions, and frozen policy context. An operation
rule alone cannot release a sibling that still lacks a capability; a directory
grant alone cannot release an uncertain command in Ask. Edits already authorizes
ordinary commands under required confinement. Every queue exit
uses the permission
queue's exactly-once settlement gate. The queue is transient
runtime state and is not written to the session sidecar; unfinished
prompts are aborted on their owning request or root-session teardown. A child
turn awaiting one of these prompts remains active and continues to occupy one
tree capacity slot.
The permission hook boundary precedes queue admission. An admitted entry has
already fired PermissionRequest; rendering the head, redrawing it, and
coalescing siblings by re-running policy never fire that hook again.
Absence timeout¶
Permission prompts wait indefinitely by default. With
mevedel-permission-queue-timeout set to a number of seconds (default nil,
Customize suggests 3600), the root session's whole unanswered human queue
expires after that long without a human answer. Root and agent cards share the
one deadline, kept in the transient permission-absence session slot.
- The first card admitted to an empty queue starts the period, whether or not the view is visible; siblings join it. Only a genuine answer from the local card or an authorized browser restarts it, before any callback runs. Automatic review, coalescing, mode changes, folding a card, typing and render failure do not. An empty queue cancels it. The clock is plain wall time; a suspend past the deadline expires the queue on wake. Cards show the stored deadline outside their folded details; nothing redraws on a timer.
- Expiry detaches the complete queue and sets the hold below before any
callback can run, retires remote controls
(
mevedel-collaboration-retire-permission-requests), then settles each entry through the exactly-once gate with(deny . REASON), settlement phaseexpiredandabsencehook provenance. The reason tells the model the user may be absent, that nothing was authorized, and not to retry just to get an answer. No rule, grant or prohibition is stored. The reason cons is the same denial shape every entry kind and origin already understands. - The hold means the session is waiting for the user. While it lasts, a new
ask that needs a human, including reviewer fallback, is denied at once with
:settlement-source absence-heldand no card, notification or deadline; Goal continuation (absencegate) and queued follow-up delivery wait; and the interaction zone shows one notice with the count and time. Reviews already running, and any approval they validly reach, are untouched. Only submitting a user message ends the hold: a composer prompt or steering,/goal OBJECTIVE,/plan ARGS, a$skill, a queued follow-up, or an authorized browser guest's message, once it is recorded or queued; a submission its hook or dispatch rejects keeps the hold. Drafts, edits to already-queued input, inspection and other slash commands,/stop, mail and reviewer activity do not. - A late local or browser answer for an expired card grants nothing and reports that the request expired or is no longer pending. Turning the option off cancels deadlines and clears holds in every live session; a changed duration applies from the next admission or answer. Neither state is persisted or cloned into forks.
- Expiry settles the waiting turns; it does not itself promise sleep. Other turns, running commands and collaboration can keep the sleep inhibitor held.
mevedel-permission-notify-function (default nil) is called once per admitted
card with its entry plist, at admission -- never for decisions the rule chain
or hooks settle without prompting, and never again on head redraw, coalesce,
or resolution. Only prompts the user must answer notify, never auto-resolved
tool calls; how many that is per session depends entirely on the session's
permission mode and rules. It fires at enqueue rather than after an
unanswered delay because the machine is certainly awake then; an
idle-escalation timer dies with a suspend, which is exactly the situation --
the user stepped away, the machine slept on an open prompt -- the hook exists
for. Errors are demoted so a broken notifier cannot break admission.
The value is always a small wrapper that formats a message from the entry;
the entry carries mevedel's card keys, not a notification API's keywords, so
pointing the variable directly at notifications-notify shows an empty
notification.
;; One formatter, reused by every wrapper below. A notification is a
;; glance surface: name the session, name the tool, and give one
;; truncated line of the command -- never the whole thing.
(defun my/mevedel-permission-summary (entry)
(let ((session (plist-get entry :session)))
(format "%s%s: %s"
(if session (concat (mevedel-session-name session) " ") "")
(or (plist-get entry :tool-name) (plist-get entry :kind))
(truncate-string-to-width
(replace-regexp-in-string
"[ \t\n]+" " "
(or (plist-get entry :command)
(plist-get entry :expression)
(plist-get entry :specifier-value)
""))
110 nil nil t))))
;; Desktop notification via Emacs's built-in DBus client
;; (notifications.el ships with Emacs and talks to the same
;; freedesktop daemon notify-send does):
(require 'notifications)
(setq mevedel-permission-notify-function
(lambda (entry)
(notifications-notify
:title "mevedel needs permission"
:urgency 'critical
:timeout 0
:body (my/mevedel-permission-summary entry))))
;; The same through the notify-send binary; destination 0 makes
;; call-process fire-and-forget:
(setq mevedel-permission-notify-function
(lambda (entry)
(call-process "notify-send" nil 0 nil
"--app-name=mevedel" "--urgency=critical"
"--expire-time=0"
"mevedel needs permission"
(my/mevedel-permission-summary entry))))
;; Phone push through ntfy, for the prompt that opens after you have
;; left the desk:
(setq mevedel-permission-notify-function
(lambda (entry)
(call-process "curl" nil 0 nil "-s"
"-d" (my/mevedel-permission-summary entry)
"https://ntfy.sh/YOUR-TOPIC")))
The desktop wrappers request a non-expiring critical notification. The notification server may ignore the timeout hint.
mevedel-permission-prompt.el is the focused UI owner for all four entry
kinds. It owns generic permission controls, agent attribution, dangerous-command
presentation, and Eval presentation. The queue passes
the entry, attribution, pending count, and settlement callback through
mevedel-permission-prompt-render; the UI owner selects its presentation.
Rendering an Eval card does not load the execution-policy adapter. The queue
retains ordering and outcome semantics; the shared interaction primitive retains
overlay settlement and request cancellation.
Cards start folded. The folded card shows the request kind, agent attribution,
the model's optional reason, an operation preview, the detected-command summary,
the complete Scope: an approval uses, the patterns a session/always allow
would add, and every warning. Live Eval's unrestricted in-Emacs execution, full
escalation's disabled confinement, protected paths, dangerous commands and
unsupported shell syntax always stay visible. Once remembering choices differ
from their defaults, the folded card also states what session or workspace
approval would remember. TAB shows the details: the full command or
expression, admission mode and approval cause, why automatic review deferred,
the per-capability authority breakdown, remembering controls and the scope
picker hint. Folding changes presentation only. Every key, including g, c,
n and p, works in either state.
The preview elides a Bash command longer than
mevedel-permission-command-display-limit (400 characters) and an Eval
expression longer than mevedel-eval-expression-display-limit (8 lines).
The mevedel--remote descriptor a browser collaborator reads is built from the
complete card, independent of local folding -- the native expansion binding
does not control the browser, which must not approve what it cannot see. It includes the admission mode, approval
cause, warnings and selected resource scope, including directory selections on
one-shot cards without remembering controls. A side-conversation mutation also
carries the warning that its parent request is still active; the browser and
Emacs disclose the same concurrency risk before approval.
mevedel-bash-policy.el supplies Bash classification and reusable rule patterns. mevedel-tool-exec-permission.el combines that policy
with the generic permission chain, persists approved authority, and adapts
Bash and Eval decisions to the permission queue. Execution and rendering stay
in mevedel-tool-exec.el.
Permission diagnostics are persisted to permission-log.el in the session
directory when mevedel-permission-log-enabled is non-nil. The log is
diagnostic only: resume never replays it into live permission state.
Entries recorded before first materialization are buffered transiently and
flushed when the session directory is created.
Each tool invocation that reaches permission checking records a sanitized
permission-decision event with fields such as tool name, origin, mode,
outcome, specifier, protected-path flag, resolver path, and rule bucket. It
does not include raw ApplyPatch content, arbitrary tool args, or extra raw
Bash/Eval payloads. Prompt lifecycle events remain separate: queue
enqueue/resolve/abort/coalesce events describe prompt handling without raw
Bash commands or Eval expressions.
Mode fields describe the invocation's effective request policy, including an implementation override, rather than assuming the stored session baseline. Permission presentation and reminders use the same effective-mode path without promoting the override to a session default.
Each queued card gets a :permission-id at admission, separate from the owning
:request-id and nested tool-call identifiers. Its captured
:permission-mode-base and :permission-mode-effective remain unchanged when
the user changes modes while the card is pending. Eval's live/batch choice is
recorded separately as :eval-mode. The visible card shows the admission mode
and the cause supplied by the policy owner: mode-based operation judgment,
resource boundary, protected resource, explicit ask rule, hook, missing
additive filesystem/network authority, or execution without confinement.
Count permission-displayed to measure actual initial card displays. Admission
is permission-enqueued; refreshing a card's scope or queue count does not emit
another display. Automatic review records permission-review-completed with
outcome, source, elapsed seconds and the settled reviewer's bounded explanation
(:review-reason), without adding raw reviewer responses or supplied user
evidence. The explanation stays in permission-log.el, not unified telemetry
or forwarded side audits, and does not confer authority. It does not count
as a displayed card or a human answer. Each admitted request settles once as
permission-resolved for a user answer, permission-coalesced for a covering
policy decision, permission-swept for owner-request teardown,
permission-expired for an expired queue or an ask denied during the hold
(:settlement-source expired or absence-held), or
permission-aborted for cancellation, queue abort, or rendering failure.
:settlement-source distinguishes these paths. A crash may leave an admission
without a terminal record; that is an unrecorded outcome, not an approval.
The permission log retains :resource-originals and :selected-resources,
including path, read/write access, and :recursive extent (nil means exact),
plus :approval-lifetime for invocation, session, or workspace approval.
Local permission-log entries append in UTF-8 without rereading the log or
checking an already-existing parent directory. Remote entries remain queued
until session settlement publishes their delta.
Unified telemetry retains the categorical identity, mode, cause, outcome, and
lifetime fields, while its allowlist drops resource paths and capability
payloads. Forwarded side-conversation events use the same categorical fields;
feedback is recorded as a denial outcome without copying the user's text.
Bash specifics¶
Bash analysis returns a normalized command class, structured argument vectors,
parser source, literal resources, and human-readable reasons. It uses a
normally configured Bash Tree-sitter grammar when available and a conservative
scanner otherwise. Redirections, substitutions, expansions, assignments,
subshells, here-documents, control flow, parse errors, and unsupported operators
are complex. A dangerous component takes precedence in a compound request.
Here-document payload is data for the separate native-backgrounding check:
literal & and coproc text do not reject a call. Background operators outside
the payload and in substitutions of expanding (unquoted-delimiter) bodies still
reject it; quoted delimiters disable those substitutions.
Delimiter recognition joins escaped newlines only in expanding bodies;
quoted payload retains its original physical lines.
Delimiter words using ANSI-C or locale quoting remain conservatively scanned
as raw source rather than guessing their expanded terminators.
Read-only classification uses argument-aware built-in policies. Reading,
searching, listing, comparing and path inspection need no broad default Bash
allow patterns. Besides the existing readers, supported policies include
diff, cmp, readlink, realpath, du, and literal file/string/integer
test/[ forms. Unsupported options, variable lookup and recursive arithmetic
expressions remain unknown. Find, ripgrep, base64, sed and awk reject deletion,
helper execution, output-file options and unrecognized programs.
Uniq accepts ordinary inspection options before its input and an optional -
stdout operand, but rejects output filenames and options after the input.
Git inspection covers status, diff, log, show, rev-parse, ls-files,
ls-tree, bounded cat-file and merge-base, for-each-ref, and explicit
branch listing. Recognized flags, literal -C paths and -- path operands
are supported; mutations, arbitrary config overrides and helper-enabling
options are not. Classification requires hardened launch: pager, filesystem
monitor, external diff/text conversion, signature verification, tracing,
optional index writes and implicit object fetching are disabled. The target
launcher clears every inherited GIT_* environment entry before installing
fixed inspection controls; environment enumeration failure refuses launch.
This relies on trusted Git/env executables, PATH and dynamic loader, not on the
classifier replacing ordinary child confinement. Worktree
status/diff use a private temporary metadata view with copied HEAD/index
and flattened effective repository/user/system config. Main and shared index
copies retain their original timestamps so Git still detects same-size edits
whose timestamps match cached file metadata ("racily clean" entries), without
writing source indexes. Includes and filter
commands are removed from the launch config; active configured clean/process
filters and gitlinks cause explicit refusal. Final submodule traversal is
disabled, so concurrent source config, attributes or index edits cannot enable
those helpers after preflight. Objects, refs, worktree files and attributes
remain live: output is not an atomic repository snapshot. The view uses the
execution target's authorized temporary directory and is removed on ordinary
exit and caught signals; an uncatchable kill may leave a private temporary view.
Ordinary config remains available except for these safety overrides.
Git inspection does not need .git write grants.
Supported pipes and &&, ||, ; chains are read-only when every component
is read-only. Read-only launch uses Bash without startup files or inherited
shell functions/options, and prepares Git/ripgrep spans from parsed arguments while
preserving shell connectors, quoting and short-circuit behavior. Original
authored commands remain the permission/hook/audit facts; prepared launch
text is not new user authority. Parser/preparation disagreement refuses
launch. Unknown commands and unsupported syntax remain conservative.
Tree-sitter extraction must retain every scanner segment before classification.
Standalone assignments such as IFS=,; false | cat remain complex, preserving
the assignment and using ordinary authorized launch instead of entering
read-only preparation with a silently shortened command list.
Ripgrep receives --no-config, so inherited RIPGREP_CONFIG_PATH cannot inject
helper-enabling options absent from the analyzed command.
Bash keeps its specialized permission entry and controls, but an ask passes
through the pipeline's shared PermissionRequest boundary before that entry
is admitted.
Edits authorizes arbitrary Bash syntax within required confinement; unknown
commands, pipes, redirects and substitutions alone do not prompt. Full Access
runs without confinement or prompts. Ask uses conservative classification: unknown
commands default to ask. Explicit hard denies still apply in all modes. Direct user-authored session, persistent, and defcustom
patterns may authorize dangerous or complex forms. Invocation- and
request-scoped delegated patterns may not. Explicit denies always win.
When a prompted dangerous command is literal and contains no dynamic shell or
glob ambiguity, session/workspace remembering stores the complete command
exactly. Executable paths retain this restriction: /bin/rm cache is not
generalized to /bin/rm cache:*. Shells and wrappers likewise remain exact,
whether named directly or by path. Shell quoting cannot hide the executable's
name from these checks. Executable names containing whitespace or shell syntax
remain exactly as authored, rather than becoming a prefix for a different
executable after quote removal. Prefix generation also requires literal source
spelling, including skipped environment assignments: 'LANG=C' names an
executable and must not be discarded as an assignment. Ambiguous commands remain
invocation-only; users may still author broader rules directly.
Child confinement¶
Bash, batch Eval, and native external tool helpers share the guarded child
launcher and use one effective policy: required in Edits, off in Full Access,
and the session's configured mevedel-sandbox-mode in Ask. On Linux, the backend resolves
bwrap with executable-find and caches a real probe of the core mount, user,
process, and network namespaces. Each probe attempt defaults to a 500 ms bound
and retains at most 64 KiB of combined diagnostics. The probe waits for its own
process only, running no timers or other process output that would charge
their time to the bound. A probe that times out or cannot start says nothing
about the backend, so it is cached as retryable and the next launch probes
again. If the full probe fails,
Mevedel retries without replacing /proc. A successful retry retains the
mount, user, process, and network boundary while exposing the host /proc
view; pending and execution facts record proc: host instead of treating the
whole backend as unavailable. A full successful profile mounts / read-only,
rebinds the workspace, temporary directory, memory roots, manually configured
roots, and session working directory writable, installs a fresh /proc, and
changes to the canonical working directory. Its private /dev supplies
/dev/null without host authority; redundant additive grants for that device
are ignored rather than remounted. The default profile also isolates the
network. A justified additive network request requires authority in Ask and
Edits and changes only network isolation for that invocation. Full Access already
has unrestricted network and filesystem access. The namespace and mount boundary
is inherited by descendants.
Native tools pass already-authorized input and search paths to the launcher as
read-only mounts and pass only their generated-artifact directories as writable
roots. Each invocation also gets a private writable scratch directory, which is
removed on settlement. This currently covers diff for previews, rg for
Read directory listings, Glob, and Grep, and pdfinfo, pdftoppm, and
ImageMagick for Read media handling. These helper profiles are chosen by native
code; they do not add model-facing escalation arguments or another permission
prompt. Live Emacs operations, long-lived language servers, hooks, and
user-triggered Git, clipboard, and UI helpers are not external tool helpers.
An exact filesystem grant may name a symlink. The launcher opens the granted
path but mounts its canonical target, leaving the requested command and symlink
chain unchanged while avoiding Bubblewrap mounts onto symlink entries. Before
Bubblewrap starts, the target-side launcher compares the device/inode identity
of each already-open descriptor with the identity captured during grant
resolution. A failed open, missing descriptor stat support, or replacement
mismatch refuses the child; best-effort does not retry that authority failure
as an unrestricted launch. Beneath a masked parent it recreates only the link
hops and empty traversal directories needed to reach that exact target.
An inaccessible file is masked by a private empty file with mode 000, then mounted read-only. Dedicated launcher descriptors supply the empty contents; the child's stdin remains available. No chmod is applied to a host inode or an already read-only bind. Multiple masks and exact file additions share the existing descriptor-backed launcher with distinct descriptor numbers.
Bubblewrap cannot add authority for only a directory inode: binding a directory
also exposes its descendants. Exact directory write grants, and exact directory
read grants beneath inaccessible masks, therefore refuse confined preparation with a
message identifying the directory and the prompt's recursive-scope selection.
An exact directory read already available through the baseline filesystem adds
no mount. An explicitly approved recursive grant subsumes redundant exact
mounts only when it supplies the same enforceable authority, without merging
their separate identities in the authority store. Execution-specific coverage
does not cross a separately protected descendant: a recursive checkout grant
does not authorize .git writes. The missing child request reaches the human
card, whose displayed recursive directory scope requires explicit approval.
Sufficient approved child-tree authority is reused without another prompt;
meaningful exact child-file grants are retained during mount normalization.
Capability checks, queued rechecks, approval validation and mount normalization
share this rule. Native resource-grant coverage remains unchanged.
A justified additive filesystem request names exact absolute paths and marks
each as read or write. Ungranted paths require approval in Ask and Edits;
Full Access bypasses ordinary resource asks. Invocation approval applies only
to the current child. Reusable approval
stores each selected path either in the matching operation profile or as an
independent resource grant shared with native filesystem tools. It never
creates both entries implicitly. Prepared directory defaults and the card's g
selection can broaden a requested resource to a displayed directory tree. Both the current
child and a selected remembered profile receive that explicit extent. Narrowing
the remembering toggles does not narrow the current invocation's approval.
The default remembered selection includes every requested capability, including
ones already granted before the card appeared. The network and path toggles
therefore cover the complete request, rather than only its missing additions.
Recursive profile entries carry :recursive t, exposing the directory tree
through one bind mount at the selected access level; model-facing requests
name exact paths only. A
later matching default call
reattaches paths from its matching direct profiles without a second grant.
Revoking a profile removes that command-scoped authority; revoking an independent
grant removes only the independent authority. Other sufficient approvals remain
effective. Existing profiles also use these semantics: deleting a former
companion resource grant no longer revokes a profile. No migration or automatic
cleanup of existing independent grants is performed. Approved paths
are rebound at only the requested access level. A grant that contains a
protected descendant is bound before that descendant is masked; all other
grants are bound after protected masks are installed. An explicit write grant
supersedes the read-only restriction at that exact mount, including when the
mount precedes protected children. It does not remove those children's masks.
Inaccessible parents
expose traversal only far enough to reach the named mount, so their contents
and sibling resources remain hidden. Command or Eval authorization is resolved
independently and is never supplied by the resource grant. Explicit path denies
remain final. Network and filesystem additions may be combined without
changing any unrequested confinement boundary.
Protected linked-worktree .git pointers resolve both the worktree metadata
directory and its commondir target. Both retain the applicable protection.
Failed confined results identify the discovered shared metadata location for
objects and refs; this is repository-layout information, not a diagnosis
inferred from stderr or an automatic grant. A new explicit invocation can
request the missing scope.
Before Bash or batch Eval starts, one shared resolver merges capabilities
explicitly requested by that invocation with every matching direct
session/workspace/global profile. A non-empty effective profile promotes
use_default to additive confinement; require_escalated is never changed.
This resolution happens both during permission checking and immediately before
child launch. It never applies to live Eval. A capability not present in a
matching approved profile is not inferred from failures or earlier commands:
the model must make a new explicit invocation with the missing capability and
justification. A started process is never replayed.
In Ask, before Bash executes, identified literal resources are resolved against
the working directory. Resources outside the allowed roots require a covering
additive grant — exact, or recursive over an ancestor directory; bare . and
.. operands participate in this check. Command
authorization is resolved first so an explicit command deny retains precedence.
Provider-shaped empty optional fields, such as disabled network plus empty
filesystem lists, are treated as omitted rather than as an authority request.
Non-empty additions still require with_additional_permissions and a
justification.
require_escalated is a separate complete bypass for Bash and batch Eval. It
requires a justification and cannot be combined with additive permissions. It
prompts in Ask and Edits unless a matching
direct user-authored :sandbox-permissions require-escalated rule already
exists or the optional reviewer approves that invocation. Full Access already
authorizes direct execution. The prompt and diagnostics identify that filesystem,
network, and process confinement will all be disabled. Operation denials,
including Bash mutations prohibited by Plan, still apply before escalation.
In Ask and Edits, explicit operation ask rules still need approval even when a
remembered rule already authorizes escalation. Once approved, the child runs directly as the
user and reports sandbox: escalated. Delegated rules
cannot grant this authority, and non-interactive trusted skill expansion cannot
request it or create reusable escalation rules. An ordinary sub-agent may still
place the same user-visible request in the shared permission queue.
Literal stable Bash commands, including dangerous commands, and literal batch Eval expressions may be remembered exactly. Dynamic, glob-bearing, or otherwise ambiguous Bash remains invocation-only; experts may still author an exact, scoped, or deliberately broad qualified rule directly. Reusable deny remains available because it can only reduce authority.
Remembered Eval rules use :expression, including ordinary permission,
additive profiles, and full escalation. They match the complete expression text
with no wildcard expansion or whitespace normalization. Approving
(message "*") therefore does not authorize (message "different").
Deliberately authored :pattern rules retain glob semantics; the remembered
authority cockpit labels literal expression rules explicitly.
best-effort in Ask executes directly when the initial probe is unavailable.
Edits always requires confinement, regardless of the stored preference. Full
Access deliberately selects direct execution. Once
confined preparation begins, a failure is returned without an unrestricted
replacement. A private marker emitted immediately before exec distinguishes
launch refusal from a started command; grant refusals do not emit that marker.
Detection and removal match complete, case-sensitive marker lines without
splitting the command's entire output. Lookalikes embedded in ordinary output
remain intact.
Signals, timeouts, and ordinary command failures are returned once and never
replayed. required refuses an unavailable backend, while off selects direct
execution deliberately. Direct execution reports filesystem: unrestricted
and network: unrestricted; refused attempts report unavailable boundaries.
A pre-marker failure retains its launcher error, diagnostics, or exit code.
The next independent invocation reprobes the backend, so a transient launch
failure does not disable confinement permanently. Bash and batch-Eval results append their active
sandbox facts for the model and audit trail. Native helper facts remain
internal and in tests rather than being added to successful tool content.
Trusted skill substitutions keep those facts out of the substituted literal.
The first direct fallback in each live session emits one user-visible warning
and adds one model-visible note to the affected tool result. Later fallbacks do
not repeat the warning, but completed Bash and batch-Eval results retain their
actual invocation facts for the transcript and audit trail. Additive filesystem
facts include read and write grant counts. Mevedel does not add a persistent
sandbox status-line item.
Protected restrictions are layered after writable roots. Existing glob matches
and canonical targets become concrete mounts; .git pointer files also protect
their Git directory target. Read-only paths remain visible but immutable, while
inaccessible directories are replaced by empty read-only mounts. Determinable
missing directory roots receive identity-checked temporary mount targets that
are removed after settlement through their captured physical parents. Cleanup
does not change their modes, retains nonempty targets, and refuses a substituted
parent instead of following it. A protected path crossing a symlink that the
child could rewrite fails closed instead of relying on a racy canonical-path
snapshot.
Optional approval review¶
mevedel-permission-reviewer is user by default. Set it to auto to review
permission exceptions before displaying a card. The guardian workload may
approve the complete invocation once, deny with a reason, or defer. It sees
actual root user intent, exact arguments, requested capabilities, current target
and confinement facts, the prepared scope, and direct saved rules related to the
operation. Those saved-rule candidates are evidence, not grants. It cannot persist
grants or change mode. Failure, timeout and uncertainty fall back to the ordinary
human card, which explains why review deferred. Full Access bypasses review
entirely.
Review cancellation and evidence revalidation prevent late responses, mode changes, target replacement or revoked authority from reusing an obsolete approval. Human queues also recheck policy on mode changes. Already-running processes retain their admitted authority; queued children refuse launch if the captured permission mode has changed.
Accepted automatic allow-once decisions retain display-only reviewer provenance
after final policy, ownership, evidence and cancellation checks. The affected
tool row can show Auto-approved · once, with the bounded reason available
to its presentation. This records the approval, not successful execution: a later
failure retains it. Human approvals and human fallback, saved rules, hook allows,
ordinary mode policy and Full Access do not manufacture reviewer provenance.
The record grants no authority and does not affect future calls or polling.
Approval reviewer owns the trusted prompt, bounded evidence and JSON response contract. Historical approval counts do not measure the new reviewer's safety or achieved reduction in prompts.
Introspection source reads¶
EmacsDescribe decides its own permission from its arguments, before allow
rules. A value read always asks, even under a saved allow rule for the tool,
because variables can hold private state. A library source read bypasses
ordinary path arguments only for a simple library name whose resolved canonical
file remains below a canonical local load-path entry. Absolute names,
directory components, remote entries, and library symlinks that escape their
load-path entry are denied before the reader runs. Use the ordinary Read tool
when source outside that closed set is needed; its path goes through the normal
resource permission boundary. A library location reads no file: it checks only
simple names in local load-path directories, never remote entries. It and other
introspection reads follow ordinary policy.
Eval¶
Eval asks through the same session permission queue's Eval-specific
entry type when approval is required. Edits automatically permits confined
batch Eval, while live Eval remains an exception. Full Access permits both. Like Bash,
an ask fires PermissionRequest before queue admission without changing the
specialized card. The expression shown in the prompt is subject to
mevedel-eval-expression-display-limit. The prompt also shows the
requested execution mode and, for live Eval, whether UI preservation is
enabled. An optional justification on any Eval call appears on the card and
in reviewer evidence as the model's quoted reason. It never authorizes anything
and remains mandatory only for non-default batch permission requests.
The full-auto reminder explicitly states that live Eval runs inside Emacs without confinement, alongside the tool and permission-card disclosures.
Eval supports two execution modes. live is the default and evaluates
inside the current Emacs process so the expression can inspect live
buffers, variables, windows, timers, advice, and package state. Live
Eval restores the selected frame's window configuration by default;
callers can pass preserve_ui: false only when intentional UI
manipulation is desired. batch starts a child emacs --batch -Q
process with the current load-path and the session working directory.
Batch Eval protects the interactive Emacs session from UI/global-state
mutation and uses the same effective child confinement as Bash. Edits refuses
unavailable confinement; Ask may use its configured fallback, and Full Access
runs directly as the same OS user with unrestricted filesystem/network disclosure.
Skill body elisp injections (!el inline and ```!el fenced blocks)
are the exception: they pass a trusted-literal flag because the
expression is author-written SKILL.md content, not model-generated Eval
input. A matching Eval allow authorizes model-generated or trusted Eval, while
a matching ask requires approval in Ask and Edits; Full Access bypasses it. Deny
rules still win absolutely.
Trusted skill expansion cannot create an interactive prompt and therefore
requires existing authority, typically from the skill's allowed-tools:
[Eval].
Standalone/sticky Plan mode withholds Eval regardless of this authority;
directive Planning also withholds it. Goal turns use ordinary Eval policy. Markers
introduced by argument
substitution are not trusted literals and are left as text.
Literal markers may still contain substituted text in their expression
body; only the marker syntax and delimiters carry the trusted-literal
provenance.
Sub-agent permission propagation¶
Sub-agent buffers carry mevedel--session set buffer-locally to the
root session struct, by reference (allocated in
mevedel-agent-conversation-open). The pipeline reads
mevedel--session from the current buffer at tool-dispatch entry, so a
tool dispatched at any nesting depth observes the root's permission mode,
direct rules, explicit denies, protected resources, resource grants, and
confinement policy. Any
"allow-session" / "deny-session" outcome accepted inside the sub-agent's
prompt is written via setf on the same struct -- so the new rule
applies immediately to the root and to every other live sub-agent.
An Agent working_directory override changes the base for relative operands,
not this shared authority. The pipeline captures its conversation directory for
file tools and execution; Bash resource analysis uses that captured directory
even when the root session has a different cwd. Path rules match the resolved
absolute target with their existing glob semantics; a directory selection does
not rewrite or re-anchor shared rules. Sandbox writable roots are still derived
from session policy, never enlarged merely because an external Git worktree was
selected. Native operations outside allowed roots need ordinary resource
authority. Confined execution additionally retains the requirement that cwd
itself be in a configured writable root; additive grants alone do not satisfy
that cwd boundary. Ordinary authorization for unconfined execution is unchanged.
Plan restrictions are the exception to live sharing. An agent turn started
under Plan carries an immutable Plan read-only stamp, so leaving Plan or
accepting a proposal does not widen a turn that is already running. See
Plan mode.
An agent's tool list limits which operations it can request. A read-only agent can still need a permission prompt for a protected or outside-root resource; read-only capability does not confer unrestricted read authority.
All queued permission prompts render in the root session's interactive
view buffer, not inside the sub-agent transcript buffer or a read-only
transcript inspection view. Queue entries carry a canonical origin (/root
or a retained agent path such as /root/worker/verifier), and request teardown
only aborts entries owned
by the ending request. This keeps a retained agent's visible prompt
open across root-view rerenders and unrelated request cleanup until the
user explicitly resolves it or interrupts that agent turn. Redraws use the
ordinary interaction-zone lifecycle and preserve the active composer draft.
If the permission step ever runs without a session in context,
mevedel-tool-permission-step emits a display-warning once per tool
("Permission step for ... ran with no session in context"); that
fallback would silently consult only the defcustom-scoped global
defaults, which is the actual hazard. Repeats are logged quietly to
*Messages* without disturbing the echo area.
mevedel-tool-permission.el owns permission-step path fan-out, decision
logging, permission hook dispatch, and prompt orchestration. The Pipeline only
places that step in the standard tool sequence.
Bash permission example¶
(setq mevedel-permission-rules
'(("Bash" :action ask) ; default ask
("Bash" :pattern "echo" :action allow)
("Bash" :pattern "echo *" :action allow)
("Bash" :pattern "ls" :action allow)
("Bash" :pattern "ls *" :action allow)
("Bash" :pattern "git log*" :action allow)
("Bash" :pattern "rm *" :action deny)))
(setq mevedel-bash-dangerous-commands
'("rm" "sudo" "dd" "chmod" "curl" "wget" "ssh"))
Use space-boundary patterns ("ls" + "ls *") rather than "ls*" to
avoid matching lsof. Supported plain syntax is commands joined by
&&, ||, ;, or |. Dynamic shell forms are complex and require either
an interactive decision, full-auto, or a deliberately authored direct-user
pattern.
A portable project-local store that authorizes the Eask CLI, network access, and writes within npm's cache directory is:
(:rules
(("Bash" :pattern "npx @emacs-eask/cli *"
:network t
:file-system ((:path "~/.npm" :access write :recursive t))
:action allow))
:resource-grants nil)
A :recursive t entry grants a whole directory tree — here read access to
the system Emacs installation for one Bash command. Its profile exposes the
tree to the sandboxed child through one read-only bind mount:
(:rules
(("Bash" :pattern "some-command *"
:file-system ((:path "/usr/share/emacs" :access read :recursive t))
:action allow))
:resource-grants nil)