STAGE 4 / Native automation · LESSON 23
What can this code touch?
Process execution can invoke another program with the current user's privileges. A Kujo capability gate controls access to that native surface; it does not automatically restrict everything the child executable can do.
Native contract
spawn_process accepts an argv array and returns a ProcessResult struct. Inspect exitcode, stdout, stderr, success, timed_out, cancelled, and output truncation flags. Options bound timeout and captured output, and can control inherited environment and working directory.
execute_status uses a shell string and requires separate shell-exec authority. Prefer argv when you do not need shell syntax. This keeps spaces and punctuation inside an argument from becoming shell operators. It does not prevent a dangerous argument from being interpreted by the chosen executable itself.
The example invokes /usr/bin/printf with a fixed format and string on macOS/Linux. This is a platform-specific native integration drill. In your own program, select an operator-approved executable rather than accepting an arbitrary model-proposed program name.
Machine receipt
ProcessResult is a struct, not directly a JSON dictionary. Copy the fields your caller needs. Refuse to treat truncated output as a complete artifact. A timeout or cancellation forces success false, but your application still needs a cleanup and retry policy.
Professional pattern
Use a fixed executable, validated arguments, a small environment, finite timeout, and bounded output. Redact sensitive values at the process capture boundary. A child process with broad credentials can leak them regardless of whether the parent prints a Secret wrapper safely.
Common mistakes
Do not use shell quoting as your only command policy. Do not infer success from stdout text while ignoring exit status. Do not grant shell-exec just to avoid assembling an argv array. The stage build uses a controlled process and serializes only selected fields.
Working example
let result := spawn_process(["/usr/bin/printf", "%s", "course-process"], {"timeout_ms": 2000, "max_output_bytes": 1024, "inherit_env": false})
assert_equal(result.success, true)
assert_equal(result.stdout, "course-process")
print(to_json({"ok": result.success, "exitcode": result.exitcode, "stdout": result.stdout}))
Run it
From the course repository root, use the pinned Kujo 1.9.0 runtime.
kujo check examples/23.kujo
kujo run --untrusted --allow-process-exec examples/23.kujo
Captured output
{"exitcode":0,"ok":true,"stdout":"course-process"}
Break it and diagnose it
The argv array is empty. The runtime must reject the invalid arguments before starting a child process.
spawn_process([], {"timeout_ms": 2000})
kujo run --untrusted --allow-process-exec examples/23-break.kujo
Exit status: 4. Captured diagnostic:
[KUJOVM001] [vm] Runtime Error: spawn_process requires a non-empty array of command arguments
--> 0:0
Exercise
Replace printf with an explicitly configured equivalent on your operating system. Test a nonzero exit and a bounded timeout. Produce a dictionary receipt that rejects truncation and never includes inherited secrets.
Checkpoint
- I use argv for direct process calls.
- I inspect status and truncation.
- I understand child-process authority.
Process replacement and POSIX tooling
Kujo 1.4.0 adds exec_process(argv, options) for POSIX command launchers. Unlike spawn_process, successful replacement does not return a ProcessResult: the new program inherits terminal streams and working directory. Explicit argv avoids shell parsing but does not limit the executable's authority. The operation requires process-exec.
Related file_lock, file_unlock, path_owned, and symlink_atomic APIs support package installation. Advisory locks coordinate cooperating processes; they cannot stop a malicious same-user writer. Atomic symlink publication requires a trusted parent and both write/delete authority. These POSIX APIs fail explicitly on unsupported platforms, including Windows; they do not emulate unsafe shell operations. Review the native security contract before using them.
Deadlines cover output-pipe lifetime
In 1.8.1, a subprocess deadline remains active while inherited stdout/stderr pipes drain after the direct child exits. Unix timeout or cancellation terminates the process group. Windows commands enter a per-command job before execution; timeout, cancellation and command return clean up descendants, and failed job admission fails closed. Background descendants must not rely on outliving the call.
Processes that escape Unix groups still require external isolation. GIF conversion uses a 30-second deadline and 1 MiB capture per stream; these limits do not constrain converter memory or output-file size. The course reviews the tagged process contracts; it does not claim local macOS checks certify Windows jobs.
Verified 2026-10-22 · Official Kujo 1.9.0 release · Source contract · Download example