Open
Conversation
Forward stderr from temporary OutputCollectors in pipeline execution and command substitution to the session's stderr sink instead of discarding it. Resolve relative file paths in while loop input redirects against the session's working directory. Handle :exec return value in SessionCase test helper.
Expand CommandPort to be the single interface for all user-facing OS process execution. Add thin delegates for ExCmd.Process lifecycle operations (os_pid, read, write, close_stdin, close_stdout, await_exit) and gateway functions with restricted-mode enforcement (start_link, stream, system_cmd). Internal helpers now call public delegates instead of ExCmd.Process directly.
Replace direct ExCmd.Process and System.cmd calls in JobProcess, Coproc, Pipeline, and Command builtin with CommandPort delegates. CommandPort is now the single gateway for all user-facing OS process execution. Add restricted-mode guard to external_command? in Pipeline to prevent streaming bypass.
dbernheisel
reviewed
Mar 26, 2026
Collaborator
dbernheisel
left a comment
There was a problem hiding this comment.
Most of this looks to include changes from the other branch -- ignoring that, I think this is a good direction, though let's expand the CommandPolicy idea a bit into a struct that can be built to hold more decisions.
I'm thinking something like:
%CommandPolicy{
commands: :no_external | [{:allow | :disallow, [...]}, fn x -> end] | fn x -> ... end,
paths: [~r/.../, "..", fn x -> ... end],
files: [~r/.../, "..", fn x -> ... end],
}this would be evaluated at the time of the access request and respond appropriately.
| nil <- try_elixir_interop(command_name, args, stdin, session_state), | ||
| nil <- try_builtin(command_name, args, stdin, session_state) do | ||
| nil <- try_builtin(command_name, args, stdin, session_state), | ||
| :ok <- check_command_policy(command_name, session_state) do |
Collaborator
There was a problem hiding this comment.
this might need to be evaluated before builtin? What do you think
Comment on lines
+208
to
+212
| if CommandPolicy.allowed?(CommandPolicy.from_state(state), name) do | ||
| {:file, path} | ||
| else | ||
| :not_found | ||
| end |
Collaborator
There was a problem hiding this comment.
Not quite related, but I wonder if CommandPolicy needs to be a struct so it can hold more than just an atom -- eg, they might allow/disallow commands, but also want to allow/disallow paths
6 tasks
Per review feedback, restricted mode is a session concern — not a process-spawning concern. Moves the helper and updates all callers.
Move command restriction enforcement from CommandPort to AST level.
CommandPort is now a plain process-spawning pipe with no policy logic.
Three policy types:
- :unrestricted (default, no restrictions)
- :disallow_external (blocks all external commands)
- {:allow, MapSet} (whitelist specific commands)
Policy is checked in AST.Command.resolve_and_execute, Pipeline.external_command?,
and each builtin that dispatches externally (exec, command, coproc). Background
jobs also check policy before spawning. Policy is immutable once set -- cannot be
changed via set or shopt at runtime.
Backwards compatible: `restricted: true` normalizes to `:disallow_external`.
Normalize {:allow, list} to {:allow, MapSet} at session init so users
can write {:allow, ["cat", "grep"]} instead of wrapping in MapSet.new.
eaa809f to
c62d70a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
PR #5 consolidated all
ExCmdusage intoCommandPortand added restriction gates there. However, per @dbernheisel's review feedback, restriction decisions should be made at a higher level — not in the process-spawning layer. The booleanrestricted: true/falseis also too rigid; users need fine-grained control over which external commands are allowed.Changes
New:
Bash.CommandPolicymoduleA pure-function module defining three policy types:
:unrestricted— default, no restrictions:disallow_external— block all external command execution{:allow, commands}— whitelist specific commands (accepts list orMapSet, matches both name andPath.basename)CommandPort is now a dumb pipe
All restriction logic removed from
CommandPort. Therestricted?/1helper,is_restrictedguard, andrestrictedparameter onstart_link,stream, andsystem_cmdare gone. CommandPort is purely a process-spawning interface.Policy enforcement at the AST level
Policy checks happen where command dispatch decisions are made:
AST.Command.resolve_and_executecheck_command_policy/2between builtin fallthrough and external dispatchPipeline.external_command?CommandPolicy.allowed?/2gates streaming optimizationexecbuiltinCommandPort.executecommandbuiltincommand -vlookupscoprocbuiltinSession(background jobs)do_start_background_jobandstart_background_job_syncBackwards compatible
options: %{restricted: true}is normalized tocommand_policy: :disallow_externalat session init.Immutable once set
setbuiltin preservescommand_policyacross option changesshopt restricted_shellis read-only and reflectscommand_policy != :unrestrictedUsage
Test plan
test/bash/command_restrictions_test.exs— 55 tests covering::disallow_externalblocks external commands (simple, absolute path, pipeline, subshell, command substitution, background jobs){:allow, list}permits whitelisted, blocks otherssetandshoptcommand -vrespects policyrestricted: truetest/bash/command_port_test.exs— updated (restriction tests removed, arities fixed)mix format --check-formattedcleanmix compile— no warnings