"AIShell-Gate" Copyright (c) 2026 AIShell Labs LLC Winston-Salem NC USA. All Rights Reserved. Author: Sean T. Gilley Do not remove this notice. Use of this Software requires a valid license. Evaluation copies expire 30 days from download. THIS SOFTWARE IS PROVIDED BY AISHELL LABS LLC "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL AISHELL LABS LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. For full license terms see the LICENSE file or: www.aishellgate.com www.aishell.org/aishellgate aishell-gate-policy(1) ==================== NAME ---- aishell-gate-policy -- deterministic policy engine for AI-generated shell commands BETA RELEASE ------------ This is a beta release. Do not use on production systems. Best tested on a clean, isolated OS instance. Use --dry-run (on aishell-gate-exec) to validate policy configuration without executing anything. EDITIONS -------- AIShell-Gate is available in Standard and Enterprise editions. Run --version to identify your edition. Features marked [ENTERPRISE] below are not available in the standard binary; invoking them produces a clear "not available in standard edition" message rather than a silent failure. Standard edition includes: all policy evaluation, built-in presets, confirmation levels, interactive mode, audit logging (JSON Lines), jail-root enforcement, session policy, custom policy file layers, and --dump-standard-template. Enterprise adds: --dump-policy (operator overlay layers as JSON — base, project, and user overrides; the standard_policy layer and built-in catalog are not exposed) and HMAC-SHA256 keyed audit chain verification via --audit-verify. SYNOPSIS -------- aishell-gate-policy [options] [input-file] aishell-gate-policy --policy-preset ops_safe --audit-log audit.jsonl aishell-gate-policy --policy-preset read_only request.json aishell-gate-policy --audit-key audit.key --audit-verify /var/log/ag.jsonl Inspect operator overlay layers: aishell-gate-policy --policy-preset dev_sandbox --dump-policy Generate editable policy template: aishell-gate-policy --dump-standard-template \ | sed '1,/^{$/{ /^{$/!d }' > _base.json Interactive mode (educational — does not execute): $ aishell-gate-policy aishell-gate-policy [educational mode] Preset: ops_safe Default-deny: yes Net-default-deny: yes Source: human User: alice CWD: /home/alice ... policy> git status ALLOW git status Confirmation required: none (no prompt needed) Flag analysis: all flags are known-safe. To execute through the gateway: echo 'git status' | aishell-gate-exec --source human With executor (aishell-gate-exec): echo '{"goal":"check repo","actions":[{"cmd":"git status"}]}' \ | aishell-gate-exec --policy-binary ./aishell-gate-policy --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. Authority runs from lowest (built-in defaults) to highest (per-user). The policy engine scans layers highest-authority first and stops at the first layer that produces any matching rule -- allow or deny. The winning layer's decision is final; lower-authority layers are not consulted once a higher-authority layer has matched. Within the winning layer, deny rules are evaluated before allow rules, so a layer that contains both a deny and an allow matching the same command will deny. This precedence means higher-authority layers can fully override lower ones, including by producing an allow where a lower-authority layer would have denied. If organization-wide denies must be unoverridable from higher layers, place them in a layer that sits at or above every layer that could produce a conflicting allow -- typically the user layer for single-operator deployments, or by restricting which layers operators can modify in multi-tenant deployments. 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 Unix commands provides base risk scores, risk flags, and minimum confirmation floors. The catalog covers 346 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. Flag catalog For 295 of the 346 catalogued commands, individual flags are assessed with explicit dispositions. The flag catalog is a friction layer, not an access control gate: it never denies commands, only raises the confirmation level required and documents the reason. Dispositions: FLAG_ALLOW -- flag is known-safe; no confirmation escalation. Presence actively reduces friction on known-safe invocations. FLAG_WARN -- flag meaningfully changes the risk profile; confirmation raised to CONFIRM_ACTION. Every entry carries a reason in the format "what the flag does; why it matters". FLAG_DANGER -- flag is particularly dangerous; confirmation raised to CONFIRM_TYPED. Reason is the specific threat vector. FLAG_UNKNOWN -- flag not in the catalog. The system reports honestly: "risk unassessed, not evaluated as safe." Confirmation raised to CONFIRM_ACTION as a precaution. Incompleteness is safe by design. FLAG_UNKNOWN is the catch-all default. The catalog exists to reduce friction on known-safe flags and label known risks. Every gap is self-reported via FLAG_UNKNOWN rather than silently allowed or silently escalated. 51 commands have has_flag_catalog=false (interpreters, specialist compilers, trivial commands). Their exclusion is explicitly documented in the source. Catalog statistics: Commands with flag catalog: 295 Commands without: 51 (documented rationale for each) Total flag entries: 3296 FLAG_ALLOW: 1498 FLAG_WARN: 1504 FLAG_DANGER: 294 Flag assessment appears in: Interactive mode output: [safe] / [warn] / [danger] / [unassessed] labels JSON output: "flag_assessment" object with "highest", "known_risky", "unknown", and "note" fields on every action Audit log: FLAG_UNKNOWN tokens recorded for operational review 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. Loopback/localhost warnings On the ALLOW path, network targets are scanned for loopback addresses. A graded warning is emitted when a command targets localhost or a known AI inference API port. See EVALUATION ORDER step 10. 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 [ENTERPRISE] 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 builtin layer cmd_allow and cmd_deny lists with a named set. The arg_rules, path_rules, and net_rules of the builtin layer are preserved unchanged. Project and user override files still apply on top of any preset via the hierarchical evaluation order (user > project > base > builtin). Omitting --policy-preset uses the full builtin policy, which is equivalent to ops_safe. Some presets are more permissive than the default; others are more restrictive. 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. ci_build (aliases: ci) Unattended build and test pipeline. All allowed commands carry CONFIRM_NONE — confirmation prompts would hang a headless CI runner. Requires --jail-root to constrain file operations. Requires batch mode. ci_deploy (aliases: deploy) Unattended deployment pipeline. Superset of ci_build adding remote transfer, container operations, Kubernetes, Helm, Terraform, and service restart. Destructive operations (kubectl delete, helm uninstall, terraform destroy) remain denied. Requires batch mode. ci_admin (aliases: admin) CI/CD infrastructure management with mixed confirmation levels. Reads and inspections at CONFIRM_NONE; writes and destructive operations at CONFIRM_ACTION; terraform destroy and interactive shells at CONFIRM_TYPED. Intended for supervised admin sessions. 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. --policy-preset Select a built-in policy preset. Valid names: read_only (readonly), dev_sandbox (dev), ops_safe (ops, default), danger_zone (danger). Presets replace the builtin layer cmd_allow and cmd_deny lists while preserving arg_rules, path_rules, and net_rules. Some presets are more permissive than the default builtin policy (danger_zone allows most commands); some are more restrictive (read_only allows only read and observe commands). Omitting --policy-preset uses the full builtin policy, which is equivalent to ops_safe. Upper policy layers (--policy-user, --policy-project, --policy-base) can override any preset on a per-rule basis via the hierarchical evaluation order. --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. --verbose Enable verbose diagnostic logging to stderr. --dump-policy [ENTERPRISE] Print the operator-supplied overlay layers as JSON and exit. Emits the base, project, and user override layers — the rules the operator has added on top of the standard policy. The standard_policy layer and the built-in command/flag catalog are intentionally omitted; they constitute proprietary AIShell Labs intellectual property. Runs after all policy layers have been applied: aishell-gate-policy --policy-preset dev_sandbox --dump-policy aishell-gate-policy --policy-user ~/.aishell/user.json --dump-policy Output structure: { "aishell_gate_policy_dump": true, "version": "...", "preset": "...", "default_deny": true|false, "net_default_deny": true|false, "note": "standard_policy layer and catalog omitted ...", "overlay_layers": [ { "name": "base|project|user", "source": "", "cmd_allow": [ {"pattern":"...","confirm":"...","reason":"..."}, ... ], "cmd_deny": [ {"pattern":"...","reason":"..."}, ... ], "arg_rules": [ {"cmd_pattern":"...","arg_glob":"...","decision":"...","reason":"..."}, ... ], "path_rules": [ {"cmd_pattern":"...","path_glob":"...","decision":"...","reason":"..."}, ... ], "net_rules": [ {"cmd_pattern":"...","host_glob":"...","decision":"...","reason":"..."}, ... ] }, ... ] } Empty overlay layers are omitted. When running with preset defaults and no user policy files, overlay_layers contains a note explaining that all policy derives from standard_policy and the active preset. To inspect how the engine evaluates a specific command — including which rule and layer matched — use interactive mode or pipe a command to the policy engine with --json. Those interfaces show the full per-command decision record without exposing catalog internals. Intended for verifying that override files were applied correctly and for security review of operator-added rules. --dump-standard-template Emit the builtin policy layer as a ready-to-edit JSON override file and exit. The output is valid JSON (with a plain-text header block before the opening brace explaining the schema). Strip the header to produce a clean file: aishell-gate-policy --dump-standard-template \ | sed '1,/^{$/{ /^{$/!d }' > _base.json Combine with --policy-preset to template any preset: aishell-gate-policy --policy-preset read_only --dump-standard-template \ | sed '1,/^{$/{ /^{$/!d }' > read_only_base.json The output file may be used directly as a --policy-base, --policy-project, or --policy-user override file without any reformatting. All rule arrays from the builtin layer are included (cmd_allow, cmd_deny, arg_rules, path_rules, net_rules). The header documents the full schema including optional keys and the _replace flags. Intended as a starting point for custom policy authoring. Rather than writing override rules from scratch, operators can dump the builtin layer, edit the relevant entries, and load the result as an override. --test-plan Read a JSON test suite file and evaluate each test case against the active policy. Reports PASS or FAIL per case. Exits 0 if all tests pass, 1 if any fail, 2 on file/parse error. Test file format: { "tests": [ { "cmd": "git status", "expected": "allow", "label": "git ok" }, { "cmd": "rm -rf /", "expected": "deny", "label": "rm deny" }, { "cmd": "make", "expected": "allow", "preset": "dev_sandbox" } ] } Fields per test case: cmd -- command to evaluate (required) expected -- "allow" or "deny" (required) label -- human-readable description for output (optional) preset -- override --policy-preset for this test only (optional) source -- override --source for this test only (optional; not yet wired) Output format: PASS [0] git ok FAIL [1] rm deny expected: deny got: allow reason: (see policy log) [test-plan] Results: 1/2 passed (1 FAILED) Suitable for committing alongside policy files and running in CI. A policy change that breaks an expected allow/deny is caught before deployment. Combine with any preset and policy layer flags: aishell-gate-policy --policy-preset ops_safe --test-plan tests.json aishell-gate-policy --policy-preset dev_sandbox --test-plan dev_tests.json --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-log 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. Note: there is no default path — audit logging is disabled unless this flag is given explicitly. This log uses a different internal format (entry_hash field) from the exec audit log (chain_hmac field); never point both programs at the same file. --audit-key Load a 64-byte raw 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 && chmod 640 audit.key WARNING: the exec audit key file uses a different format (64 ASCII hex characters). The two key files are NOT interchangeable. Requires --audit-log (or --audit-verify) to have effect. --audit-verify [ENTERPRISE] 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. Verifies policy audit logs only; do not pass an exec audit log here. 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. Valid root keys (14 total): session, cmd_allow, cmd_allow_replace, cmd_deny, cmd_deny_replace, arg_rules, arg_rules_replace, path_rules, path_rules_replace, net_rules, net_rules_replace, writable_dirs, writable_dirs_replace, net_default_deny 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. Stack-level boolean flags: net_default_deny: true|false (optional; default true for ops_safe, read_only, dev_sandbox; default false for CI presets) When true, any command with a detected network target (URL, host:port, parseable address) must have an explicit net_rules allow entry for every target. Targets with no matching allow rule are denied, mirroring the command default-deny model. When false, network targets are subject only to explicit deny rules (cloud metadata, CGNAT, etc.). Set in a policy file to override the preset default: { "net_default_deny": false } -- opt out for CI pipelines { "net_default_deny": true } -- enforce explicitly The built-in cloud metadata deny rules (169.254.*, metadata.google.internal, 168.63.129.16, 192.0.0.192, 100.*, ::1) are unconditional and operate independently of net_default_deny. 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) net_rules -- user policy file loopback warning: When the user override file (--policy-user) is loaded, net_rules are scanned for DECISION_ALLOW rules targeting loopback addresses (localhost, 127.x.x.x, ::1, 0.0.0.0, host.docker.internal). A warning is printed to stderr at load time. If the allowed port matches a known AI inference API port, the service name is included. Informational only -- does not block or alter the rule. 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 the highest-authority layer that produces any match. Layer scanning stops at that layer; lower-authority layers are not consulted. In default-deny mode, at least one allow rule in the winning layer must match the command and all path arguments. Net default-deny gate: if net_default_deny=true (preset default for ops_safe, read_only, dev_sandbox), every detected network target must match a net_rules allow entry. Targets with no match are denied as "default deny: no policy allow rule matched" -- same as a command with no allow rule. This check is independent of default_deny so CI presets can allow unrestricted network access while still enforcing the command allow-list. 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. Loopback/localhost warning -- if the command targets a loopback address (localhost, 127.x.x.x, ::1, 0.0.0.0, or host.docker.internal), a graded warning is written to the human_extra field and included in both raw and JSON output. Three severity tiers: TIER 1 Known AI inference API port on loopback (Ollama, LM Studio, llama.cpp, Gradio, etc.): service named, AI self-access risk called out. When source=ai, the confirm level is escalated to CONFIRM_ACTION minimum. TIER 2 Loopback host, unknown port: generic localhost warning. TIER 3 Local alias (0.0.0.0, host.docker.internal): may reach local services. Only the highest-tier warning per evaluation is emitted. This step runs on the ALLOW path only, after flag_catalog_check. 11. Taint cleared -- output marked as validated. Layers are scanned highest-authority first. Within each layer, deny rules are evaluated before allow rules; a deny match in the winning layer returns immediately. Once a layer produces any match (allow or deny), lower-authority layers are not consulted. Consequently, a deny rule in a lower-authority layer is not overridden by a higher-layer allow -- it is simply not read. To enforce an unoverridable deny across a multi-layer stack, place the deny in a layer at or above every layer that could produce a conflicting allow. 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. As of v1.02 it also carries a protocol version block as the first field: "protocol": {"name": "aishell-gate-policy-response", "version": "1.0"} The name and version allow callers to detect schema version. Major version bumps signal breaking changes; minor bumps are additive only. 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 (summary): --policy-binary PATH Path to this binary (aishell-gate-policy) --plan FILE Read input JSON plan from FILE instead of stdin --eval-timeout SECS Kill this process if evaluation exceeds SECS --max-response-bytes N Kill this process if its response exceeds N bytes --confirm-tty / --confirm-lock / --confirm-pipe Human confirmation routing (single-session TTY, multi-session lock, or FIFO relay) For the full executor flag reference, default values, and confirmation deployment guidance see aishell-gate-exec(1). FLAG SEPARATION: aishell-gate-exec and aishell-gate-policy have distinct flag sets. Do not pass exec flags to this binary or policy flags to exec directly. In particular, --audit-log on both binaries writes to DIFFERENT log files with incompatible formats; never point them at the same path. Policy engine flags passed through directly (no -- separator needed): --policy-preset NAME Built-in policy preset --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-binary ./aishell-gate-policy \ --policy-preset ops_safe \ --audit-log ./exec.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-binary /usr/bin/aishell-gate-policy \ # --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 ------------- TWO-LOG ARCHITECTURE aishell-gate uses two separate, independent audit logs: policy log: written by this binary via --audit-log; format identified by the "entry_hash" field (sentinel-substitution SHA-256 chain) exec log: written by aishell-gate-exec; format identified by the "chain_hmac" field (HMAC-SHA256 chain) These logs use incompatible internal formats and are verified by different binaries: aishell-gate-policy --audit-verify FILE (policy logs only) aishell-gate-exec --audit-verify FILE (exec logs only) Never point both programs at the same audit log file. The policy engine has NO default audit log path and NO audit-related environment variables. Audit logging is disabled unless --audit-log is given explicitly. Using --audit-log 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 alongside --audit-log, 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 is distributed as pre-compiled binaries. Source code is available to organisations conducting a security review or audit. Contact info@aishell.org to request access. aishell-gate-policy (standard): cc -std=c11 -Wall -Wextra -O2 -o aishell-gate-policy aishell-gate-policy-110-1.c aishell-gate-policy (enterprise): cc -std=c11 -Wall -Wextra -O2 -DENTERPRISE -o aishell-gate-policy aishell-gate-policy-110-1.c aishell-gate-exec (standard): cc -std=c11 -Wall -Wextra -O2 -o aishell-gate-exec aishell-gate-exec-41-1.c aishell-gate-exec (enterprise): cc -std=c11 -Wall -Wextra -O2 -DENTERPRISE -o aishell-gate-exec aishell-gate-exec-41-1.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). USAGE EXAMPLES -------------- Validate a single command interactively: ./aishell-gate-policy Validate via stdin in read-only preset: echo "cat /etc/passwd" | ./aishell-gate-policy --policy-preset read_only Validate a JSON action envelope: cat request.json | ./aishell-gate-policy --policy-preset dev_sandbox Validate from a file with audit logging: ./aishell-gate-policy --policy-preset ops_safe --audit-log ./policy.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 --verbose --policy-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-log ./policy.jsonl \ --policy-preset ops_safe Verify an audit log's chain integrity: ./aishell-gate-policy --audit-key audit.key --audit-verify ./policy.jsonl Run a policy test suite: ./aishell-gate-policy --policy-preset ops_safe --test-plan tests/policy.json ./aishell-gate-policy --policy-preset dev_sandbox --test-plan tests/dev.json 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-binary ./aishell-gate-policy \ --policy-preset dev_sandbox \ --audit-log ./exec.jsonl Run executor in best_effort mode (continue past failures): ./aishell-gate-exec --policy-binary ./aishell-gate-policy \ --policy-preset ops_safe --plan request.json \ -- --audit-log ./exec.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 exit codes are documented in aishell-gate-exec(1). DESIGN NOTES ------------ AIShell Labs LLC — we are an integrity company. 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. The two binaries serve two user populations simultaneously: AI agents -- the original use case. An AI generates a JSON plan; the executor gates it through the policy engine, collects human confirmation where required, and executes allowed commands. Human operators -- aishell-gate-policy interactive mode is a purely educational surface: it assesses commands, explains every flag with its documented reasoning, and emits a gateway echo line. It never executes. aishell-gate-exec interactive mode is the execution surface: it enforces confirmation gates and runs commands. Together they support a sysadmin workflow where learning and execution are distinct acts. Integrity by design: The flag catalog embodies the project's integrity principle. Every FLAG_WARN entry carries "what the flag does; why it matters." Every FLAG_UNKNOWN states "risk unassessed, not evaluated as safe" -- the system never implies it assessed a flag and found it safe when it did not. The 49 commands without a flag catalog have explicit documented rationale for their exclusion. No entry was added or omitted silently. 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 flag assessment. The flag catalog notes tell the operator exactly why a flag raises the confirmation requirement. If uncertain, the system denies and explains. This is by design. This tool reduces accidental damage from AI-generated commands, supports human operator learning, and provides 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 complements them. 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; see aishell-gate-exec(1) aishell-gate-mcp(1) -- MCP server; exposes evaluate_plan and execute_plan tools 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 ------- Run each component with --version for current version information: aishell-gate-policy --version aishell-gate-exec --version aishell-confirm --version aishell-gate --version REVISION HISTORY (manpage) -------------------------- v107 Policy layer precedence corrected throughout. Earlier versions described the allow-phase merge as "all layers scanned; OR of all matches; MAX of all matching confirms." The actual engine scans layers highest-authority first and stops at the first layer that produces any match. The description in "Policies", evaluation step 8, and the trailing summary paragraph have been rewritten to match this behaviour. No code change; documentation only. v106 Previous release.