camel-component-exec 0.23.0

Fail-closed system command execution component for rust-camel
Documentation

camel-component-exec

Fail-closed system command execution component for rust-camel.

Features

  • Profile-pinned, fail-closed — Zero profiles → nothing executes. Each profile is a named capability bundle (executable + arg-policy + env + cwd + caps) pinned at startup.
  • No shell (execvp) — Commands run via execvp-style semantics: binary path + literal argument array. No shell interpretation, no string concatenation injection surface.
  • Per-element arg-policy — Every argument is validated against ArgPolicy: any, exact { values }, or prefix { values }. deny_flags (broad prefix match) always wins over allow.
  • Environment sanitization — Child starts with empty environment. Three-layer control: env.allow (inherit from host), env.set (explicit KEY=VALUE), then global.deny_env glob patterns applied last/always-win.
  • cwd confinementworking_dir validated at startup relative to canonical_workspace_root. No .. escape, no absolute paths, no silent directory creation.
  • Timeout with process-group tree-kill — Unix: SIGKILL to entire process group. Windows v1: child.start_kill() (process-group tree-kill deferred).
  • Non-error outcomes — Timeout and exit-code-not-in-accepted-list return Ok(exchange) with result JSON + headers, NOT Err. Routes branch on CamelExecExitAccepted via choice().
  • Audit — Every execution emits an ExecAuditEvent with profile, args, env keys, exit code, and duration (redacted).

Quick Start

Add to Camel.toml:

[components.exec]
workspace_root = "."
default_timeout_secs = 30

[[components.exec.profiles]]
name = "echo"
executable = "echo"
args = { allow = "any" }
working_dir = "."
timeout_secs = 10
accepted_exit_codes = [0]

Use in a route:

routes:
  - id: "exec-echo"
    from: "timer:tick?period=5000"
    steps:
      - setHeader:
          name: "CamelExecArgs"
          value: ['{"args":["hello","world"]}']
      - to: "exec:echo"
      - to: "log:info"

Or programmatically:

use camel_component_exec::ExecBundle;
ctx.add_bundle(ExecBundle::from_toml(config))?;

URI Scheme

exec:{profile-name}

The target profile is determined by the endpoint URI path. There is NO dynamic override from exchange headers or body. Conditional dispatch to different profiles is achieved via choice() or recipient_list() in the route definition.

Security Model

See ADR-0037 for the full capability model.

Key tenets:

  • Fail-closed by default — No profiles → nothing executes. No shell injection surface. No PATH hijacking at runtime (canonical executable pinned at startup).
  • Profile-pinned capabilities — Exchange data cannot select executables or modify profile configuration.
  • Injection-resistant — execvp-style argument passing, no string concatenation. Arg-policy per-element with deny-first ordering.
  • Auditable — Every execution recorded with redacted env/args in audit events.

Configuration Reference

[components.exec] (global)

Field Type Default Description
workspace_root String "." Base directory for cwd confinement
default_timeout_secs u64 30 Default timeout per profile (seconds)
default_concurrency usize 1 Default producer semaphore capacity
deny_env Vec<String> (see below) Global env-var deny glob patterns applied LAST

Default deny_env patterns: LD_*, DYLD_*, PYTHONPATH, RUSTFLAGS, GIT_*, SSH_AUTH_SOCK, *_TOKEN, *_KEY.

[[components.exec.profiles]]

Field Type Default Description
name String Profile name (referenced as exec:{name})
executable String Binary name (PATH-resolved) or absolute path
args ArgPolicy exact with empty values Argument policy
deny_flags Vec<String> [] Flag denylist (broad prefix match, always wins)
allow_shell bool false Allow shell binary as executable
env.allow Vec<String> [] Host env var names to inherit
env.set HashMap {} Explicit KEY=VALUE pairs
working_dir String "." Working directory (relative to workspace_root)
timeout_secs Option<u64> global default Per-profile timeout override
accepted_exit_codes Vec<i32> [0] Exit codes treated as success
concurrency Option<usize> global default Per-profile concurrency cap

ArgPolicy Modes

Mode Description
any Every element accepted
exact { values: ["a", "b"] } Element must string-equal one of values
prefix { values: ["--"] } Element must byte-start-with one of values
Omitted Deny all non-empty args (fail-closed default)

Exchange Headers

Header Direction Description
CamelExecArgs Input JSON array of argument strings
CamelExecExitCode Output Exit code of the process
CamelExecExitAccepted Output true if exit code is in profile's accepted list
CamelExecTimedOut Output true if the process was killed by timeout
CamelExecStderr Output Stderr content (base64 string)
CamelExecStdoutTruncated Output true if stdout exceeded cap
CamelExecStderrTruncated Output true if stderr exceeded cap

The exchange body contains an ExecResult JSON object with exit_code, stdout, stderr, timed_out, duration_ms, profile, and truncation flags.

Architecture Decisions

  • ADR-0037 — Exec Component Fail-Closed Capability Model

Example

See examples/exec-example/ for a working example.

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.