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-policy(1) ==================== NAME ---- aishell-gate-policy -- deterministic policy engine for AI-generated shell commands SYNOPSIS -------- aishell-gate-policy [options] [input-file] aishell-gate-policy --preset ops_safe --audit audit.jsonl aishell-gate-policy --preset read_only request.json aishell-gate-policy --audit-key audit.key --audit-verify /var/log/ag.jsonl Interactive mode: $ aishell-gate-policy > git status ALLOW: command validated by policy (confirmation: none) With executor (aishell-gate-exec): echo '{"goal":"check repo","actions":[{"cmd":"git status"}]}' \ | aishell-gate-exec --policy ./aishell-gate-policy --preset ops_safe DESCRIPTION (Short) ------------------- aishell-gate-policy evaluates shell commands or structured JSON requests and decides whether they are permitted under a configurable policy. It does NOT execute commands. It is the policy engine half of a two-component system. The companion executor, aishell-gate-exec(1), handles plan intake, human confirmation, and execve() once the policy engine has issued an ALLOW decision. FULL DESCRIPTION ---------------- aishell-gate-policy sits between AI systems (or humans) and Unix execution, providing deterministic validation, risk classification, and policy enforcement before any command runs. The separation of evaluation from execution is a core security property, not a convenience. aishell-gate-policy has no ability to execute anything. aishell-gate-exec has no policy logic. An executor that is compromised cannot grant itself permission to run a command that the policy engine has denied, because the permission decision lives in a separate process across a hard OS boundary. This tool follows several core principles: - Validate-only by design - Default deny - Explainable decisions - Human-first UX - Machine-readable output - No shell evaluation - Explicit confirmation levels - Audit-first mindset It does not replace Unix permissions or security models. It provides an explicit, policy-governed decision layer before execution occurs. Core concepts: Sessions Each invocation represents a session with identity (uid/gid), working directory, environment constraints, and mode (interactive, batch, daemon). SSH session detection is automatic via environment variables. Session policy can gate evaluation entirely on uid/gid allowlists, username allowlists, TTY state, session mode, and time-of-day -- before any command rule is evaluated. Policies Rules defining what is allowed or denied. Policies are layered: built-in defaults (or a selected preset), optional base overrides, optional project overrides, and optional per-user overrides. Each layer can extend or replace the layer beneath it. A deny at any layer is final; no later layer can promote it to allow. Unknown top-level keys in a policy file are rejected immediately (not silently ignored) to catch typos like "cmd_denny". Unknown session sub-keys are also rejected, preventing silent no-ops from mistyped policy fields like "deny_sssh". Decisions Every evaluation results in either ALLOW or DENY. There is no WARN state. Confirmation levels govern how much human review is required for allowed commands. Confirmation levels none -- no confirmation required; proceed immediately plan -- show the plan before executing; human review suggested action -- explicit per-command human confirmation required typed -- human must type out a specific phrase to confirm Confirmation levels are set by policy rules and raised (never lowered) by risk classification. An allowed command with a high risk score will have its confirmation level upgraded automatically. Escalation is strictly monotonic: a command confirmed at "typed" cannot be downgraded by a lower risk score. Risk classification Commands are scored 0-100 and tagged with risk flags: destructive -- modifies or deletes data irreversibly exfiltration -- transfers data off-system privilege_escalation -- attempts to gain elevated access persistence -- modifies scheduling, services, or startup scan -- probes network or system topology Score is computed in two steps. First, risk_classify() assigns a base score from the command catalog and applies argument modifiers: +10 recursive flag on a destructive command +10 --force with rm or mv +15 any argument targets a system path (/etc, /usr, /boot, etc.) +10 curl or wget called with a URL argument Score is capped at 100 and floored at 0. Second, risk_apply_confirmation() applies escalation thresholds: score >= 40 -> confirm raised to at least plan score >= 70 -> confirm raised to at least action score >= 90 -> confirm raised to at least typed Blast radius is also assigned: none, single, tree, system, or unknown. "system" is set when a root or system path is targeted; "tree" when recursive; "single" otherwise. IO classification Each command is classified as read, write, mixed, net, exec, or unknown. This is recorded in the audit log and JSON output and may influence sandbox guidance provided to the executor. Command catalog An internal catalog of common Unix commands provides base risk scores, risk flags, and minimum confirmation floors. The catalog covers ~80+ commands across categories: observe, read_text, write_fs, overwrite, destructive, priv, persist, network, scan, pkg, interpreter, and build. Representative catalog scores: ls, cat, grep -- score 0-5, confirm none git -- score 20, confirm plan curl, wget -- score 60, confirm action rm -- score 80, confirm typed dd, parted, fdisk -- score 95, confirm typed mkfs, wipefs -- score 98, confirm typed Policy rules may impose stricter requirements than the catalog; catalog values are a floor, not a ceiling. Commands not in the catalog start at score 0, flags 0, confirm none, and are governed by policy rules alone. Sandbox guidance (advisory) The engine can emit sandbox mode hints and resource limits for the downstream executor. These are advisory only -- the engine does not enforce them at the kernel level. The one active exception is cwd_jail: when a jail root is configured, path arguments for write-like commands are evaluated against the jail root during policy evaluation. See SANDBOX OPTIONS. Taint tracking All input is marked tainted on arrival. A result is untainted only after the engine has completed evaluation and issued an ALLOW decision. Taint status is reported in JSON output and the audit log. Presets Built-in policy bundles for common workflows. See PRESET MODES. Audit Structured append-only logging of every evaluation in JSON Lines format with tamper-evident hash chaining. Each entry is linked to the previous by a SHA-256 (or HMAC-SHA256) hash. See AUDIT LOGGING. Busy summary Each evaluation produces a 3-5 line plain-text summary (busy_summary_text) suitable for display to a human or inclusion in an LLM context window. MODES OF OPERATION ------------------ Interactive (human) Run with no input file when stdin is a TTY. Commands are typed one per line. > rm -rf / DENY: destructive command denied: rm Output includes: source -> normalized -> decision -> rule -> reason -> suggestion -> trace. Interactive special commands: (empty line) no-op quit or exit terminate json or :json print full JSON for the previous command JSON input (AI / automation) Provide a JSON envelope via file argument or stdin. Input must start with a '{' character. Output is structured JSON. Example envelope: { "goal": "check repository state", "source": "ai", "actions": [ "git status", { "type": "command", "cmd": "git diff" } ] } The "goal" field is required. The "actions" array is required. Each action may be a bare string (treated as type=command) or an object with "type" and "cmd" fields. Only type=command is currently supported. The "source" field is optional (raw|envelope|ai|web|unknown). Output JSON includes per-action: decision, confirm, layer, reason, argv, risk score/flags/blast_radius, busy_summary_text, taint status, and suggestions on deny. The envelope also includes session metadata and policy_sources. Raw command mode (batch) Pipe or pass a single command line that does not start with '{'. Output is human-readable by default. Use --json to get JSON output. echo "git status" | aishell-gate-policy Chain verification mode Use --audit-verify to validate the hash chain of an existing log file. The program prints a per-line report to stdout and exits without reading any command input. aishell-gate-policy --audit-verify /var/log/ag.jsonl aishell-gate-policy --audit-key audit.key --audit-verify /var/log/ag.jsonl Exit codes for --audit-verify: 0 = chain intact, 1 = tampering detected, 2 = file open or read error. PRESET MODES ------------ Presets replace the base policy layer with a named command allow/deny list. Project and user override files still apply on top of the preset. The default preset if --preset is not specified is ops_safe. read_only (aliases: readonly) Inspection-only. Read and observe commands are allowed. All write, delete, permission-change, and privilege commands are denied. Intended for audit or diagnostic sessions. Allowed examples: ls, cat, grep, find, ps, df, git status, git log, sha256sum, stat, head, tail, rg. Denied examples: rm, mv, cp, dd, mkfs, sudo, tee, chmod, chown. dev_sandbox (aliases: dev) Developer workflow. Allows common build tools, compilers, and package managers. Blocks disk destructors and privilege escalation. Write operations are still subject to writable_dirs constraints if configured. Allowed examples: ls, cat, grep, find, git, make, cmake, ninja, cc, clang, gcc, python, python3, pip, cargo, go. Denied examples: dd, mkfs, mount, umount, sudo. ops_safe (aliases: ops, default) Conservative operational profile. This is the default. Allows a small set of read-only and repo-inspection commands. Denies shells, interpreters, destructive tools, and privilege escalation. Allowed examples: ls, uname, df, ps, git status, git diff. Denied examples: rm, dd, mkfs, chmod, chown, sudo, sh, bash, zsh, python, perl, ruby, git config. danger_zone (aliases: danger) Minimal restrictions. A wildcard allow rule permits most commands. The base deny list (shells, interpreters, privilege escalation, destructive disk tools) still applies. Risk classification and confirmation levels are still enforced. Use with caution. OPTIONS ------- General options: --help / -h Show usage summary and exit. --help-policy Print the policy file format reference (rule schemas, confirm levels, session keys, evaluation order, and an example policy file) and exit. --version / -V Print version string and build info and exit. --preset Select a built-in policy bundle. Valid names: read_only (readonly), dev_sandbox (dev), ops_safe (ops, default), danger_zone (danger). --mode Override session mode detection. Values: auto, interactive, batch, daemon. Default is auto (TTY detection). --source Set the provenance label for raw or interactive input. Values: raw, envelope, ai, web, unknown. Default is raw. --json In raw command mode, output full JSON instead of human text. --deterministic Force C locale and UTC timezone for reproducible output. --debug Enable debug logging to stderr. Policy file options: --policy-base Path to the base policy override file. Default: ./aishell-gate-policy_base.json --policy-project Path to the project policy override file. Default: ./aishell-gate-policy_project.json --policy-user Path to the user policy override file. Default: ./aishell-gate-policy_user.json Audit options: --audit Append a tamper-evident JSON Lines entry per command to the given file. Each entry is SHA-256 hashed and linked to the previous entry by its hash, forming a verifiable chain. See AUDIT LOGGING. --audit-key Load a 64-byte binary key from file and switch audit chaining to HMAC-SHA256 mode. With a key, only a key-holder can forge valid chain hashes. Files shorter than 64 bytes are zero-padded. To generate a key: head -c 64 /dev/urandom > audit.key Requires --audit (or --audit-verify) to have effect. --audit-verify Validate the hash chain of an existing audit log file and exit. Prints a per-entry report to stdout. Exit codes: 0 = chain intact, 1 = chain break or tamper detected, 2 = file open/read error. Combine with --audit-key to verify an HMAC-SHA256 chain. Sandbox advisory options: --sandbox Set the sandbox mode hint for the executor. Values: none, cwd_jail, chroot, container, userns. Default is none. Note: cwd_jail is the only mode actively enforced during evaluation (path arguments are checked against the jail root). The remaining modes are advisory hints recorded in the JSON output for the executor to act on. See SANDBOX GUIDANCE. --jail-root Set the jail root path. Implies --sandbox cwd_jail. When set, path arguments for write-like commands must fall within this root. Uses a relaxed canonicalization that allows nonexistent target paths. The jail containment check requires that the canonicalized path begins with jail_root followed by '/' or end-of-string; a prefix match alone is insufficient (e.g. "/tmp/jail" does not contain "/tmp/jailbreak/x"). --limit-cpu Advisory CPU time limit hint for the executor. Default: 10. --limit-as-mb Advisory address space limit hint for the executor. Default: 1024. --limit-wall-ms Advisory wall clock limit hint for the executor. Default: 15000. Input file: A positional argument (not starting with --) is treated as the path to an input file. The file may contain either a JSON envelope or a raw command line. If no file is given, stdin is read. There is no --in flag; the file path is a positional argument only. POLICY FILES ------------ Three optional JSON override files may be layered on top of the active preset: aishell-gate-policy_base.json -- base overrides aishell-gate-policy_project.json -- project-level overrides aishell-gate-policy_user.json -- per-user overrides All three are optional. If a file is present but unparseable, the program fails closed with a nonzero exit and an error message naming the offending key or rule index. If a file is absent, defaults are used silently. Unknown top-level keys are rejected immediately to catch typos; a key like "cmd_denny" causes a hard error rather than being silently ignored. A failed config load has zero effect on the running policy. The engine takes a full snapshot of the current policy layer before applying any override; if parsing fails at any point, the snapshot is restored and the layer is left exactly as it was before the load attempt. Override file format: { "cmd_allow": [ { "pattern": "whoami", "confirm": "none", "io": "read", "reason": "..." }, { "pattern": "git status","confirm": "none", "reason": "..." } ], "cmd_deny": [ { "pattern": "curl", "reason": "no network in this env" } ], "arg_rules": [ { "cmd_pattern": "rm", "arg_glob": "-rf", "decision": "deny", "reason": "rm -rf denied" } ], "path_rules": [ { "path_glob": "/tmp/*", "decision": "allow", "reason": "temp dir ok" } ], "net_rules": [ { "host_glob": "169.254.*", "decision": "deny", "reason": "link-local denied" } ], "writable_dirs": [ "/tmp", "/home/user/projects" ] } List behavior: All rule lists (cmd_allow, cmd_deny, arg_rules, path_rules, net_rules) append to defaults by default. To replace a list entirely, set the corresponding _replace flag: { "cmd_allow_replace": true, "cmd_allow": [ ... ] } Replace flags: cmd_allow_replace, cmd_deny_replace, arg_rules_replace, path_rules_replace, net_rules_replace. Rule fields: cmd_allow entries: pattern -- command name or "command subcommand" (required) confirm -- none|plan|action|typed (optional; default none for allow) io -- read|write|mixed|net|exec|unknown (optional; informational) reason -- explanation string (optional) cmd_deny entries: pattern -- command name or "command subcommand" (required) reason -- explanation string (optional) arg_rules entries: cmd_pattern -- limit to this command (optional; omit for any command) arg_glob -- fnmatch pattern matched against each argument (required) decision -- allow|deny (optional; default deny) confirm -- none|plan|action|typed (optional) reason -- explanation string (optional) path_rules entries: cmd_pattern -- limit to this command (optional) path_glob -- fnmatch pattern matched against canonicalized path (required) decision -- allow|deny (optional; default deny) reason -- explanation string (optional) net_rules entries: cmd_pattern -- limit to this command (optional) host_glob -- fnmatch pattern matched against extracted hostname (required) port_lo -- lower port bound, inclusive (optional; 0 means any) port_hi -- upper port bound, inclusive (optional; 0 means any) decision -- allow|deny (optional; default deny) reason -- explanation string (optional) writable_dirs: Array of path strings. Advisory allowlist of directories where write commands may operate. Used by the executor and logged in audit output. Session overrides (within an override file): { "session": { "deny_ssh": true, "require_tty": false, "allow_modes": ["interactive", "batch"], "allow_uids": [1000, 1001], "deny_uids": [], "allow_gids": [], "deny_gids": [], "allow_users": ["alice", "bob"], "deny_users": [], "time_window_start": "08:00", "time_window_end": "18:00" } } time_window_start and time_window_end are UTC times in "HH:MM" format. Both keys must be present together -- either key alone is a parse error (fail closed). Time-window enforcement is enabled automatically when both keys are present; no separate "enabled" flag is required. A window that wraps midnight (start > end, e.g. "22:00" to "06:00") is supported. Omit both keys entirely to disable time-window enforcement. Maximum 16 entries per uid/gid/user list. Session enforcement runs before command rule evaluation. A session denied here is denied regardless of command allow rules. Unknown session keys are rejected with a parse error. A typo such as "deny_sssh" will not be silently ignored -- the policy file will fail to load and the evaluator will deny all commands. EVALUATION ORDER ---------------- For each command, evaluation proceeds in this order: 1. Input rejection -- immediate deny if any of the following are present: Shell metacharacters: | ; & > < ` $() ${} && || Command injection sequences: newline (\n), carriage return (\r) Non-printable C0 control bytes: 0x01-0x08, 0x0b, 0x0c, 0x0e-0x1f DEL character: 0x7f Note: tab (0x09) is treated as whitespace and is not rejected. Note: quoting characters (" ' `) are shell metacharacters and are rejected. Quote processing is not performed; quoted arguments containing spaces cannot be expressed. This is a security decision -- implementing a shell grammar subset would introduce bypass surface. 2. Command length check -- deny if longer than 4096 characters. 3. Risk classification -- compute risk score, flags, and blast radius from the command catalog and argument inspection. 4. Session enforcement -- check uid, gid, user, mode, SSH, TTY, and time-of-day constraints from all policy layers. 5. Deny rules -- cmd_deny and deny arg_rules from all layers in order. 6. Path deny rules -- for each path argument, canonicalize (allowing nonexistent targets) and check path_rules. 7. Network deny rules -- for each detected host:port in arguments, check net_rules. 8. Allow rules -- cmd_allow, allow arg_rules, allow path_rules, and allow net_rules from all layers. In default-deny mode, at least one allow rule must match the command and all path arguments. 9. Confirmation upgrade -- if risk score >= 40, 70, or 90, confirm level is raised to plan, action, or typed respectively. Escalation is strictly monotonic; the level is never lowered. 10. Taint cleared -- output marked as validated. The first deny match returns immediately. Allow rules are evaluated after all deny checks pass. A deny match in any layer cannot be overridden by an allow rule in any other layer. AI / JSON INTEGRATION --------------------- aishell-gate-policy accepts structured requests and produces deterministic JSON responses. Each action result includes: decision -- "allow" or "deny" confirm -- "none", "plan", "action", or "typed" layer -- policy layer that made the decision reason -- human-readable explanation io -- "read", "write", "mixed", "net", "exec", or "unknown" risk.score -- integer 0-100 risk.flags -- array of flag strings risk.blast_radius -- "single", "tree", "system", or "unknown" risk.summary -- short human-readable risk note busy_summary_text -- 3-5 line plain-text summary for humans or LLMs taint.tainted -- bool; false only after an ALLOW decision taint.source -- provenance label of the input argv -- parsed argument array (ALLOW only) suggestions -- allowed_commands and allowed_paths arrays (DENY only) The envelope response also includes session metadata, policy_sources, and an overall_decision field. IMPORTANT: aishell-gate-policy does not execute commands. The ALLOW output label reads "Validated command (pass to executor)" to make this explicit. Enforcement is performed by aishell-gate-exec(1) or another external wrapper. This separation is intentional: the executor has no policy logic and cannot approve or deny a command. Every decision is made by the policy engine in a separate process; the executor reads the JSON result and acts on it. EXECUTOR INTEGRATION -------------------- aishell-gate-exec(1) is the companion execution harness. It accepts a JSON plan from an AI agent, submits each action to aishell-gate-policy as a child process, interprets the JSON evaluation result, collects human confirmation where required, and calls execve() with the validated argv array. It contains no policy logic. Policy decisions are made entirely by the policy engine in a separate process. Plan input format (stdin or --plan FILE): { "goal": "human-readable description of intent", "source": "ai" | "envelope" | "raw" | "web" (default: "ai"), "strategy": "fail_fast" | "best_effort" (default: "fail_fast"), "actions": [ { "type": "command", "cmd": "git status" }, { "type": "command", "cmd": "npm test" } ] } strategy "fail_fast" -- stop on first non-zero exit; log skipped count strategy "best_effort" -- continue through all actions Executor-specific flags: --policy PATH Path to the aishell-gate-policy binary --plan FILE Read input JSON from FILE instead of stdin --eval-timeout SECS Kill the policy engine if evaluation exceeds this time --max-response-bytes N Kill the policy engine if response exceeds N bytes (default: 8 MiB; 0 = no limit) --confirm-tty PATH Single-session interactive use only. Reads 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 PATH 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 --confirm-pipe BASEPATH Secure pipe-based operator confirmation relay. 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, cmd, goal, source, risk, blast_radius, reason, and for typed confirmations the challenge code). aishell-confirm reads it, displays the full context on the operator's own terminal, reads the operator's response, and writes it back through BASEPATH.resp. ai-agent never opens any PTY device. Use with --confirm-lock for multi-session safety. Default base path: /run/aishell-gate/confirm Policy engine flags passed through directly (no -- separator needed): --preset NAME Built-in policy bundle --policy-base FILE Base policy override file --policy-project FILE Project policy override file --policy-user FILE User policy override file --jail-root PATH Restrict write-like paths to PATH and below --sandbox MODE Sandbox hint for executor All remaining policy engine flags can be forwarded using -- as a separator: aishell-gate-exec [exec-flags] -- [policy-engine-flags] Executor exit codes: 0 All actions ALLOWed, confirmed, and executed 1 One or more actions DENYed by policy 2 Human confirmation refused (or no terminal available for confirmation) 3 Policy engine process error 4 JSON parse error (input plan or policy engine response) 5 Usage or argument error 6 execve() failure after confirmed ALLOW (binary not found or exec error) Execution environment: The executor never inherits the caller's PATH or environment. Binaries are resolved against a compile-time SAFE_PATH (/usr/local/bin, /usr/bin, /bin, /usr/sbin, /sbin). Only an explicit allowlist of environment variables is propagated; all others -- including LD_PRELOAD, DYLD_INSERT_LIBRARIES, PYTHONPATH, and GIT_EXEC_PATH -- are silently dropped. Commands are run via execve() directly; no shell is invoked. Typical integration: aishell-gate-exec \ --policy ./aishell-gate-policy \ --preset ops_safe \ --audit /var/log/aishell.jsonl \ --plan request.json Remote SSH deployment with a human operator in a second session: # One-time setup on the remote host (as root): # groupadd aishell-gate # usermod -aG aishell-gate operator # usermod -aG aishell-gate ai-agent # mkdir -p /run/aishell-gate # chown root:aishell-gate /run/aishell-gate # chmod 2770 /run/aishell-gate # echo 'd /run/aishell-gate 2770 root aishell-gate -' \ # > /etc/tmpfiles.d/aishell-gate.conf # Operator's own SSH session — keep this window open: # $ aishell-confirm # creates FIFOs, arms relay # In authorized_keys forced command for ai-agent: # command="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" # When the AI's plan triggers a confirmation requirement, the full # request (cmd, goal, source, risk, reason, challenge) appears on # the operator's terminal via the FIFO relay. ai-agent never opens # any PTY device. Multiple concurrent AI sessions are serialised # through the lock; each waits its turn for the operator's attention. SANDBOX GUIDANCE ---------------- The engine emits sandbox mode hints via --sandbox and resource limit hints via --limit-cpu, --limit-as-mb, and --limit-wall-ms. These are advisory outputs for a downstream executor to act on. The engine does not enforce kernel-level containment for any mode except cwd_jail. Sandbox modes: none No sandbox hint. Default. cwd_jail Path enforcement: write-like commands must target paths within --jail-root. This is enforced during evaluation. The executor receives the jail root in the JSON output for additional enforcement if desired. chroot Advisory hint. The executor should arrange a chroot environment. container Advisory hint. The executor should run the command in a container or namespace. userns Advisory hint. The executor should use a user namespace. Resource limit hints (all advisory; the engine does not enforce them): cpu_seconds CPU time budget as_mb Address space limit in megabytes wall_ms Wall clock budget in milliseconds AUDIT LOGGING ------------- Using --audit writes one JSON object per line to the given file. The file is opened in append mode. Failure to open the file is reported to stderr but does not affect decisions or exit status. Each audit entry is tamper-evident: a SHA-256 hash is computed over the entry's content (with the entry_hash field set to a 64-zero sentinel before hashing), and each entry stores the hash of the previous entry in prev_hash. A gap in seq values or a hash mismatch indicates deleted or modified records. When --audit-key is provided, the chain uses HMAC-SHA256 instead of plain SHA-256. Only a holder of the key can forge valid chain hashes. File locking: flock(LOCK_EX) is acquired on the file descriptor before writing and released after flush, preventing interleaved entries from concurrent processes. The HMAC chain anchor and sequence counter are updated while the lock is held to prevent a forked chain from concurrent invocations on the same file. Each audit entry includes: seq -- monotonic sequence counter; gaps = missing entries session_id -- 16 hex chars, unique per process invocation ts -- ISO 8601 UTC timestamp uid, gid -- numeric session identity user -- username from passwd database host -- hostname at invocation host_id -- same as host (schema alias) server_identity -- same as host (schema alias) cwd -- working directory at invocation stdin_tty, is_ssh, mode -- session context flags has_jail, jail_root -- jail configuration if active policy_version -- AISHELL_VERSION constant policy_preset -- active preset name source -- taint source: raw|envelope|ai|web|unknown input -- raw input string (alias: command) argv -- parsed argument array (ALLOW only) decision -- "allow" or "deny" (alias: result) confirm -- confirmation level string layer -- policy layer that fired reason -- policy reason string io -- IO classification string deny_kind -- "none", "command", "arg", "path", "net", "user", or "default" deny_detail -- matched pattern or path that triggered deny risk_flags -- pipe-separated risk flag string risk_score -- integer 0-100 blast_radius -- blast radius string risk_summary -- short risk note busy_summary -- multi-line summary text prev_hash -- entry_hash of the preceding record (chain link) chain_mode -- "sha256" or "hmac-sha256" entry_hash -- SHA-256 or HMAC-SHA256 of this record's content Audit files should be protected by filesystem permissions. The engine writes to the file and flushes after each entry; no rotation is performed. To verify a log file's chain integrity: aishell-gate-policy --audit-verify /var/log/ag.jsonl To verify an HMAC-SHA256 chain (requires the original key): aishell-gate-policy --audit-key audit.key --audit-verify /var/log/ag.jsonl You can also verify a single entry manually: sed 's/"entry_hash":"[0-9a-f]\{64\}"/"entry_hash":"000...000"/' line \ | sha256sum BUILD ----- aishell-gate-policy: cc -Wall -Wextra -O2 -std=c11 aishell-gate-policy.c -o aishell-gate-policy aishell-gate-exec: cc -std=c11 -Wall -Wextra -o aishell-gate-exec aishell-gate-exec.c No external libraries are required by either binary. JSMN (JSON tokenizer) and SHA-256 are embedded in both. Requires a POSIX.1-2008 compatible system (Linux, macOS, BSDs). Optional: embed a git hash for --version output: cc -Wall -Wextra -O2 -std=c11 \ -DGIT_HASH=\"$(git rev-parse --short HEAD)\" \ aishell-gate-policy.c -o aishell-gate-policy USAGE EXAMPLES -------------- Validate a single command interactively: ./aishell-gate-policy Validate via stdin in read-only preset: echo "cat /etc/passwd" | ./aishell-gate-policy --preset read_only Validate a JSON action envelope: cat request.json | ./aishell-gate-policy --preset dev_sandbox Validate from a file with audit logging: ./aishell-gate-policy --preset ops_safe --audit /var/log/aishell.jsonl \ request.json Get full JSON output for a raw command: echo "git status" | ./aishell-gate-policy --json Validate with a cwd jail constraint: ./aishell-gate-policy --jail-root /home/user/projects Validate with debug output: ./aishell-gate-policy --debug --preset read_only Generate an HMAC key and enable authenticated audit chaining: head -c 64 /dev/urandom > audit.key ./aishell-gate-policy --audit-key audit.key --audit /var/log/ag.jsonl \ --preset ops_safe Verify an audit log's chain integrity: ./aishell-gate-policy --audit-key audit.key --audit-verify /var/log/ag.jsonl Print policy file format reference: ./aishell-gate-policy --help-policy Print version and build info: ./aishell-gate-policy --version Run a multi-action plan through the executor: echo '{ "goal": "update and test", "source": "ai", "strategy": "fail_fast", "actions": [ {"cmd": "git pull"}, {"cmd": "npm install"}, {"cmd": "npm test"} ] }' | ./aishell-gate-exec --policy ./aishell-gate-policy \ --preset dev_sandbox \ --audit /var/log/aishell.jsonl Run executor in best_effort mode (continue past failures): ./aishell-gate-exec --policy ./aishell-gate-policy \ --preset ops_safe --plan request.json \ -- --audit /var/log/aishell.jsonl EXIT STATUS ----------- aishell-gate-policy: 0 Evaluated without internal error (result may be ALLOW or DENY) 1 Internal error (out of memory, policy parse failure) 2 Usage error (unknown option, missing argument) For --audit-verify mode: 0 Chain intact -- no tampering detected 1 Chain break or tamper detected 2 File open or read error aishell-gate-exec: 0 All actions ALLOWed, confirmed, and executed 1 One or more actions DENYed by policy 2 Human confirmation refused 3 Policy engine process error 4 JSON parse error (input plan or policy engine response) 5 Usage or argument error 6 execve() failure after confirmed ALLOW DESIGN NOTES ------------ The two-component architecture -- separate policy engine and executor -- is the central security property of the system, not a structural convenience. aishell-gate-policy has no ability to execute anything. It only emits a JSON decision record. aishell-gate-exec has no policy logic whatsoever. It cannot approve or deny a command; it reads the decision from the policy engine process and acts accordingly. This separation means that a compromised executor cannot grant itself permission to run a command that the policy engine has denied, because the permission decision lives in an independent process that the executor has no ability to influence. The boundary between the two components is the JSON decision record: structured, logged, and auditable. aishell-gate-policy intentionally avoids: - Shell evaluation of any kind - Implicit execution - Hidden retries - Silent overrides - Network access - External library dependencies (JSMN and SHA-256 are embedded) Explainability is a first-class feature. Every decision includes a layer, reason, deny_kind, and suggestions. The busy_summary field provides a concise briefing for both humans and LLMs. The ALLOW output label "Validated command (pass to executor)" is intentional: this program is a policy gate, not a shell. If uncertain, the system denies and explains. This is by design. This tool is designed to reduce accidental damage from AI-generated commands and provide a tamper-evident record of every execution decision. It is not a substitute for OS-level access control, kernel sandboxing, or proper permission management. It is designed to complement those mechanisms. KNOWN LIMITATIONS ----------------- Command parsing is whitespace-only. Quoted arguments containing spaces cannot be expressed; the shell metacharacter check rejects quote characters outright. This is a deliberate security decision: implementing a shell grammar subset introduces ambiguity and bypass surface. Policy rules match against naive whitespace tokens. Network rules match argument strings, not resolved IP addresses. Rules blocking hostnames or IP ranges can be bypassed by DNS aliases, URL encoding, or HTTP redirects. Network rules provide best-effort intent capture, not strong enforcement. The JSON token limit is 4096 tokens per parse. Very large policy files or request envelopes may be rejected. The error message will indicate a parse failure; reduce the size of the input. Path rules call a relaxed canonicalization function that resolves existing path components and allows nonexistent leaf components. Symlinks within the existing portion of a path are resolved; symlinks in nonexistent components are not checked. Time-window enforcement uses hour granularity. Minutes in time_window_start and time_window_end are parsed and validated but not used in enforcement. Maximum 16 entries per uid, gid, or user list in session policy. Maximum 63 arguments per command. Maximum 8 allowed_commands and 8 allowed_paths in suggestion output. SEE ALSO -------- aishell-gate-exec(1) -- companion execution harness (aishell-gate-exec.c, v0.24) aishell-confirm(1) -- operator confirmation relay; creates FIFOs for --confirm-pipe fnmatch(3) -- glob pattern matching used by rule evaluation flock(2) -- advisory locking used by the audit subsystem and confirm lock execve(2) -- system call used by the executor for command execution sha256(3) -- hash function used for audit chain integrity VERSION ------- aishell-gate-policy 0.66.0-beta aishell-gate-exec 0.25.0-beta