Launching processes¶
Starkite separates direct, shell-free binary execution from shell-wrapped execution. This approach enforces strong security boundaries while maintaining flexibility for developers.
The os module provides four functions to execute commands:
-
os.exec(cmd, args=[])Executes a binary directly and shell-free. If the command returns a non-zero exit code, it halts execution and raises a Starlark-level error. -
os.try_exec(cmd, args=[])Executes a binary directly and shell-free, returning anExecResultfor programmatic error handling. -
os.shell(cmd)Runs a command string inside a shell wrapper (default:/bin/sh -c), allowing pipes, redirections, and environment expansion. It halts execution and raises a Starlark-level error on non-zero exit codes. -
os.try_shell(cmd)Runs a command string inside a shell wrapper, returning anExecResultfor programmatic error handling.
Direct Process Execution (os.exec & os.try_exec)¶
By default, os.exec() and os.try_exec() execute target binaries directly and shell-free. This direct execution prevents common shell injection vulnerabilities.
To pass arguments to the binary, specify them as a list/tuple of strings in the second argument:
def check_os():
# Executes 'uname' directly with argument '-a'
os_info = os.exec("uname", ["-a"])
print("System OS:", os_info.strip())
Alternatively, if only a single string is passed, Starkite automatically splits the string by whitespace to resolve the binary and its arguments:
def check_os_single_string():
# Automatically splits by whitespace: runs 'uname' with '-a'
os_info = os.exec("uname -a")
print("System OS:", os_info.strip())
Permissions¶
Direct process execution requires the allow-all permission profile (or a custom profile explicitly granting os.exec permissions):
Shell-Wrapped Execution (os.shell & os.try_shell)¶
For tasks requiring shell features such as pipes (|), redirections (>, <), environment variable expansion (e.g., $VAR), command substitution, or filename globbing, use os.shell() or os.try_shell(). These run the command string inside a shell wrapper (default: /bin/sh -c on Unix/Linux systems):
def check_disk_space():
# Shell wrapper allows pipes and redirections
disk_info = os.shell("df -h / | tail -1")
print("Disk Info:", disk_info.strip())
Permissions¶
Because shell execution allows powerful shell features (like pipes and redirection) that present higher security risks, it is blocked under the default allow-all profile. Shell-wrapped execution requires the allow-all-shell permission profile:
Programmatic Error Handling (os.try_exec & os.try_shell)¶
Use os.try_exec() or os.try_shell() when you need to handle exit status codes programmatically. They never raise a Starlark error on command failure; instead, they return an ExecResult object.
def check_disk_space_safe():
# Safe to handle failure programmatically
disk = os.try_shell(
"df -h / | tail -1",
timeout = "5s",
cwd = "/tmp",
env = {"LANG": "C"},
)
if disk.ok:
fields = disk.stdout.split()
printf("Disk Available: %s (Used: %s)\n", fields[3], fields[4])
else:
printf("Warning: Disk check failed with code %d\n", disk.code)
Execution Options¶
All process execution functions accept the following optional keyword arguments:
| Option | Type | Default | Purpose |
|---|---|---|---|
shell |
string |
"/bin/sh" |
Shell binary used to execute the command string (applicable to os.shell and os.try_shell only). |
cwd |
string |
"" |
Working directory in which to run the sub-process. |
env |
dict |
None |
Environment variable overrides (mapping string to string) for the command execution context. |
timeout |
string |
"60s" |
Time limit for execution (e.g., "10s", "5m"). The process is killed if the timeout is exceeded. |
userid |
string | int |
None |
User identity under which to run the process (POSIX only). See details below. |
groupid |
string | int |
None |
Group identity under which to run the process (POSIX only). See details below. |
input |
string | bytes | io.reader |
None |
Data or read stream to write to the process standard input. |
output |
io.writer |
None |
Write stream to redirect the process standard output to. |
Handling Results¶
os.try_exec() and os.try_shell() return an ExecResult struct containing the following attributes:
.ok(bool):Trueif the command exited with code0and no internal errors occurred..code(int): The integer process exit code returned by the command..stdout(string): The standard output stream captured from the command..stderr(string): The standard error stream captured from the command..error(string): A combined error message if the command failed or timed out.
User and Group Execution¶
Local command execution natively supports running sub-processes under specified user and group identities (UID/GID) in POSIX environments (Linux, macOS).
Use the userid and groupid optional keyword arguments to configure the OS credentials of the spawned process:
userid(string|int): The username or numeric User ID (UID) of the target user.groupid(string|int): The group name or numeric Group ID (GID) of the target group.
Examples¶
Running as a Specific User (Username)¶
To run a command as a specific user, pass the username to userid:
def query_database():
# Runs the psql command directly as the postgres user
result = os.try_exec("psql", ["-c", "SELECT version();"], userid="postgres")
if result.ok:
print("Database Version:", result.stdout.strip())
Running with Numeric IDs¶
To run a command using specific numeric UID and GID:
def run_unprivileged_task():
# Runs the command as UID 65534 (nobody) and GID 65534 (nogroup)
result = os.exec("id", userid=65534, groupid=65534)
print("Identity:", result.strip())
Required Privileges¶
Changing process credentials (setuid/setgid) requires the parent kite process to have sufficient operating system privileges (typically running as root or having CAP_SETUID/CAP_SETGID capabilities). If kite is run under a standard non-privileged user, the OS kernel will reject the credential switch, and os.exec() will return a standard OS permission error (e.g., operation not permitted).
Streaming Input and Output¶
Subprocess execution integrates with the unified streaming contract via the input and output keyword arguments. This allows piping data directly into a process's standard input and redirecting its standard output.
Streamable Inputs¶
The input argument accepts:
* string: Passed directly as standard input to the command.
* bytes: Passed directly as standard input to the command.
* io.reader: An active read stream (such as a stream returned by fs.path.get_reader() or an HTTP client response stream). The stream is copied to the subprocess standard input.
Once the command terminates (or times out), the input stream is automatically closed by the runtime.
Streamable Outputs¶
The output argument accepts:
* io.writer: An active write stream (such as a stream returned by fs.path.get_writer()). The subprocess standard output is written directly to the stream.
Once the command terminates (or times out), the output stream is automatically flushed and closed by the runtime.
Examples¶
Piping a File to a Process¶
To pipe a file's content directly into the standard input of a process:
def count_lines():
p = fs.path("data.txt")
p.write_text("line 1\nline 2\nline 3\n")
# Streams file data directly to the stdin of 'wc -l'
reader = p.get_reader()
result = os.exec("wc -l", input=reader)
print("Line count:", result.strip())
Piping Process Output to a File¶
To redirect a process's standard output directly to a file:
def save_system_info():
out_file = fs.path("sysinfo.txt")
# Redirects stdout of 'uname -a' to the file writer
writer = out_file.get_writer()
os.exec("uname -a", output=writer)
print("Saved content:", out_file.read_text().strip())
Multi-stage Pipeline (File-to-File via Subprocess)¶
To stream from an input file, process it with a command, and write the output directly to another file:
def process_pipeline():
in_file = fs.path("input.log")
out_file = fs.path("output.log")
in_file.write_text("debug log entry\nerror log entry\ninfo log entry\n")
r = in_file.get_reader()
w = out_file.get_writer()
# Pipes input.log into 'grep error' and writes the output directly to output.log
os.exec("grep error", input=r, output=w)
print("Filtered log:", out_file.read_text().strip())