Launching processes¶
Starkite provides direct, structured process execution via the os module. All command execution is governed uniformly under standard process execution permissions.
The os module provides two primary execution functions:
-
os.exec(cmd, args=[])Executes a binary directly. 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, returning anExecResultfor programmatic error handling.
Direct Process Execution (os.exec & os.try_exec)¶
os.exec() and os.try_exec() execute target binaries directly via standard system calls. Direct process execution avoids shell spawning overhead and prevents shell injection vulnerabilities.
Starkite supports two invocation styles:
1. Single Command String (Lexical Tokenization)¶
When passing a single command string, Starkite uses an internal lexical tokenizer to parse the command and its arguments without invoking an external shell:
def check_status():
# Double quotes preserve arguments with internal spaces
os.exec('git commit -m "initial commit"')
# Single quotes preserve literal payloads (e.g. JSON or Regex)
os.exec("grep -E '^[0-9]+' output.txt")
# Backslash escapes outside quotes
os.exec("cat file\\ with\\ spaces.txt")
# Quoted executable paths
os.exec('"/opt/custom tools/bin/app" --verbose')
Lexical Quoting Rules¶
| Syntax | Behavior | Example | Parsed Result |
|---|---|---|---|
Single quotes ('...') |
Preserves literal text inside quotes. No escape interpretation. | '{"k":"v"}' |
{"k":"v"} |
Double quotes ("...") |
Preserves spaces; interprets backslash escapes (\", \\). |
"hello \"world\"" |
hello "world" |
Escaped space (\) |
Preserves space as part of the current token. | file\ name.txt |
file name.txt |
| Quoted binary | Quoted executable path containing spaces. | "/opt/my app/bin" |
/opt/my app/bin |
2. Structured Arguments List¶
To pass pre-constructed argument slices without string formatting, pass the binary name as the first argument and a list/tuple of strings as the second:
def check_os():
# Passes arguments directly as argv elements
os_info = os.exec("uname", ["-a"])
print("System OS:", os_info.strip())
Running Shell Features (Pipes, Redirection, Chaining)¶
Because direct process execution does not use a shell, shell features like pipes (|), redirections (>, <), and chaining (&&, ;) can be invoked via os.shell() or the shell factory shortcuts:
def check_disk_space():
sh = os.sh()
disk_info = sh.exec("df -h / | tail -1")
print("Disk Info:", disk_info.strip())
Permissions¶
Direct process execution requires the allow-all permission profile (or a custom profile explicitly granting os.exec permissions):
Shell Execution (os.shell and Shortcuts)¶
For scripts requiring shell parsing, multi-command pipelines, or environment persistence, the os module provides the os.shell() constructor and factory shortcuts.
Shell Constructors and Shortcuts¶
| Constructor | Default Command | Default Flag | Description |
|---|---|---|---|
os.shell(...) |
Platform default (/bin/sh or cmd.exe) |
Platform default (-c or /c) |
Configurable shell instance |
os.sh(...) |
/bin/sh |
-c |
Standard POSIX shell |
os.bash(...) |
/bin/bash |
-c |
Bash shell |
os.zsh(...) |
/bin/zsh |
-c |
Zsh shell |
os.cmdexe(...) |
cmd.exe |
/c |
Windows Command Prompt |
os.powershell(...) |
pwsh (or powershell.exe) |
-Command |
PowerShell shell |
Each constructor accepts initial defaults:
* command (string): Target shell binary or path.
* flag (string): Argument flag preceding the script (-c, /c, -Command).
* cwd (string): Default working directory.
* env (dict): Bound environment variable map.
* timeout (string): Bound execution timeout (default: "60s").
* userid / groupid (string | int): User and group execution identities (POSIX only).
Shell Methods: exec and try_exec¶
A Shell instance provides two execution methods:
* sh.exec(script, ...): Executes the script string in the shell, returning standard output. Halts execution with an error on non-zero exit.
* sh.try_exec(script, ...): Executes the script string, returning an ExecResult for programmatic exit code checks.
Both methods accept per-invocation overrides for cwd, env, timeout, userid, groupid, input, and output.
Examples¶
Pipelines and Redirections¶
def extract_process_info():
bash = os.bash()
top_proc = bash.exec("ps aux | sort -nrk 3,3 | head -n 5")
print("Top processes by CPU:\n", top_proc)
Multi-line Script Execution¶
def run_setup():
sh = os.sh()
setup_script = """
set -e
mkdir -p build/logs
touch build/logs/app.log
"""
sh.exec(setup_script)
Bound Context and Per-Call Overrides¶
def run_build():
# Configure base shell environment
ci = os.bash(
cwd = "/repo",
env = {"CI": "true", "GOOS": "linux"},
timeout = "5m",
)
# Per-call environment overrides merge with bound options
ci.exec("make build", env = {"VERBOSE": "1"})
Permission Model¶
Constructing a Shell performs no I/O and requires no permissions. Calling sh.exec(script) delegates directly to os.exec([sh.command, sh.flag, script]) and validates against standard os.exec permission rules for the target shell binary.
Programmatic Error Handling (os.try_exec)¶
Use os.try_exec() when you need to handle exit status codes programmatically. It never raises a Starlark error on command failure; instead, it returns an ExecResult object.
def check_disk_space_safe():
# Safe to handle failure programmatically
disk = os.try_exec(
"sh",
["-c", "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 |
|---|---|---|---|
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() returns 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())