|
2 | 2 |
|
3 | 3 | [](LICENSE) |
4 | 4 | [](https://github.com/AperionAI/shield/actions/workflows/release.yml) |
5 | | -[](https://github.com/AperionAI/shield/actions) |
| 5 | +[](https://github.com/AperionAI/shield/actions) |
6 | 6 | [](https://www.rust-lang.org/) |
7 | 7 | [](https://github.com/AperionAI/shield/pkgs/container/shield) |
8 | 8 | [](SECURITY.md) |
@@ -34,6 +34,86 @@ the relying party — no rewrite, no re-install. |
34 | 34 |
|
35 | 35 | --- |
36 | 36 |
|
| 37 | +## What's new in v0.8 |
| 38 | + |
| 39 | +Two strong additions that build directly on the v0.7 bypass-closing |
| 40 | +story: |
| 41 | + |
| 42 | +1. **Shell shims (`--install-shims`) — closes the non-git command |
| 43 | + bypass.** v0.7 closed the "agent reaches around MCP and lets a |
| 44 | + destructive change land in a commit" bypass with git hooks. v0.8 |
| 45 | + closes the parallel "agent reaches around MCP and runs a |
| 46 | + destructive shell command directly" bypass. One command installs |
| 47 | + tiny `/bin/sh` wrappers in `~/.aperion-shield/bin/` for **10 |
| 48 | + high-blast-radius CLIs** (`aws`, `gcloud`, `az`, `kubectl`, `helm`, |
| 49 | + `terraform`, `psql`, `mongosh`, `redis-cli`, `rm`). The user puts |
| 50 | + that dir first on `$PATH` and every invocation routes through the |
| 51 | + active shieldset before reaching the real binary. Same engine, same |
| 52 | + YAML rules, same audit JSONL stream — the shim path reuses the |
| 53 | + `shell` tool-call scope that MCP and `--check-staged` already use, |
| 54 | + so adding a rule for one surface covers all three. |
| 55 | + ```bash |
| 56 | + aperion-shield --install-shims --for aws,kubectl,terraform |
| 57 | + # next destructive call -> refused with rule + safer alternative |
| 58 | + # $ aws s3 rm --recursive s3://prod-bucket |
| 59 | + # [aperion-shield/check-cmd] APPROVAL-REQUIRED -- `aws s3 rm --recursive s3://prod-bucket` |
| 60 | + # rule : cloud.aws_s3_recursive_delete (severity=High) |
| 61 | + # reason : Bulk S3 delete -- irreversible if versioning is off. |
| 62 | + # suggest : Enable versioning, then use lifecycle rules to expire ... |
| 63 | + ``` |
| 64 | + Bypass for a single invocation: `SHIELD_SHIMS_DISABLE=1 aws ...` |
| 65 | + (env override, parity with `--no-verify` for hooks). Foreign-file |
| 66 | + collisions (you wrote your own `~/.aperion-shield/bin/aws` |
| 67 | + wrapper) are NEVER overwritten — Shield refuses the install with a |
| 68 | + non-zero exit and tells you what to do. |
| 69 | + |
| 70 | +2. **`--explain`: first-class decision transparency.** Take any |
| 71 | + tool-call descriptor and get a complete decision walkthrough: |
| 72 | + every rule that matched, every adjustment signal applied |
| 73 | + (workspace probe, decision memory, burst detector), the full |
| 74 | + severity ladder (raw → composite + points → final), the resolved |
| 75 | + decision, and the `safer_alternative`. Three output formats — |
| 76 | + `text` for terminals, `markdown` for PR review comments, `json` |
| 77 | + with a stable schema for piping into other tooling. The |
| 78 | + `--explain-force-prod` / `--explain-force-burst` flags let you |
| 79 | + answer "what would this same call decide in a different context?" |
| 80 | + without rebuilding the environment. |
| 81 | + ```bash |
| 82 | + echo '{"name":"shell","arguments":{"command":"rm -rf /"}}' \ |
| 83 | + | aperion-shield --explain --input - |
| 84 | + # ---------------------------------------------------------- |
| 85 | + # shield --explain |
| 86 | + # ──────────────── |
| 87 | + # tool : shell |
| 88 | + # call : {"command":"rm -rf /"} |
| 89 | + # |
| 90 | + # rules matched ............................. 1 |
| 91 | + # fs.recursive_delete_root Critical pts=8 |
| 92 | + # ... |
| 93 | + # decision .................................. BLOCK |
| 94 | + # rule_id : fs.recursive_delete_root |
| 95 | + # severity : Critical |
| 96 | + # reason : rm -rf on filesystem root is forbidden. |
| 97 | + # suggest : Scope to a specific subdirectory, ... |
| 98 | + ``` |
| 99 | + |
| 100 | +3. **243 tests passing** (was 192 in v0.7, 148 in v0.6, 133 in v0.5) |
| 101 | + — +51 new tests: 22 in-module + 7 end-to-end for shims (real |
| 102 | + `/bin/sh` execution against a fake real binary, foreign-file |
| 103 | + collision, bypass env, fall-through when Shield isn't on `$PATH`, |
| 104 | + `--list-shims` separation); 15 in-module + 7 end-to-end for |
| 105 | + `--explain` (text / markdown / JSON stable-schema format |
| 106 | + round-trips, force flags, legacy `tool/params` descriptor shape, |
| 107 | + missing-tool refusal). |
| 108 | + |
| 109 | +> **Heads up: HTTP/SSE MCP transport is the v0.9 headline.** The |
| 110 | +> Streamable HTTP transport (POST + SSE relay with backpressure) is a |
| 111 | +> meaty piece of work that deserves to be the lead feature of its own |
| 112 | +> release, not a half-finished breadth bump here. v0.8 is the |
| 113 | +> "bypass-closing" release; v0.9 will be the "any-transport" release. |
| 114 | +
|
| 115 | +--- |
| 116 | + |
37 | 117 | ## What's new in v0.7 |
38 | 118 |
|
39 | 119 |  |
@@ -625,6 +705,269 @@ You can layer your own rules on top via `--rules my.yaml`. |
625 | 705 |
|
626 | 706 | --- |
627 | 707 |
|
| 708 | +## Shell shims (new in v0.8) |
| 709 | + |
| 710 | +`aperion-shield --install-shims` writes tiny `/bin/sh` wrappers that |
| 711 | +route every invocation of selected CLIs through Shield's engine |
| 712 | +before the call reaches the real binary. This closes the parallel |
| 713 | +bypass surface to v0.7's git hooks: where the hooks catch destructive |
| 714 | +code landing in a commit, the shims catch destructive commands the |
| 715 | +agent runs *directly from a shell*. |
| 716 | + |
| 717 | +### Install |
| 718 | + |
| 719 | +```bash |
| 720 | +# install shims for every supported command (10 by default) |
| 721 | +aperion-shield --install-shims |
| 722 | + |
| 723 | +# OR pick a subset |
| 724 | +aperion-shield --install-shims --for aws,kubectl,terraform |
| 725 | + |
| 726 | +# OR install into a different directory (default: ~/.aperion-shield/bin/) |
| 727 | +aperion-shield --install-shims --shim-dir ~/bin/aperion |
| 728 | +``` |
| 729 | + |
| 730 | +Shield prints exactly what to add to your shell rc so the shim dir |
| 731 | +wins lookup against the system binaries: |
| 732 | + |
| 733 | +```bash |
| 734 | +zsh : echo 'export PATH="$HOME/.aperion-shield/bin:$PATH"' >> ~/.zshrc |
| 735 | +bash : echo 'export PATH="$HOME/.aperion-shield/bin:$PATH"' >> ~/.bashrc |
| 736 | +fish : fish_add_path -p '$HOME/.aperion-shield/bin' |
| 737 | +``` |
| 738 | + |
| 739 | +### Supported commands (out of the box) |
| 740 | + |
| 741 | +| Surface | Commands | |
| 742 | +|---|---| |
| 743 | +| AWS / GCP / Azure | `aws`, `gcloud`, `az` | |
| 744 | +| Kubernetes | `kubectl`, `helm` | |
| 745 | +| Infra-as-Code | `terraform` | |
| 746 | +| Databases | `psql`, `mongosh`, `redis-cli` | |
| 747 | +| Filesystem | `rm` | |
| 748 | + |
| 749 | +(You can also shim arbitrary commands — the shieldset is the source |
| 750 | +of truth for what counts as destructive. Default list just bounds |
| 751 | +what `--install-shims` instruments without a `--for` filter.) |
| 752 | + |
| 753 | +### What happens on a refused call |
| 754 | + |
| 755 | +```text |
| 756 | +$ aws s3 rm --recursive s3://prod-bucket |
| 757 | +[aperion-shield/check-cmd] APPROVAL-REQUIRED -- `aws s3 rm --recursive s3://prod-bucket` |
| 758 | + rule : cloud.aws_s3_recursive_delete (severity=High) |
| 759 | + reason : Bulk S3 delete -- irreversible if versioning is off. |
| 760 | + suggest : Enable versioning, then use lifecycle rules to expire -- never `--recursive --force`. |
| 761 | + note : approvals require an MCP-mediated invocation (this shim cannot prompt) |
| 762 | +
|
| 763 | +bypass options for a single invocation: |
| 764 | + SHIELD_SHIMS_DISABLE=1 <command> ... (env override, one-shot) |
| 765 | + aperion-shield --uninstall-shims (remove all shims) |
| 766 | +``` |
| 767 | + |
| 768 | +The real `aws` binary is **never exec'd** when Shield refuses. The |
| 769 | +exit code propagates so CI scripts notice the refusal. |
| 770 | + |
| 771 | +### Bypass / disable |
| 772 | + |
| 773 | +| Knob | Effect | |
| 774 | +|---|---| |
| 775 | +| `SHIELD_SHIMS_DISABLE=1 <cmd>` | one-shot bypass; shim execs the real binary directly | |
| 776 | +| `aperion-shield --uninstall-shims` | remove every Shield-managed shim from the dir | |
| 777 | +| `aperion-shield missing on $PATH` | shim fails open and execs the real binary (so teammates without Shield don't have their tooling broken — fail-open by design) | |
| 778 | + |
| 779 | +### Exit codes (`--check-cmd`) |
| 780 | + |
| 781 | +Same table as `--check-staged` so operators only memorise one set: |
| 782 | + |
| 783 | +| Code | Meaning | |
| 784 | +|---|---| |
| 785 | +| 0 | engine returned Allow (or shadow) → shim execs the real binary | |
| 786 | +| 1 | Block decision → shim refuses, banner on stderr | |
| 787 | +| 2 | Approval / IdentityVerification → can't prompt at shim time (no MCP inbox loop), refused with a note pointing the user at MCP-mediated invocation | |
| 788 | +| 3 | operational error (couldn't load shieldset, argv empty, ...) | |
| 789 | + |
| 790 | +### Coexistence with existing wrappers |
| 791 | + |
| 792 | +If you've hand-rolled a wrapper at `~/.aperion-shield/bin/aws` (or |
| 793 | +wherever your shim dir is) before installing Shield, `--install-shims` |
| 794 | +**refuses to overwrite it** — exits 1, leaves your file alone, and |
| 795 | +tells you what it found. Pick a different `--shim-dir`, or delete |
| 796 | +your file yourself first. |
| 797 | + |
| 798 | +### List / inspect |
| 799 | + |
| 800 | +```bash |
| 801 | +aperion-shield --list-shims |
| 802 | +# /Users/me/.aperion-shield/bin/: |
| 803 | +# [shield ] aws |
| 804 | +# [shield ] kubectl |
| 805 | +# [shield ] terraform |
| 806 | +# [foreign] my-custom-wrapper <- not Shield-managed |
| 807 | +``` |
| 808 | + |
| 809 | +### Uninstall |
| 810 | + |
| 811 | +```bash |
| 812 | +aperion-shield --uninstall-shims |
| 813 | +# REMOVED aws |
| 814 | +# REMOVED kubectl |
| 815 | +# REMOVED terraform |
| 816 | +# KEPT my-custom-wrapper (no Aperion marker; left alone) |
| 817 | +``` |
| 818 | + |
| 819 | +--- |
| 820 | + |
| 821 | +## `--explain`: walk through any decision (new in v0.8) |
| 822 | + |
| 823 | +Shield's adaptive scoring is one of its strengths and one of the |
| 824 | +most common sources of "wait, why did *that* call get gated?" |
| 825 | +operator confusion. `--explain` answers the question in one shot — |
| 826 | +which rules tripped, which adjustment signals fired, where the |
| 827 | +severity tiers actually chained, and what the safer alternative is. |
| 828 | + |
| 829 | +### Run it |
| 830 | + |
| 831 | +```bash |
| 832 | +# from a file |
| 833 | +aperion-shield --explain --input call.json |
| 834 | + |
| 835 | +# from stdin |
| 836 | +echo '{"name":"shell","arguments":{"command":"rm -rf /"}}' \ |
| 837 | + | aperion-shield --explain --input - |
| 838 | + |
| 839 | +# from a heredoc |
| 840 | +aperion-shield --explain --input - <<'EOF' |
| 841 | +{"name": "execute_sql", "arguments": {"query": "UPDATE users SET email_verified=TRUE WHERE email_verified=FALSE"}} |
| 842 | +EOF |
| 843 | +``` |
| 844 | + |
| 845 | +Accepts either descriptor shape: |
| 846 | + |
| 847 | +| Shape | Source | |
| 848 | +|---|---| |
| 849 | +| `{"name": ..., "arguments": ...}` | MCP-canonical (Cursor / Claude Code / etc.) | |
| 850 | +| `{"tool": ..., "params": ...}` | legacy / some custom tooling — still accepted | |
| 851 | + |
| 852 | +### Output formats |
| 853 | + |
| 854 | +```bash |
| 855 | +aperion-shield --explain --input call.json # text (default) |
| 856 | +aperion-shield --explain --input call.json --explain-format markdown # PR-comment friendly |
| 857 | +aperion-shield --explain --input call.json --explain-format json # stable schema |
| 858 | +``` |
| 859 | + |
| 860 | +#### text (default) |
| 861 | + |
| 862 | +```text |
| 863 | +shield --explain |
| 864 | +──────────────── |
| 865 | +tool : shell |
| 866 | +call : {"command":"rm -rf /"} |
| 867 | +
|
| 868 | +rules matched ............................. 1 |
| 869 | + fs.recursive_delete_root Critical pts=8 |
| 870 | +
|
| 871 | +adjustments applied ....................... 0 |
| 872 | + (none) |
| 873 | +
|
| 874 | +severities |
| 875 | + raw : Critical |
| 876 | + composite : High (composite_points=8) |
| 877 | + final : Critical |
| 878 | +
|
| 879 | +decision .................................. BLOCK |
| 880 | + rule_id : fs.recursive_delete_root |
| 881 | + severity : Critical |
| 882 | + reason : rm -rf on filesystem root is forbidden. |
| 883 | + suggest : Scope to a specific subdirectory, e.g. `rm -rf ./build/`. |
| 884 | +``` |
| 885 | + |
| 886 | +#### markdown — drops cleanly into a PR review comment |
| 887 | + |
| 888 | +```markdown |
| 889 | +### `aperion-shield --explain` |
| 890 | + |
| 891 | +| field | value | |
| 892 | +|---|---| |
| 893 | +| tool | `shell` | |
| 894 | +| call | `{"command":"rm -rf /"}` | |
| 895 | +| decision | **BLOCK** | |
| 896 | +| final severity | `Critical` | |
| 897 | + |
| 898 | +**Rules matched (1):** |
| 899 | + |
| 900 | +| rule | severity | points | reason | |
| 901 | +|---|---|---|---| |
| 902 | +| `fs.recursive_delete_root` | `Critical` | 8 | rm -rf on filesystem root is forbidden. | |
| 903 | + |
| 904 | +... |
| 905 | +``` |
| 906 | + |
| 907 | +#### json — stable schema for tooling |
| 908 | + |
| 909 | +```json |
| 910 | +{ |
| 911 | + "tool": "shell", |
| 912 | + "arguments": {"command": "rm -rf /"}, |
| 913 | + "rules_matched": [ |
| 914 | + { |
| 915 | + "rule_id": "fs.recursive_delete_root", |
| 916 | + "severity": "Critical", |
| 917 | + "points": 8, |
| 918 | + "reason": "rm -rf on filesystem root is forbidden.", |
| 919 | + "safer_alternative": "Scope to a specific subdirectory, ..." |
| 920 | + } |
| 921 | + ], |
| 922 | + "adjustment_signals": { |
| 923 | + "workspace_is_prod": false, |
| 924 | + "burst_in_progress": false, |
| 925 | + "fingerprint_repeatedly_approved": false, |
| 926 | + "fingerprint_recently_denied": false |
| 927 | + }, |
| 928 | + "severity_raw": "Critical", |
| 929 | + "severity_composite": "High", |
| 930 | + "severity_final": "Critical", |
| 931 | + "composite_points": 8, |
| 932 | + "decision": { |
| 933 | + "kind": "block", |
| 934 | + "rule_id": "fs.recursive_delete_root", |
| 935 | + "severity": "Critical", |
| 936 | + "reason": "rm -rf on filesystem root is forbidden.", |
| 937 | + "safer_alternative": "...", |
| 938 | + "contributing_rules": [] |
| 939 | + } |
| 940 | +} |
| 941 | +``` |
| 942 | + |
| 943 | +### What-if exploration |
| 944 | + |
| 945 | +The four `--explain-force-*` flags let you ask "what would the same |
| 946 | +call decide in a different context?" without rebuilding the actual |
| 947 | +environment: |
| 948 | + |
| 949 | +| Flag | What it does | |
| 950 | +|---|---| |
| 951 | +| `--explain-force-prod` | pretend the workspace probe said *prod* | |
| 952 | +| `--explain-force-burst` | pretend the burst detector is firing | |
| 953 | +| `--explain-force-repeatedly-approved` | demonstrate the decision-memory **demotion** path | |
| 954 | +| `--explain-force-recently-denied` | demonstrate the decision-memory **escalation** path | |
| 955 | + |
| 956 | +Use the JSON output + `--explain-force-prod` together to drive a |
| 957 | +"would this break in prod?" status check on a PR. |
| 958 | + |
| 959 | +### Exit codes (`--explain`) |
| 960 | + |
| 961 | +Mirror `--check-cmd` so the same CI plumbing works: |
| 962 | + |
| 963 | +| Code | Meaning | |
| 964 | +|---|---| |
| 965 | +| 0 | Allow or Warn | |
| 966 | +| 1 | Block | |
| 967 | +| 2 | Approval / IdentityVerification | |
| 968 | + |
| 969 | +--- |
| 970 | + |
628 | 971 | ## Identity gates (new in v0.5) |
629 | 972 |
|
630 | 973 | For the highest-blast-radius calls -- `DROP DATABASE`, force-push to a |
|
0 commit comments