Copyright - License Notice -------------------------- "AIShell-Gate" Copyright (c) 2026 AIShell Labs LLC Winston-Salem NC. USA. All Rights Reserved. Author: Sean T. Gilley Use of this Software requires a valid license obtained from AIShell Labs LLC. This notice summarizes the license terms. The controlling agreement is the license document issued upon payment. In the event of any conflict between this notice and that document, the issued license document governs. For license information and purchase: www.aishell.org/aishellgate EVALUATION AND BETA COPIES Unlicensed evaluation and beta copies of this Software expire 30 days from the date of download. After expiration, continued use requires a purchased license. No evaluation or beta copy may be used in a production environment. FIXED-HOST LICENSE A per-host license covers installation and use on a single, fixed physical or virtual machine, identified at the time of purchase. This license is perpetual for that host. Use on any additional host requires a separate license. CLOUD AND EPHEMERAL DEPLOYMENTS For cloud, containerized, or other ephemeral deployments where the host identity is not fixed, licensing is based on the average number of concurrently running instances. See cloud-licensing.txt included with this distribution. SOURCE CODE ACCESS Source code is made available exclusively to licensees who have executed a Non-Disclosure Agreement with AIShell Labs LLC, for the sole purpose of security review and audit. No other use of the source code is permitted. The source code may not be modified, redistributed, or disclosed. ALL RIGHTS RESERVED You may not redistribute, sublicense, resell, or transfer this Software or any license to any third party. You may not modify, adapt, or create derivative works. You may not remove or alter any copyright or attribution notices. NO WARRANTY THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. LIMITATION OF LIABILITY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, AISHELL LABS LLC SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE. aishell-gate-exec(1) ==================== NAME ---- aishell-gate-exec -- JSON executor wrapper for aishell-gate-policy SYNOPSIS -------- aishell-gate-exec [executor-flags] [policy-flags] [-- other-policy-flags] aishell-gate-exec --policy ./aishell-gate-policy --plan plan.json aishell-gate-exec --preset ops_safe --jail-root /work --plan plan.json echo '{"goal":"check repo","actions":[{"type":"command","cmd":"git status"}]}' \ | aishell-gate-exec --policy ./aishell-gate-policy --preset dev_sandbox DESCRIPTION ----------- aishell-gate-exec is the execution harness that sits between a JSON action plan (from an AI agent or operator) and the Unix system. It has no policy logic of its own. Its job is to: 1. Read a structured JSON action plan from stdin or --plan FILE. 2. Submit the plan to aishell-gate-policy for policy evaluation. 3. Display the evaluation summary to the operator. 4. Collect human confirmation for any action that requires it. 5. Execute allowed and confirmed actions via execve() with a resolved absolute binary path and a sanitised environment. 6. Write a structured audit trail of every step. aishell-gate-exec enforces a strict separation between evaluation and execution. The policy engine is a separate process invoked via fork/execve across a hard OS boundary. The executor reads only the JSON output of that process; it never parses the human-readable trace text that the engine also emits. Commands are never passed through a shell. The pre-tokenised argv[] array from the policy engine's JSON response is passed directly to execve(), eliminating the class of shell-injection vulnerabilities that arise when a command string is re-evaluated at execution time. INPUT FORMAT ------------ The input is a JSON object, read from stdin by default or from a file with --plan. The following fields are recognised: goal (required) Human-readable description of intent. Included in the audit log and displayed in the evaluation banner. Maximum 511 characters. source (optional) Provenance label forwarded to the policy engine for taint tracking. Values: "ai" (default), "envelope", "raw", "web". strategy (optional) Multi-action failure policy. Values: "fail_fast" Stop at the first action that exits non-zero. Remaining confirmed actions are skipped and logged. The first non-zero exit code is returned. Default. "best_effort" Attempt all allowed and confirmed actions regardless of prior failures. Returns the first non-zero exit code encountered (preserves root cause, not last error). actions (required) Array of action objects. Each action may be: { "type": "command", "cmd": "git status" } or a bare string shorthand: "git status" Only type "command" is currently supported. Maximum 32 actions. Example: { "goal": "inspect repository state", "source": "ai", "strategy": "fail_fast", "actions": [ { "type": "command", "cmd": "git status" }, { "type": "command", "cmd": "git diff --stat" } ] } OPTIONS ------- Executor flags: --policy Path to the aishell-gate-policy binary. Resolved to an absolute path at startup via realpath(3) and verified executable before any plan is read. Must contain a '/' character — bare names are rejected to prevent PATH-based substitution. Default: ./aishell-gate-policy --plan Read the input JSON plan from file instead of stdin. --eval-timeout Kill the policy engine process with SIGKILL if evaluation takes longer than this many seconds. Execution fails closed on timeout. Default: 30. Set to 0 to disable. --max-response-bytes Kill the policy engine process and fail closed if its JSON response exceeds n bytes. Buffer growth is capped before the limit to prevent memory exhaustion. Default: 8388608 (8 MiB). Set to 0 to disable. --audit-log Write a structured JSON Lines audit log to file. Default: $AISHELL_AUDIT_LOG environment variable, or /var/log/aishell/audit.log if the variable is unset. If --audit-log is given explicitly, failure to open the file is fatal. If using the default path, a failure produces a warning but does not abort execution. --confirm-tty Single-session interactive use only. Read confirmation prompts from PATH instead of /dev/tty. Use this when running a single AI session on a machine you are logged into and want to redirect prompts to a specific device. Not suitable for multi-session remote deployments — use --confirm-pipe instead. When neither flag is given the default is /dev/tty, which works correctly in any session that has a controlling terminal. --confirm-lock Serialise concurrent confirmation prompts. Only one aishell-gate-exec session may be in the confirmation phase at a time; others block on an exclusive flock(2) until the current session finishes and releases the lock. The lock is released before any execve(2) call so command execution across sessions proceeds in parallel. The lock file is created automatically if it does not exist. Required when --confirm-pipe is in use and multiple AI sessions may connect simultaneously. Default path: /run/aishell-gate/confirm.lock The lock is only acquired when --confirm-lock or --confirm-pipe is explicitly set; single-session deployments using /dev/tty are not affected. --confirm-pipe Secure pipe-based operator confirmation relay for remote multi-session deployments. BASEPATH.req and BASEPATH.resp must be named FIFOs created by the aishell-confirm(1) companion tool, which owns them and runs as the operator. aishell-gate-exec writes a JSON request to BASEPATH.req containing the full command context: session_id, action index, confirm level, cmd, goal, source, risk_score, blast_radius, reason, and for typed confirmations the challenge code. aishell-confirm reads it, displays all context on the operator's own terminal, reads the operator's response, and writes it back through BASEPATH.resp. aishell-gate-exec never opens any PTY device in this path. Use with --confirm-lock for multi-session safety. Default base path: /run/aishell-gate/confirm --verbose Print internal diagnostics to stderr: envelope contents, response JSON, policy engine exit code, response byte cap, and execution path resolution. --help / -h Print a usage summary and exit. --version Print the version string and exit. Common policy engine flags (no -- required): The six policy engine flags that appear on most invocations may be given directly alongside executor flags, without a -- separator: --preset Built-in policy bundle: read_only, dev_sandbox, ops_safe (default), danger_zone. Forwarded to the policy engine. --policy-base Base policy override file. Forwarded to the policy engine. --policy-project Project policy override file. Forwarded to the policy engine. --policy-user User policy override file. Forwarded to the policy engine. --jail-root Restrict write-like commands to this path tree. Forwarded to the policy engine. --sandbox Sandbox mode hint: none, cwd_jail, chroot, container, userns. Forwarded to the policy engine. Other policy engine flags (pass after --): Any flag not listed above that needs to reach the policy engine must appear after a -- separator. Everything after -- is forwarded verbatim. Maximum 64 total forwarded flags (including the six above). Less-common policy engine flags: --audit Policy engine audit log (separate from the executor audit log written by --audit-log) --mode Session mode: auto, interactive, batch, daemon --source Taint source override See aishell-gate-policy(1) for the full list. EXECUTION MODEL --------------- Evaluation pipeline aishell-gate-exec submits the plan to the policy engine subprocess as a JSON envelope over a pipe. The subprocess receives the envelope on its stdin and writes its JSON decision report to stdout. aishell-gate-exec reads this report with a configurable deadline (--eval-timeout) and byte cap (--max-response-bytes). If either limit is exceeded, the subprocess is sent SIGKILL and execution fails closed. The JSON report is the sole authoritative source of policy decisions. Any human-readable text the policy engine emits alongside the JSON is for operator display only and is never parsed. Confirmation collection All human confirmations are collected before any command is executed. This prevents a partially-executed plan when an operator refuses a mid-sequence action. The confirmation window is serialised across concurrent sessions via --confirm-lock (see OPTIONS). Two confirmation paths are available: TTY path (single-session interactive use) Prompts are read from /dev/tty (or --confirm-tty PATH). This is the correct path for a single operator running a single AI session on a machine they are logged into. The executor opens the TTY device directly. Pipe path (remote multi-session deployments) When --confirm-pipe is set, the executor writes a JSON request to the named FIFO and reads the response from the paired FIFO. aishell-confirm(1) owns both FIFOs and runs as the operator, displaying full command context on their terminal and relaying the response. The executor never opens any PTY device. The typed challenge code is sent only through the pipe — it never reaches the AI's stderr channel. Confirmation levels: none No prompt; proceed immediately. plan "Proceed? [y/N]" — review the step before execution. action "Approve? [yes/NO]" — explicit operator approval required. typed Command-specific challenge must be typed back exactly. The typed challenge is an 8-character alphanumeric code derived from the command text using FNV-1a hashing. Visually ambiguous characters (0, 1, i, l, o) are excluded. The challenge changes with every unique command, proving the operator read the specific command being authorised rather than muscle-memorying a fixed word. 31^8 ≈ 852 billion combinations. Execution Each allowed and confirmed command is executed via fork+exec. All plans use fork+wait so the parent process always survives to write closing audit entries (EXEC_COMPLETE, SESSION_END) before exiting. The parent then exits with the child's exit code, so the exit code visible to the caller is identical to the child's. Absolute binary path The command name is resolved against a compile-time safe path list: /usr/local/bin, /usr/bin, /bin, /usr/sbin, /sbin The inherited $PATH is never consulted. A binary not found in the safe path list fails with exit code 6 rather than silently proceeding. Sanitised environment A minimal environment is constructed from an explicit allowlist of variables: HOME, USER, LOGNAME, TERM, COLORTERM, LANG, LC_ALL, LC_CTYPE, LC_MESSAGES, LC_TIME, LC_NUMERIC, LC_COLLATE, TZ, TMPDIR. PATH is always set to the safe path list above. All other variables from the calling environment are silently dropped, including LD_PRELOAD, DYLD_INSERT_LIBRARIES, PYTHONPATH, GIT_EXEC_PATH, and any other interpreter or dynamic-linker injection vectors. Pre-tokenised argv[] The argv[] array comes directly from the policy engine JSON response. aishell-gate-exec never re-parses or re-splits a command string at execution time. Multi-action plans Actions are run sequentially. The strategy field controls behaviour on non-zero exit: fail_fast stops at the first failure and logs how many confirmed actions were skipped; best_effort continues through all actions and returns the first non-zero exit code encountered. STARTUP SECURITY CHECKS ----------------------- Two checks run at startup, before argument parsing and before any file is opened: Setuid / setgid refusal If the real and effective UID (or GID) differ, the OS elevated this process via the setuid or setgid bit. aishell-gate-exec refuses to run with borrowed privileges because the confirmation UI reads from /dev/tty, environment variables affect the safe path search, and any bug in the executor binary becomes a local privilege escalation path. Fix: chmod u-s aishell-gate-exec (or chmod g-s). Binary writable by group or others If an attacker can overwrite the executor binary, the policy engine is never consulted and policy is bypassed entirely. The binary's permissions are checked via stat(). On Linux, /proc/self/exe is used to obtain the real path, immune to argv[0] spoofing. If the stat fails, a warning is emitted and this check is skipped. Fix: chmod go-w aishell-gate-exec. A security violation fails with exit code 5. If an audit log is accessible, a SECURITY_VIOLATION event is written before exit. AUDIT LOGGING ------------- aishell-gate-exec writes one JSON Lines entry to the audit log per event. Entries are HMAC-SHA256 chained: each record contains the HMAC of the previous entry, so a gap or modification is detectable by any holder of the key. Log path (checked in this order): 1. --audit-log FILE command-line flag 2. AISHELL_AUDIT_LOG environment variable 3. /var/log/aishell/audit.log HMAC key (checked in this order): 1. AISHELL_AUDIT_KEY environment variable (64 hex characters) 2. /etc/aishell/audit.key (64 hex characters, first line) 3. Per-session random key from /dev/urandom (ephemeral; chain cannot be verified across sessions in this mode — a warning is emitted to stderr at startup; the warning text contains the word "ephemeral" to aid grep-based monitoring) Concurrent write safety: flock(LOCK_EX) is acquired on the log file descriptor before each write. The HMAC chain anchor and sequence counter are updated while the lock is held to prevent a forked chain if two processes race on the same log file. Multi-session audit note: each aishell-gate-exec invocation is an independent process with its own in-memory chain state. When multiple sessions write to the same log file, the file contains one internally consistent HMAC chain per session_id, not a single global sequence. Verify chains per session_id; treating the entire log as a single linear sequence will produce false verification failures when sessions are concurrent. Event types written to the audit log: SESSION_START Executor started; policy engine path, version, key mode (persistent or ephemeral). SESSION_END Session closed; exit code, actions executed, policy-denied count. PLAN_RECEIVED Goal, source, strategy, action count. POLICY_DECISION Per-action: cmd, allow/deny, reason, layer, risk score, policy version. CONFIRMATION_REQUESTED Operator prompted for a specific action. CONFIRMATION_RESULT Whether the operator approved or refused. EXEC_START Command about to be exec'd; resolved absolute path. EXEC_COMPLETE Command finished; exit code. EXEC_DENIED Execution blocked at the executor level (empty argv, binary not found in safe path). SECURITY_VIOLATION Startup security check failed, or policy engine response was missing required fields. ENVIRONMENT ----------- AISHELL_AUDIT_LOG Default audit log path when --audit-log is not specified. AISHELL_AUDIT_KEY 64-character hex HMAC key for audit chain authentication. Takes priority over /etc/aishell/audit.key. EXIT STATUS ----------- 0 All actions allowed, confirmed, and executed (all exited 0). 1 Policy DENY on one or more actions; no commands were executed. 2 Human confirmation refused; no commands were executed. 3 Policy engine process error (spawn failed, timeout, byte limit, killed by signal, or bad exit code from the policy engine). 4 JSON parse error (input plan or policy engine response). 5 Usage or argument error, startup security check failed, or confirmation lock/pipe could not be opened. 6 execve() failure after confirmed ALLOW: binary not found in the safe path list, or exec returned an error. BUILD ----- cc -std=c11 -Wall -Wextra -o aishell-gate-exec aishell-gate-exec.c No external libraries are required. JSMN (JSON tokenizer) and SHA-256 are embedded. Requires a POSIX.1-2008 compatible system (Linux, macOS, BSDs). USAGE EXAMPLES -------------- Basic interactive use, reading the plan from stdin: echo '{ "goal": "check repository status", "actions": [{"type":"command","cmd":"git status"}] }' | ./aishell-gate-exec --policy ./aishell-gate-policy Read plan from a file with a read-only preset: ./aishell-gate-exec --policy ./aishell-gate-policy \ --plan plan.json --preset read_only Multi-action plan with best_effort strategy and audit log: ./aishell-gate-exec --plan multi.json --preset dev_sandbox \ --audit-log /var/log/aishell/exec.log Pass a project policy file and a jail root: ./aishell-gate-exec --plan plan.json \ --policy-project ./aishell-gate-policy_project.json \ --jail-root /home/ci/workspace Custom evaluation timeout: ./aishell-gate-exec --eval-timeout 10 --plan plan.json Pass a less-common policy engine flag (-- required): ./aishell-gate-exec --plan plan.json --preset ops_safe -- --mode batch Verbose diagnostics for troubleshooting: ./aishell-gate-exec --verbose --plan plan.json Remote multi-session deployment (operator runs aishell-confirm in a second SSH session): ./aishell-gate-exec --policy /usr/local/bin/aishell-gate-policy \ --preset ops_safe \ --confirm-pipe /run/aishell-gate/confirm \ --confirm-lock /run/aishell-gate/confirm.lock \ --audit-log /var/log/aishell/audit.log \ --plan plan.json Print version: ./aishell-gate-exec --version LIMITS ------ Maximum actions per plan: 32 Maximum policy engine flags: 64 JSON token budget: 4096 tokens per parse Maximum goal length: 511 characters Maximum command length: 4095 characters Typed challenge length: 8 characters (FNV-1a derived, 31^8 combinations) DESIGN NOTES ------------ aishell-gate-exec deliberately has no policy logic. Policy belongs entirely to aishell-gate-policy, which is a separate binary evaluated in a separate process. This separation means the executor can be audited for a single concern: does it faithfully interpret and execute what the policy engine decided, with correct confirmation and a clean environment? The JSON channel between the two programs is the trust boundary. aishell-gate-exec trusts the JSON decision report produced by the policy engine subprocess and nothing else. It does not accept policy overrides from the calling environment, from the input plan, or from any source other than that report. Confirmations are collected in a single pass before any command runs. This makes the operator's intent atomic: either everything is confirmed and then executed, or nothing is executed. A mid-plan refusal cannot leave the system in a partially-modified state that the operator did not consciously authorise. All plans use fork+wait for execution so that the parent process always survives to write closing audit entries. The exit code visible to the caller is the child's exit code propagated through the parent's return value. The audit log is independent of the policy engine audit log. The policy engine records what it evaluated and decided; aishell-gate-exec records what it confirmed and executed. Together they provide end-to-end traceability from intent (the goal field) through evaluation, confirmation, and execution. SEE ALSO -------- aishell-gate-policy(1) -- policy evaluation engine aishell-confirm(1) -- operator confirmation relay for remote deployments execve(2) -- used for all command execution flock(2) -- advisory locking used by the audit subsystem and confirmation serialisation lock fnmatch(3) -- used internally by policy engine rule evaluation AUTHOR ------ Sean T. Gilley VERSION ------- aishell-gate-exec 0.25.0-beta