Packages
fnord
0.8.49
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/approvals/shell.ex
defmodule Services.Approvals.Shell do
# ----------------------------------------------------------------------------
# Globals
# ----------------------------------------------------------------------------
@type state :: Services.Approvals.Workflow.state()
@type decision :: Services.Approvals.Workflow.decision()
@type args :: {String.t(), list(map), String.t()}
@approve "Approve"
@persistent "Approve persistently"
@deny "Deny"
@deny_feedback "Deny with feedback"
@session "Approve for this session"
@project "Approve for the project"
@global "Approve globally"
@no_feedback "The user denied the request."
@no_tty """
The application is not running in an interactive terminal.
The user cannot respond to prompts, so they were unable to approve or deny the request.
"""
# ----------------------------------------------------------------------------
# Behaviour implementation
# ----------------------------------------------------------------------------
@behaviour Services.Approvals.Workflow
@impl Services.Approvals.Workflow
@spec confirm(state, args) :: decision
def confirm(state, {op, commands, purpose}) when is_list(commands) do
with :ok <- validate_commands(commands) do
# Build list of {prefix, full_command} pairs
stages =
Enum.map(commands, fn cmd ->
{
extract_prefix(cmd),
format_stage_for_match(cmd)
}
end)
if Enum.all?(stages, &approved?(state, &1)) do
{:approved, state}
else
if !UI.is_tty?() do
UI.error("Shell", @no_tty)
{:denied, @no_tty, state}
else
UI.interact(fn ->
render_pipeline(state, op, commands, purpose)
prompt(state, stages)
end)
end
end
else
{:error, reason} -> {:denied, reason, state}
end
end
# Input validation
# ----------------------------------------------------------------------------
defp validate_commands([]), do: :ok
defp validate_commands([cmd | rest]) do
with :ok <- validate_command(cmd),
:ok <- validate_commands(rest) do
:ok
end
end
defp validate_command(%{"command" => cmd, "args" => args}) do
cond do
!is_binary(cmd) -> {:error, "command must be a string"}
!is_list(args) -> {:error, "args must be a list"}
!Enum.all?(args, &is_binary/1) -> {:error, "all args must be strings"}
true -> :ok
end
end
defp validate_command(_), do: {:error, "expected object with keys 'command' and 'args'"}
# ----------------------------------------------------------------------------
# Approval checks
# ----------------------------------------------------------------------------
# Pre-approved regex for read-only sed commands commonly used by LLMs
# Supports: -n (quiet), -E (extended regex), basic s/// substitutions, p (print), d (delete without -i)
# Blocks: -i (in-place), -f (script files), -e (expressions), s///e (execute), w/W/r file operations
# no in-place editing
# no script files
# no -e expressions (conservative)
# no s///e or s|||e execute in substitution
# no w/W/r commands with filenames
# no addressed w/W/r commands
# no range w/W/r commands
@sed_readonly_pattern "^sed" <>
"(?!.*\\s-i\\b)" <>
"(?!.*\\s-f\\b)" <>
"(?!.*\\s-e\\b)" <>
"(?!.*s[|/].*[|/].*[|/][gp0-9]*e)" <>
"(?!.*\\b[wWr]\\s+\\S)" <>
"(?!.*\\d+[wWr]\\b)" <>
"(?!.*,[wWr]\\b)" <>
".+$"
@full_cmd [
"^find(?!.*-exec)",
@sed_readonly_pattern
]
@ro_cmd [
"ag",
"cat",
"col",
"cut",
"diff",
"echo",
"fgrep",
"grep",
"head",
"jq",
"ls",
"nl",
"pwd",
"rg",
"sort",
"tac",
"tail",
"tr",
"tree",
"uniq",
"wc",
# git subcommands
"git blame",
"git branch",
"git describe",
"git diff",
"git grep",
"git log",
"git ls-files",
"git merge-base",
"git rev-list",
"git rev-parse",
"git show",
"git status"
]
@rw_cmd [
"mkdir",
"touch",
"cp",
"mv",
"rm",
"sed",
"awk",
"patch",
"truncate"
]
def preapproved_cmds do
if edit?() and auto?() do
@ro_cmd ++ @rw_cmd
else
@ro_cmd
end
end
# Returns true if either the prefix path or the full command string is
# approved or a stored full-command approval matches this stage.
defp approved?(state, {prefix, full}) do
prefix_approved?(state, prefix) or
full_cmd_preapproved?(state, full)
end
defp session_approvals(%{session: session}, kind) do
session
|> Enum.reduce([], fn
{^kind, prefix}, acc -> [prefix | acc]
_, acc -> acc
end)
end
def prefix_approved?(state, prefix) do
cond do
prefix in preapproved_cmds() -> true
prefix in session_approvals(state, :prefix) -> true
Settings.Approvals.prefix_approved?(Settings.new(), "shell", prefix) -> true
true -> false
end
end
def full_cmd_preapproved?(state, full) do
Settings.Approvals.approved?(Settings.new(), "shell_full", full) or
@full_cmd
|> Enum.concat(session_approvals(state, :full))
|> Enum.map(fn re -> Regex.compile!(re, "u") end)
|> Enum.any?(&Regex.match?(&1, full))
end
# ----------------------------------------------------------------------------
# Display
# ----------------------------------------------------------------------------
defp render_pipeline(state, op, commands, purpose) do
stages =
commands
|> Enum.with_index(1)
|> Enum.flat_map(fn {cmd, i} ->
tags =
if approved?(state, {extract_prefix(cmd), format_stage_for_match(cmd)}) do
[:green, :bright]
else
[:red, :bright]
end
formatted = format_stage(cmd)
item = "#{i}. #{formatted}"
[Owl.Data.tag(item, tags), "\n\n"]
end)
op_msg =
case op do
"|" -> "Pipeline of Commands"
"&&" -> "Chained Commands"
end
[
Owl.Data.tag("# Approval Scope ", [:red_background, :black, :bright]),
"\n\n#{op_msg}:\n\n"
]
|> Enum.concat(stages)
|> Enum.concat([
Owl.Data.tag("# Purpose ", [:red_background, :black, :bright]),
"\n\n",
purpose
])
|> UI.box(
title: " Shell ",
min_width: 80,
padding: 1,
horizontal_align: :left,
border_tag: [:red, :bright]
)
end
defp format_stage(%{"command" => cmd, "args" => args}) do
Enum.join([cmd | args], " ")
end
# Build the string used for shell_full matching: use basename(command) + args
defp format_stage_for_match(%{"command" => cmd, "args" => args}) do
base = Path.basename(cmd)
Enum.join([base | args], " ")
end
# ----------------------------------------------------------------------------
# Prompt + persistence
# ----------------------------------------------------------------------------
defp prompt(state, stages) do
opts = [@approve, @persistent, @deny, @deny_feedback]
Settings.get_auto_policy()
|> case do
{:approve, ms} -> UI.choose("Approve this request?", opts, ms, @approve)
{:deny, ms} -> UI.choose("Approve this request?", opts, ms, @deny)
_ -> UI.choose("Approve this request?", opts)
end
|> case do
@approve ->
{:approved, state}
@deny ->
{:denied, build_auto_deny_message(), state}
@deny_feedback ->
{:denied, get_feedback(), state}
@persistent ->
customize(state, stages)
end
end
@spec customize(state, [String.t()] | list({String.t(), String.t()})) :: {:approved, state}
def customize(state, stages) do
stages
|> Enum.uniq()
# Filter out already approved stages
|> Enum.reject(fn {prefix, full} -> approved?(state, {prefix, full}) end)
# Extract just the prefixes
|> Enum.map(fn {prefix, _full} -> prefix end)
# Iterate over each unapproved prefix and ask for scope or regex
|> Enum.reduce({:approved, state}, fn prefix, {:approved, acc_state} ->
choose_scope(acc_state, prefix)
end)
end
defp choose_scope(state, prefix) do
{kind, pattern} = custom_pattern(state, prefix)
scope =
UI.choose(
"""
Choose approval scope for:
#{pattern}
""",
[@session, @project, @global]
)
case kind do
:prefix -> approve_scope(scope, state, pattern)
:full -> approve_regex_scope(scope, state, pattern, prefix)
end
end
defp custom_pattern(state, prefix) do
UI.prompt(
"""
You may optionally select your own approval prefix, making it broader or more specific.
Wrap your input in slashes (/) to treat it as a regular expression.
Regular expressions are matched against the entire command string, including arguments.
There is NO implicit anchoring, so include ^ and $ as needed.
Options:
1. Customize the prefix (e.g. `docker image` to `docker image ls` or `docker` to change the specificity)
2. Enter a regular expression (e.g. `/^find(?!.*-exec).+$/` to allow find without -exec)
3. *Leave blank* to approve the default prefix as shown.
CURRENT PREFIX: #{prefix}
""",
optional: true
)
|> case do
{:error, :no_tty} ->
{:prefix, prefix}
nil ->
{:prefix, prefix}
"" ->
{:prefix, prefix}
str ->
str = String.trim(str)
if String.starts_with?(str, "/") and String.ends_with?(str, "/") do
re = String.slice(str, 1..-2//1)
if re == "" do
UI.error("Empty regex is not allowed")
custom_pattern(state, prefix)
else
case Regex.compile(re, "u") do
{:ok, _} ->
{:full, re}
{:error, {msg, _pos}} ->
UI.error("Invalid regex: #{to_string(msg)}")
custom_pattern(state, prefix)
end
end
else
{:prefix, str}
end
end
end
defp approve_scope(@session, %{session: session} = state, prefix) do
prefixes = session |> Enum.concat([{:prefix, prefix}]) |> Enum.uniq()
{:approved, %{state | session: prefixes}}
end
defp approve_scope(@project, state, prefix) do
Settings.new() |> Settings.Approvals.approve(:project, "shell", prefix)
{:approved, state}
end
defp approve_scope(@global, state, prefix) do
Settings.new() |> Settings.Approvals.approve(:global, "shell", prefix)
{:approved, state}
end
defp approve_regex_scope(@session, %{session: session} = state, inner, _prefix) do
prefixes = session |> Enum.concat([{:full, inner}]) |> Enum.uniq()
{:approved, %{state | session: prefixes}}
end
defp approve_regex_scope(@project, state, inner, _prefix) do
Settings.new() |> Settings.Approvals.approve(:project, "shell_full", inner)
{:approved, state}
end
defp approve_regex_scope(@global, state, inner, _prefix) do
Settings.new() |> Settings.Approvals.approve(:global, "shell_full", inner)
{:approved, state}
end
# ----------------------------------------------------------------------------
# Utilities
# ----------------------------------------------------------------------------
@doc """
Delegate to the pure prefix extraction logic.
"""
def extract_prefix(%{"command" => cmd, "args" => args}) do
base_cmd = Path.basename(cmd)
Services.Approvals.Shell.Prefix.extract(base_cmd, args)
end
# Prompt user for feedback when denying with feedback
defp get_feedback do
"Feedback:"
|> UI.prompt()
|> then(&"The user denied the request with the following feedback: #{&1}")
end
defp build_auto_deny_message() do
case Settings.get_auto_policy() do
{:deny, ms} when is_integer(ms) and ms > 0 ->
seconds = div(ms, 1000)
"""
The request was automatically denied after #{seconds} seconds due to an active auto-deny policy.
The user may not be monitoring their terminal. Only use pre-approved shell commands that will not require user confirmation.
"""
|> String.trim()
_ ->
@no_feedback
end
end
defp edit?, do: Settings.get_edit_mode()
defp auto?, do: edit?() && Settings.get_auto_approve()
end