From 4edca81b700ad0194720bdda5263d09c93f6ae58 Mon Sep 17 00:00:00 2001 From: O6lvl4 Date: Sun, 27 Sep 2026 19:11:45 +0900 Subject: [PATCH 1/2] Rewrite the README around a real run, and name the resolver under --no-net as the network Co-Authored-By: Claude Opus 5.5 (1M context) --- README.ja.md | 273 +++++++++++++++++-------------------- README.md | 294 +++++++++++++++++----------------------- native/denial_advice.rs | 3 + 3 files changed, 251 insertions(+), 319 deletions(-) diff --git a/README.ja.md b/README.ja.md index 36b68cc..cbfc89f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -5,12 +5,8 @@

Porta

- 実際に与えた権限だけでエージェントを動かす。
- 今使っている CLI エージェントには OS レベルの制限を、これから作るエージェントには WASM ランタイムを。 -

- -

- Almide + Wasmtime · Docker 不要 + コマンドも AI エージェントも、与えた権限だけで動かす。
+ macOS と Linux のカーネルで強制。コンテナもデーモンも要らない。

@@ -19,187 +15,162 @@ --- -## Porta とは - -AI エージェントにコマンドを実行させることは、鍵もトークンもネットワークも、 -その手の届く範囲に置くこと。Porta はコマンドを実行しつつ、OS カーネルに止め -させます —— 書き込み・読み取り・通信・ソケットを macOS は Seatbelt、Linux は -Landlock+seccomp で強制。ラッパーでもお願いでもなく、OS の境界そのもの。 -カーネルが表現できない制限は、緩めず実行を拒否します(**fail-closed**)。 - -主張ではなく証拠で。公開の脱獄[コーパス](docs/benchmarks/escapes.md)は -**macOS 22/22・Linux 28/28(user namespace を許さないホストでは 24/24)、突破ゼロ**、負けた行も残す([脅威モデル](docs/threat-model.md))。 -同じコーパスを srt・Fence・nono・landrun でも走らせた[比較表](docs/benchmarks/competitors.md)も、porta が負ける項目込みで公開。 -1 コマンドあたりの[コスト](docs/benchmarks/overhead.md)も公開:porta は macOS で約 16 ms、Linux で 2〜6 ms。 - ```text -$ porta run sh -v ./work --read-policy strict --timeout 5 -- … - ./work/out.txt に書けた 正規の作業は通る - SSH秘密鍵を読む → 拒否 - 作業ディレクトリ外に書く → 拒否 - # ハングは締切で強制終了 → 終了コード 124 -``` - -## インストール - -```bash -curl -fsSL https://raw.githubusercontent.com/almide/porta/main/scripts/install.sh | bash -``` - -macOS(Apple silicon)と Linux(x86-64 / arm64)向けの単一バイナリ。公開済み -SHA-256 で検証してから導入します。`cosign` があればリリースの Sigstore 署名も検証します -([リリースの検証](docs/cli.md#verifying-a-release))。[Almide](https://github.com/almide/almide) -があれば `go install` と同じ感覚で入れられます(Intel Mac やその他の環境はこちら)。 - -```bash -almide install github.com/almide/porta --branch main -``` - -`main` が最新リリースです。`--tag v0.6.6` で版を固定できます。ソースからビルドして -`~/.local/bin` に入れるので数分かかります(Rust 側で Wasmtime をコンパイルするため)。手でビルドする方法は -[CLI リファレンス](docs/cli.md#build-from-source)。`porta run` はネイティブ -コマンドでも `.wasm` でも取れるので、WASI にコンパイルできるものは何でも動きます。 - -Debian / Ubuntu 向けには各リリースに `.deb`(amd64・arm64)も付きます。 -`sudo apt install ./porta__amd64.deb` で `/usr/bin/porta` に入り、 -Ubuntu 23.10 以降では porta がコマンドごとに namespace を持てるようにする -AppArmor プロファイルも読み込みます。ほかの方法で入れた場合は `sudo porta setup` -で同じ状態にできます(root 所有の `/usr/local/bin/porta` にコピーし、そのコピー -だけにプロファイルを入れます)。 - -GitHub Actions では action が同じ検証つきでリリースを入れ、Ubuntu ランナーでは -ランナーが本来与えない user namespace を porta にだけ許す AppArmor プロファイルも -読み込みます: - -```yaml -- uses: almide/porta@v0.6.12 -- run: porta run ./scripts/untrusted-step.sh -v . +$ porta run sh -v ./work --no-net -- -c 'echo ok > out.txt; cat ~/.ssh/id_ed25519; echo x > ~/elsewhere.txt; curl https://example.com' +cat: /Users/me/.ssh/id_ed25519: Operation not permitted +sh: /Users/me/elsewhere.txt: Operation not permitted +curl: (6) Could not resolve host: example.com +[porta] the sandbox refused this run 3 times; what each would have needed: + file-read-data /Users/me/.ssh/id_ed25519 + → closed by the preset or --deny-read; no mount reopens it + file-write-create /Users/me/elsewhere.txt + → -v /Users/me + … ``` -## こう使う - -`--` の前は porta のオプション、後ろはコマンドの引数です。 +`out.txt` は書けた。鍵も、外のファイルも、ネットワークも届かなかった。 -### すでに使っている AI エージェントを制限する +Porta は 1 つのプロセスと、そこから起動されるすべてを包む。書き込みはマウントした +ディレクトリだけ、認証情報の置き場所は読めず、ネットワークは指定したポートやホストに +絞られ、CPU・メモリ・プロセス数・時間には上限がかかる。強制するのは macOS では +Seatbelt、Linux では Landlock・seccomp・namespace で、お願いベースのラッパーではない。 +カーネルが表現できないルールは、弱めて通すのではなく実行を拒否する。 -エージェントは依存を入れ、テストを走らせ、ファイルを編集する —— 鍵のあるマシンで。 -1つのディレクトリと1つのホストだけ与え、残りは閉じる。 +## インストール ```bash -cd project -porta init claude # または porta init codex -porta up -- -p "main.rs のバグを直して" +curl -fsSL https://raw.githubusercontent.com/almide/porta/main/scripts/install.sh | bash ``` -`claude` はそのまま動きますが、書けるのはプロジェクトと自身の `~/.claude` だけ。 -そこでも settings・commands・グローバルな `CLAUDE.md` は書けないので、ある -セッションが次のセッションにフックを仕込むことはできません。`~/.ssh`・`~/.aws`・ -Keychain は読めません。レシピは必要なものを実測したコメント付きの `porta.toml` -です。macOS では Keychain を閉じているため、ログインは `ANTHROPIC_API_KEY` か -`CLAUDE_CODE_OAUTH_TOKEN`(`claude setup-token` で発行)で渡します。Docker も -イメージも、エージェント本体の変更も不要。`--snapshot` を付ければ、終わったあとに -セッションが変えたものを一覧し、`porta rollback --yes` で元に戻せます。 +| 環境 | 方法 | +|---|---| +| macOS (Apple silicon)、Linux (x86-64, arm64) | 上のスクリプト。SHA-256 を検証し、`cosign` があれば Sigstore 署名も検証 | +| Debian、Ubuntu | [リリース](https://github.com/almide/porta/releases)の `sudo apt install ./porta__amd64.deb`。Ubuntu 23.10 以降では namespace に必要な AppArmor プロファイルも読み込む | +| それ以外の方法で入れた Ubuntu | 一度だけ `sudo porta setup` で同じプロファイルを入れる | +| GitHub Actions | `- uses: almide/porta@v0.6.16` の後、どのステップでも `porta run …` | +| その他 (Intel Mac など) | `almide install github.com/almide/porta --branch main` でソースからビルド | -### ツールに API を1本だけ許し、全試行を記録する +すべてのリリースは署名とビルド来歴付き — +[リリースの検証](docs/cli.md#verifying-a-release)。 -ある1つのエンドポイントだけ要る。egress を porta のプロキシに通し、記録を残す。 +## 今使っているコーディングエージェントを閉じ込める ```bash -porta run ./agent --proxy-allow 'api.example.com' --proxy-audit egress.jsonl -v ./work +cd my-project +porta init claude # または: porta init codex +porta up -- -p "Fix the failing test" ``` -届くのは `api.example.com` だけ。直接 TCP・UDP・Unix ソケットの egress は拒否、 -クラウドメタデータやリンクローカルに解決される名前も拒否、決定は全部 -`egress.jsonl` に残ります。 - -### 素性の知れないコマンドを、マシンを預けずに試す +`porta init` は、そのエージェントに必要な分だけを測った、コメント付きの `porta.toml` +を書く。プロジェクトとエージェント自身のディレクトリは書き込み可、そこにある設定・ +フック・グローバル指示は書き込み不可 — 1 つのセッションが次のセッションに何かを +仕込めないように — そして API キーやトークンは名前で渡す。macOS では Keychain が +閉じているので、ログインは `ANTHROPIC_API_KEY` か `CLAUDE_CODE_OAUTH_TOKEN` +(`claude setup-token`) で渡す。 -実行前にポリシーを確認してから、隔離して、締切と資源上限付きで走らせる。上限は -カーネルが、起動した子プロセス全部に強制します。 +他のコマンドもフラグで同じように動く: ```bash -porta explain ./sketchy-installer -v ./sandbox --allow-net github.com:443 # ポリシー確認、実行はしない -porta run ./sketchy-installer -v ./sandbox --allow-net github.com:443 \ - --timeout 60 --max-cpu 30 --max-procs 500 --max-file-size 200 +porta run ./installer -v ./sandbox --allow-net '*:443' --timeout 120 --max-procs 500 ``` -ハングは締切で強制終了、CPU 焼きは SIGXCPU で終わり、fork 爆弾は fork できず、 -ファイルは上限で止まる。`--max-memory-mb 512` は実行全体の実メモリの上限で、 -Linux では cgroup v2(systemd のユーザーセッションが必要、無ければフラグを拒否)、 -macOS では監視側が上限到達時に実行を終わらせます。 +`--` より前は porta の、後ろはコマンドの引数。 -### 信頼できない/生成された WASM を動かす +## 何を止めるか -ユーザー投稿やモデル生成のコードを、ホストの FS もネットワークもなし、燃料・ -メモリ・時間を縛って実行。core module でも WASI 0.2 / 0.3 コンポーネントでも同じ検査: -宣言された import ごとに、与えた能力が要ります。 +| | macOS | Linux | +|---|---|---| +| **書き込み** | `-v` のマウント、`/tmp`、`/dev` だけ | 同じ | +| **書き込み可のマウントの中** | `.git/hooks`、`.git/config`、`.envrc`、シェルの rc、エージェントの設定などは書けず、リネームで退かすこともできない | 同じ (user namespace が必要 — 下記) | +| **読み取り** | 認証情報以外すべて: `~/.ssh`、`~/.aws`、`~/.config/gh`、クラウドやレジストリのトークン、Keychain、ブラウザのプロファイルは閉じる。`--read-policy strict` で読み取りもマウント内に限定 | 同じ (Keychain を除く) | +| **認証情報のソケット** | SSH agent、gpg-agent、`docker.sock` は `--allow-unix` なしでは拒否 | 同じ | +| **ネットワーク** | 開放、ポート単位の TCP (`--allow-net`)、porta のプロキシ経由のホスト単位 (`--proxy-allow`)、遮断 (`--no-net`) | 同じ。ポート指定時は UDP も閉じる | +| **他のプロセス** | 引数や環境変数は読めない。`open(1)` / Launch Services も閉じる | 見えない: 専用の PID namespace | +| **リソース** | `--timeout`、`--max-cpu`、`--max-procs`、`--max-file-size`、`--max-memory-mb` | 同じ (メモリは cgroup v2) | +| **環境変数** | 空から始まる。`PATH`、`HOME`、ロケール、端末だけが渡り、他は `-e` / `--env-pass` で指定したものだけ | 同じ | -```bash -porta run plugin.wasm --profile worker --step-limit 5000000 --max-memory 256 -``` +何を閉じるかはコードに書いたリストではなくプリセット — +[`native/presets/default.toml`](native/presets/default.toml)、`porta explain` で表示 — +で、`--deny-read`、`--protect`、`--deny-unix` で足し、`--preset ` で置き換える。 +理由つきの全体表は [enforcement](docs/enforcement.md) に。 -### 判断ループが WASM のエージェントを作る - -`porta run` は他人が書いたエージェントを制限します。`porta agent` はループ自体が -WASM のものを動かす —— ループも各ツールも別インスタンスで、環境もディレクトリも -継承せず、モデルの資格情報はホストに留まり、クラッシュしても完了済みの書き込みを -繰り返さずに再開します。 +## 拒否されたとき ```bash -porta agent agent.toml --record run.jsonl -- "ツールを使って 20 と 22 を足して" -porta agent-resume agent.toml run.jsonl # 完了済みの書き込みは繰り返さない -porta agent-journal run.jsonl # 読み取り専用メタデータ、コードは読み込まない +porta explain ./tool -v ./work --allow-net '*:443' # ポリシーを文章で。何も実行しない +porta run ./tool -v ./work --why # 実行後: 拒否ごとに必要だったフラグ ``` -詳しくは[作るエージェント](docs/agent-runtime.md)、ゲストが回避できない -[完了チェック](docs/completion-checks.md)、WASM をレビュー済み SHA-256 に縛る -[アーティファクトピン](docs/artifact-pins.md)。 - -### 設定をプロジェクト設定として残す - -```bash -porta init native mytool # 編集用の porta.toml を生成 -porta up -- --some-flag +```text +[porta] the sandbox refused this run 2 times; what each would have needed: + file-write-data /home/me/notes/out.txt + → -v /home/me/notes + network-outbound remote:*:80 + → --allow-net '*:80' + to run again with those granted: + porta run ./tool -v ./work -v /home/me/notes --allow-net '*:80' ``` -うまくいった起動をそのまま書き出すこともできます: `porta explain claude … --save porta.toml`。 - -## 制限が実際に止める様子を見る +macOS では失敗した実行のあと、カーネルの拒否ログからこれを出す。Linux では `--why` +がサンドボックスの外から `strace` で実行を追う。 -効いている制限は目に見えないので、失敗するところを見ます。 +## 変更を元に戻す ```bash -porta run curl -- https://example.com # 既定ではネットワークは開いている -porta run curl --allow-net '*:443' -- https://example.com # HTTPS 許可 → 通る -porta run curl --allow-net '*:80' -- https://example.com # → 終了 7、443 は拒否 +porta run ./agent -v ./project --snapshot # 先にマウントを複製し、終了後に変更点を一覧 +porta rollback --yes # 元に戻す ``` -拒否された実行は当て推量を残しません。非ゼロ終了のあと、porta はカーネルの拒否 -記録を読み、どのフラグが要ったかを出します(macOS は失敗のたびに、Linux は -`--why` を付けると `strace` で実行を追って)。終了コードで「失敗したコマンド」と「そもそも走らなかった」を -区別できます —— [CLI リファレンス](docs/cli.md#when-a-run-is-refused)。 - -## 証拠 - -公開する数値は生レポート・入力ハッシュ・再採点の監査を必ず残し、CI がその監査を -回します。Porta に有利でない結果も同じ場所に公開します。 - -- [脱獄コーパス](docs/benchmarks/escapes.md) —— 既知の脱獄をバイナリに当て、 - 何が抜けたかで採点。負けた行も掲載。 -- [起動とメモリ](docs/benchmarks/startup-and-memory.md) —— 比較の限界を明記した - 固定応答の計測。 -- [封じ込めと回復](docs/benchmarks/containment-evaluation.md) —— 敵対的入力下。 - 5シナリオ中1つだけが差を分け、封じ込めはタスク完了を犠牲にした。 -- [実タスク品質](docs/benchmarks/README.md) —— 一般的な品質優位は示せておらず、 - 共有計算の追試は「勝ち」として公開せず却下した。 +APFS ではクローンなので、複製はほぼタダ。スナップショットはどのマウントの外にも +置くので、コマンドが自分の取り消し手段を書き換えることはできない。 + +## 他のツールとの比較 + +同じ[脱出コーパス](docs/benchmarks/escapes.md) — 実際の抜け道を、何が通ったかで採点 — +を 5 つのツールでそれぞれの既定のまま実行した +([全比較](docs/benchmarks/competitors.md)、負けた行も含む): + +| | porta | [srt](https://github.com/anthropics/sandbox-runtime) | [Fence](https://github.com/fencesandbox/fence) | [nono](https://github.com/nolabs-ai/nono) | [landrun](https://github.com/Zouuup/landrun) | +|---|---|---|---|---|---| +| macOS: 試行 / 脱出 | **22 / 0** | 17 / 5 | 14 / 5 | 13 / 5 | — | +| Linux: 試行 / 脱出 | **28 / 0** | 23 / 2 | 21 / 3 | 24 / 5 | 22 / 7 | + +他にあって porta にないもの: srt と Fence は既定でネットワークを閉じる。Fence は +コマンドを名前でフィルタする。nono は実行中に追加の権限を求められる。porta の +オーバーヘッドは macOS で約 16 ms、Linux で 2–6 ms +([overhead](docs/benchmarks/overhead.md))。 + +## 制限 + +- **ネットワークは既定で開いている。** `--no-net`、`--allow-net`、`--proxy-allow` + で閉じる。 +- **Linux では一部の保護に user namespace が必要** — マウント内のフック、認証情報の + ソケット、他プロセスの隠蔽。Ubuntu 23.10 以降は `.deb` か `sudo porta setup` が + プロファイルを入れるまで制限される。無い場合も porta は動き、何が開いたままかを言う。 +- **カーネルは共有。** VM ではなくプロセスのサンドボックスなので、カーネルの脆弱性は + 対象外。何を守り何を守らないかは[脅威モデル](docs/threat-model.md)に。 +- **テストは作者によるもの。** コーパス、ファザー、モンキーテストは公開され CI で + 回っているが、第三者による監査はまだない。 + +## その他の機能 + +- **アンビエント権限のない WASM。** `porta run plugin.wasm --profile worker` は + コアモジュールや WASI 0.2/0.3 コンポーネントを、ホストのファイルシステムも + ネットワークもなし、fuel・メモリ・時間に上限をつけて実行する。import はすべて + 付与した capability が必要。 +- **ループが WASM のエージェント。** `porta agent agent.toml` は判断ループと各ツールを + 別インスタンスで動かし、モデルの認証情報はホストに置き、クラッシュした実行を + 完了済みの書き込みを繰り返さずに再開する ([agent runtime](docs/agent-runtime.md))。 +- **1 つの API とログ。** `--proxy-allow api.example.com --proxy-audit egress.jsonl` + はそのホストだけを通し、すべての判断を記録する。 ## ドキュメント -- [CLI リファレンス](docs/cli.md) —— 全コマンド・フラグ・`porta.toml` キー・終了コード。 -- [強制の中身](docs/enforcement.md) —— macOS と Linux が実際に何を止めるか。 -- [脅威モデル](docs/threat-model.md) —— 何を守り、何を守らないか。 -- [アーキテクチャ](docs/architecture.md) —— ポリシーは Almide、強制は Rust。 - -## ライセンス +- [CLI リファレンス](docs/cli.md) — すべてのコマンド、フラグ、`porta.toml` のキー、終了コード +- [Enforcement](docs/enforcement.md) — 各プラットフォームが何をどう止めるか +- [脅威モデル](docs/threat-model.md) — 何を守り、何を守らないか +- [ベンチマーク](docs/benchmarks/README.md) — 脱出コーパス、比較、オーバーヘッド +- [アーキテクチャ](docs/architecture.md) — ポリシーは Almide が決め、Rust が強制する -Apache-2.0 +[Almide](https://github.com/almide/almide) と [Wasmtime](https://wasmtime.dev) で +作られている。Apache-2.0。 diff --git a/README.md b/README.md index 1578dc4..c05fbff 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,8 @@

Porta

- Run an agent with the permissions you actually granted it.
- OS-enforced limits for the CLI agents you already use, and a WASM runtime for the ones you build. -

- -

- Almide + Wasmtime · No Docker required + Run a command — or an AI agent — with only what you granted it,
+ enforced by the kernel on macOS and Linux. No container, no daemon.

@@ -19,204 +15,166 @@ --- -## What Porta is - -Letting an AI agent run commands on your machine puts your keys, tokens and -network within its reach. Porta runs the command and lets the OS kernel stop -it — writes, reads, network and sockets enforced by Seatbelt on macOS or -Landlock + seccomp on Linux, not by a wrapper or a prompt. A restriction the -kernel cannot express refuses the run rather than weakening it (**fail-closed**). - -Shown, not claimed: a published jailbreak -[corpus](docs/benchmarks/escapes.md) holds 22/22 on macOS and 28/28 on Linux (24/24 on hosts that refuse -unprivileged user namespaces), -zero escapes, losing rows kept in ([threat model](docs/threat-model.md)). -The same corpus run under srt, Fence, nono and landrun is -[published beside it](docs/benchmarks/competitors.md), including where porta loses, -and so is [what each costs a command](docs/benchmarks/overhead.md): about -16 ms under porta on macOS and 2 to 6 ms on Linux. - ```text -$ porta run sh -v ./work --read-policy strict --timeout 5 -- … - wrote ./work/out.txt legitimate work goes through - read the SSH key → refused - write outside ./work → refused - # a hung command is killed at the deadline → exit 124 -``` - -## Install - -```bash -curl -fsSL https://raw.githubusercontent.com/almide/porta/main/scripts/install.sh | bash +$ porta run sh -v ./work --no-net -- -c 'echo ok > out.txt; cat ~/.ssh/id_ed25519; echo x > ~/elsewhere.txt; curl https://example.com' +cat: /Users/me/.ssh/id_ed25519: Operation not permitted +sh: /Users/me/elsewhere.txt: Operation not permitted +curl: (6) Could not resolve host: example.com +[porta] the sandbox refused this run 3 times; what each would have needed: + file-read-data /Users/me/.ssh/id_ed25519 + → closed by the preset or --deny-read; no mount reopens it + file-write-create /Users/me/elsewhere.txt + → -v /Users/me + … ``` -A single binary for macOS on Apple silicon and Linux on x86-64 and arm64, -checked against its published SHA-256 before it is installed, and against -the release's Sigstore signature where `cosign` is installed -([verifying a release](docs/cli.md#verifying-a-release)). Or, with -[Almide](https://github.com/almide/almide), the way `go install` does it -— the route for an Intel Mac or any other target: +`out.txt` was written; the key, the file outside and the network were not. -```bash -almide install github.com/almide/porta --branch main -``` - -`main` is the latest release; `--tag v0.6.6` pins one. It builds from source -into `~/.local/bin` and takes a few minutes (the Rust side compiles -Wasmtime). Building by hand is in -[the CLI reference](docs/cli.md#build-from-source). `porta -run` takes either a native command or a `.wasm` module, so anything that -compiles to WASI runs under it. - -On Debian and Ubuntu, each release also carries a `.deb` (amd64, arm64). -Installed with `sudo apt install ./porta__amd64.deb`, it puts porta -at `/usr/bin/porta` and, on Ubuntu 23.10 and later, loads the AppArmor -profile that lets porta give each command its own namespaces. Installed any -other way, `sudo porta setup` does the same: it copies porta to a root-owned -`/usr/local/bin/porta` and loads the profile for that copy alone. - -In a GitHub Actions workflow, the action installs a release the same checked -way and, on Ubuntu runners, loads the AppArmor profile that gives porta the -user namespaces the runner otherwise withholds: - -```yaml -- uses: almide/porta@v0.6.12 -- run: porta run ./scripts/untrusted-step.sh -v . -``` +Porta wraps one process and everything it starts. Writes go only to the +directories you mount, credential stores stay unreadable, the network is +narrowed to the ports or hosts you name, and CPU, memory, processes and time +are capped. It is enforced by Seatbelt on macOS and by Landlock, seccomp and +namespaces on Linux — not by a wrapper that asks nicely — and a rule the +kernel cannot express refuses the run instead of weakening it. -## Use it for - -Everything before `--` is porta's; everything after belongs to the command. - -### Restrict an AI agent you already run - -The agent installs deps, runs tests, edits files — on the machine that holds -your keys. Give it one directory and one host, and close the rest. +## Install ```bash -cd project -porta init claude # or: porta init codex -porta up -- -p "Fix the bug in main.rs" +curl -fsSL https://raw.githubusercontent.com/almide/porta/main/scripts/install.sh | bash ``` -`claude` runs unchanged, but it can write only inside the project and its own -`~/.claude` — where its settings, commands and global `CLAUDE.md` stay -unwritable, so one session cannot plant hooks for the next — and it cannot -read `~/.ssh`, `~/.aws` or the Keychain. The recipe is a commented -`porta.toml`, measured to what the agent needs; on macOS it takes its login -as `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), -since the Keychain is closed. No Docker daemon, no image, no change to the -agent. Add `--snapshot` and porta lists what the session changed afterwards; -`porta rollback --yes` puts it back. +| Where | How | +|---|---| +| macOS (Apple silicon), Linux (x86-64, arm64) | the script above: checks the SHA-256, and the Sigstore signature where `cosign` is installed | +| Debian, Ubuntu | `sudo apt install ./porta__amd64.deb` from a [release](https://github.com/almide/porta/releases); on Ubuntu 23.10+ it also loads the AppArmor profile porta's namespaces need | +| Ubuntu, installed any other way | `sudo porta setup` once, for the same profile | +| GitHub Actions | `- uses: almide/porta@v0.6.16`, then `porta run …` in any step | +| Anything else (Intel Mac…) | `almide install github.com/almide/porta --branch main` builds from source | -### Give a tool one API, and log every attempt +Every release is signed and carries build provenance — +[verifying a release](docs/cli.md#verifying-a-release). -A tool needs one endpoint and nothing else. Route egress through porta's proxy -and keep the record. +## Confine the coding agent you already use ```bash -porta run ./agent --proxy-allow 'api.example.com' --proxy-audit egress.jsonl -v ./work +cd my-project +porta init claude # or: porta init codex +porta up -- -p "Fix the failing test" ``` -Only `api.example.com` is reachable; direct TCP, UDP and Unix-socket egress are -denied, a name resolving to a cloud-metadata or link-local address is refused, -and every decision lands in `egress.jsonl`. - -### Try an unknown command without handing over the machine +`porta init` writes a commented `porta.toml`, measured to what that agent +needs: the project and the agent's own directory writable, its settings, +hooks and global instructions there unwritable — so one session cannot plant +something for the next — and its API key or token passed by name. On macOS +the Keychain is closed, so the login goes in as `ANTHROPIC_API_KEY` or +`CLAUDE_CODE_OAUTH_TOKEN` (`claude setup-token`). -Preview the policy before anything runs, then run it boxed in, with a deadline -and resource ceilings the kernel enforces on everything it starts. +Any other command works the same way, with flags: ```bash -porta explain ./sketchy-installer -v ./sandbox --allow-net github.com:443 # see the policy, run nothing -porta run ./sketchy-installer -v ./sandbox --allow-net github.com:443 \ - --timeout 60 --max-cpu 30 --max-procs 500 --max-file-size 200 +porta run ./installer -v ./sandbox --allow-net '*:443' --timeout 120 --max-procs 500 ``` -A hang is killed at the deadline, a CPU burn ends with SIGXCPU, a fork bomb -cannot fork, and a file stops growing at the ceiling. `--max-memory-mb 512` -bounds resident memory for the whole run: a cgroup v2 ceiling on Linux (a -systemd user session is needed, and the flag is refused without one), a -supervisor that ends the run at the ceiling on macOS. +Everything before `--` is porta's; everything after is the command's. -### Run untrusted or generated WASM +## What it stops -Code from a user or a model, run with no host filesystem or network and bounded -fuel, memory and time. A core module or a WASI 0.2 or 0.3 component, checked -the same way: every import it declares needs a capability you granted. +| | macOS | Linux | +|---|---|---| +| **Writes** | only the `-v` mounts, `/tmp`, `/dev` | same | +| **Inside a writable mount** | `.git/hooks`, `.git/config`, `.envrc`, shell rc files, agent settings and the like stay unwritable and cannot be renamed away | same (needs user namespaces — see below) | +| **Reads** | everything but credential stores: `~/.ssh`, `~/.aws`, `~/.config/gh`, cloud and registry tokens, the Keychain, browser profiles; `--read-policy strict` confines reads to the mounts | same, minus the Keychain | +| **Credential sockets** | SSH agent, gpg-agent, `docker.sock` refused unless `--allow-unix` | same | +| **Network** | open, or TCP by port (`--allow-net`), or by host through porta's proxy (`--proxy-allow`), or none (`--no-net`) | same; UDP closed under a port rule | +| **Other processes** | their arguments and environment unreadable; `open(1)` / Launch Services closed | not visible: own PID namespace | +| **Resources** | `--timeout`, `--max-cpu`, `--max-procs`, `--max-file-size`, `--max-memory-mb` | same (memory through cgroup v2) | +| **Environment** | starts empty; `PATH`, `HOME`, locale and terminal cross, the rest only by `-e` / `--env-pass` | same | -```bash -porta run plugin.wasm --profile worker --step-limit 5000000 --max-memory 256 -``` +What is closed is a preset — [`native/presets/default.toml`](native/presets/default.toml), +shown by `porta explain` — not a list in code: `--deny-read`, `--protect` and +`--deny-unix` add to it, `--preset ` replaces it. The full table, with +the reasons, is in [enforcement](docs/enforcement.md). -### Build an agent whose decision loop is WASM - -`porta run` restricts an agent someone else wrote. `porta agent` runs one whose -loop is itself WASM: the loop and every tool run in separate instances that -inherit no environment or directory, model credentials stay in the host, and a -crashed run resumes without repeating completed writes. +## When something is refused ```bash -porta agent agent.toml --record run.jsonl -- "Add 20 and 22 using the tool." -porta agent-resume agent.toml run.jsonl # completed writes are not repeated -porta agent-journal run.jsonl # read-only metadata, no code loaded +porta explain ./tool -v ./work --allow-net '*:443' # the policy, in words; nothing runs +porta run ./tool -v ./work --why # afterwards: each refusal and the flag it needed ``` -See [agents you build](docs/agent-runtime.md), [completion -checks](docs/completion-checks.md) the guest cannot bypass, and [artifact -pins](docs/artifact-pins.md) that bind WASM to a reviewed SHA-256. - -### Keep the settings as project config - -```bash -porta init native mytool # writes a porta.toml to edit -porta up -- --some-flag +```text +[porta] the sandbox refused this run 2 times; what each would have needed: + file-write-data /home/me/notes/out.txt + → -v /home/me/notes + network-outbound remote:*:80 + → --allow-net '*:80' + to run again with those granted: + porta run ./tool -v ./work -v /home/me/notes --allow-net '*:80' ``` -Or let a working invocation write its own: `porta explain claude … --save -porta.toml`. - -## See a restriction actually stop something +On macOS this comes from the kernel's denial log after any failed run; on +Linux, `--why` traces the run with `strace` from outside the sandbox. -A working restriction is invisible, so watch one fail. +## Undo what it changed ```bash -porta run curl -- https://example.com # network open by default -porta run curl --allow-net '*:443' -- https://example.com # HTTPS allowed → works -porta run curl --allow-net '*:80' -- https://example.com # → exit 7, port 443 denied +porta run ./agent -v ./project --snapshot # copies the mounts first; lists what changed after +porta rollback --yes # puts them back ``` -A refused run does not leave you guessing: after a non-zero exit, porta reads -the kernel's denial records and prints which flag each refusal would have -needed (on macOS after every failed run; on Linux with `--why`, which traces -the run with `strace`). Exit codes tell a script -apart a command that failed from one that never ran — see -[the CLI reference](docs/cli.md#when-a-run-is-refused). - -## Evidence - -Every published number keeps its raw report, source hashes and an audit that -regrades it; CI runs those audits, and results that do not favour Porta are -published in the same place. - -- [Escape corpus](docs/benchmarks/escapes.md) — known jailbreaks run against the - binary, scored on what escaped, losing rows included. -- [Startup and memory](docs/benchmarks/startup-and-memory.md) — fixed-response - measurements with explicit comparison limits. -- [Containment and recovery](docs/benchmarks/containment-evaluation.md) — under - hostile inputs; one of five scenarios separated the runtimes, and containment - cost task completion. -- [Real-task quality](docs/benchmarks/README.md) — Porta has not demonstrated - general quality superiority, and the shared-compute follow-up was rejected - rather than published as a win. +A clone on APFS, so the copy costs next to nothing there. Snapshots live +outside every mount, so the command cannot rewrite its own undo. + +## How it compares + +The same [escape corpus](docs/benchmarks/escapes.md) — real ways out, scored +on what got through — run under five tools, each with its own defaults +([full comparison](docs/benchmarks/competitors.md), losing rows included): + +| | porta | [srt](https://github.com/anthropics/sandbox-runtime) | [Fence](https://github.com/fencesandbox/fence) | [nono](https://github.com/nolabs-ai/nono) | [landrun](https://github.com/Zouuup/landrun) | +|---|---|---|---|---|---| +| macOS: tried / escaped | **22 / 0** | 17 / 5 | 14 / 5 | 13 / 5 | — | +| Linux: tried / escaped | **28 / 0** | 23 / 2 | 21 / 3 | 24 / 5 | 22 / 7 | + +What the others do that porta does not: srt and Fence close the network by +default; Fence filters commands by name; nono can ask for more access +mid-run. What a command pays under porta: about 16 ms on macOS, 2–6 ms on +Linux ([overhead](docs/benchmarks/overhead.md)). + +## Limits + +- **The network is open by default.** Close it with `--no-net`, `--allow-net` + or `--proxy-allow`. +- **On Linux, some protections need user namespaces** — the hooks inside a + mount, credential sockets, hiding other processes. Ubuntu 23.10+ restricts + them until the `.deb` or `sudo porta setup` loads a profile; without one + porta runs and says what is left open. +- **Same kernel.** This is a process sandbox, not a VM: a kernel exploit is + out of scope. The [threat model](docs/threat-model.md) lists what is and is + not defended. +- **Tested by its authors.** The corpus, the fuzzer and the monkey test are + public and run in CI; there has been no third-party audit. + +## Also in the box + +- **WASM with no ambient authority.** `porta run plugin.wasm --profile worker` + runs a core module or a WASI 0.2/0.3 component with no host filesystem or + network and bounded fuel, memory and time; every import needs a capability + you granted. +- **Agents whose loop is WASM.** `porta agent agent.toml` runs the decision + loop and each tool in separate instances, keeps model credentials in the + host, and resumes a crashed run without repeating completed writes + ([agent runtime](docs/agent-runtime.md)). +- **One API and a log.** `--proxy-allow api.example.com --proxy-audit + egress.jsonl` lets through only that host and records every decision. ## Docs -- [CLI reference](docs/cli.md) — every command, flag, `porta.toml` key and exit code. -- [Enforcement](docs/enforcement.md) — exactly what macOS and Linux each stop. -- [Threat model](docs/threat-model.md) — what it defends against, and what it does not. -- [Architecture](docs/architecture.md) — Almide decides policy, Rust enforces it. - -## License +- [CLI reference](docs/cli.md) — every command, flag, `porta.toml` key and exit code +- [Enforcement](docs/enforcement.md) — exactly what each platform stops, and how +- [Threat model](docs/threat-model.md) — what it defends against, and what it does not +- [Benchmarks](docs/benchmarks/README.md) — the escape corpus, the comparison, overhead +- [Architecture](docs/architecture.md) — Almide decides policy, Rust enforces it -Apache-2.0 +Built with [Almide](https://github.com/almide/almide) and +[Wasmtime](https://wasmtime.dev). Apache-2.0. diff --git a/native/denial_advice.rs b/native/denial_advice.rs index 2176caa..4e7bd6b 100644 --- a/native/denial_advice.rs +++ b/native/denial_advice.rs @@ -64,6 +64,9 @@ fn outbound_advice(target: &str) -> String { Some((_, port)) => format!("--allow-net '*:{port}'"), None => format!("--allow-net '{endpoint}'"), }, + // The resolver's socket is closed with the network, not as a + // credential socket: reopening it alone resolves names nothing can reach. + None if target.ends_with("/var/run/mDNSResponder") => "name resolution, closed with the network: --allow-net or --proxy-allow reopens it".to_string(), None if target.starts_with('/') => format!("--allow-unix {target}"), None => "the network is closed by --allow-net; name the port to reach".to_string(), } From 4b030568e1b225a934a5e560f9fc7ffde5df5ef9 Mon Sep 17 00:00:00 2001 From: O6lvl4 Date: Sun, 27 Sep 2026 19:14:20 +0900 Subject: [PATCH 2/2] Add a Simplified Chinese README, and switch languages with badges Co-Authored-By: Claude Opus 5.5 (1M context) --- README.ja.md | 4 +- README.md | 4 +- README.zh-CN.md | 169 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 README.zh-CN.md diff --git a/README.ja.md b/README.ja.md index cbfc89f..b7b31da 100644 --- a/README.ja.md +++ b/README.ja.md @@ -10,7 +10,9 @@

- English · 日本語 + English + 日本語 + 简体中文

--- diff --git a/README.md b/README.md index c05fbff..43c9a7e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@

- English · 日本語 + English + 日本語 + 简体中文

--- diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..d9ac93c --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,169 @@ +

+ Porta +

+ +

Porta

+ +

+ 无论是命令还是 AI 智能体,都只按你授予的权限运行。
+ 由 macOS 和 Linux 内核强制执行。无需容器,无需守护进程。 +

+ +

+ English + 日本語 + 简体中文 +

+ +--- + +```text +$ porta run sh -v ./work --no-net -- -c 'echo ok > out.txt; cat ~/.ssh/id_ed25519; echo x > ~/elsewhere.txt; curl https://example.com' +cat: /Users/me/.ssh/id_ed25519: Operation not permitted +sh: /Users/me/elsewhere.txt: Operation not permitted +curl: (6) Could not resolve host: example.com +[porta] the sandbox refused this run 3 times; what each would have needed: + file-read-data /Users/me/.ssh/id_ed25519 + → closed by the preset or --deny-read; no mount reopens it + file-write-create /Users/me/elsewhere.txt + → -v /Users/me + … +``` + +`out.txt` 写入成功;密钥、挂载之外的文件和网络都没有被触及。 + +Porta 包裹一个进程及其启动的所有子进程。写入仅限于你挂载的目录,凭据存储不可读, +网络收窄到你指定的端口或主机,CPU、内存、进程数和时间都有上限。它在 macOS 上由 +Seatbelt 强制执行,在 Linux 上由 Landlock、seccomp 和命名空间强制执行——而不是一个 +“礼貌请求”的包装器。内核无法表达的规则会直接拒绝运行,而不是悄悄放宽。 + +## 安装 + +```bash +curl -fsSL https://raw.githubusercontent.com/almide/porta/main/scripts/install.sh | bash +``` + +| 平台 | 方法 | +|---|---| +| macOS (Apple silicon)、Linux (x86-64, arm64) | 上面的脚本:校验 SHA-256,安装了 `cosign` 时还会校验 Sigstore 签名 | +| Debian、Ubuntu | 从 [release](https://github.com/almide/porta/releases) 下载后 `sudo apt install ./porta__amd64.deb`;在 Ubuntu 23.10+ 上还会加载命名空间所需的 AppArmor 配置 | +| 以其他方式安装的 Ubuntu | 运行一次 `sudo porta setup`,加载同样的配置 | +| GitHub Actions | `- uses: almide/porta@v0.6.16`,之后任意步骤中都可 `porta run …` | +| 其他 (Intel Mac 等) | `almide install github.com/almide/porta --branch main` 从源码构建 | + +每个 release 都带有签名和构建来源证明—— +[验证 release](docs/cli.md#verifying-a-release)。 + +## 约束你正在使用的编码智能体 + +```bash +cd my-project +porta init claude # 或: porta init codex +porta up -- -p "Fix the failing test" +``` + +`porta init` 会写出一个带注释的 `porta.toml`,按该智能体实际所需量身定制:项目和智能体 +自己的目录可写,其中的设置、钩子和全局指令不可写——这样一次会话无法为下一次会话埋下 +东西——API 密钥或令牌按名称传入。macOS 上钥匙串是关闭的,所以登录凭据通过 +`ANTHROPIC_API_KEY` 或 `CLAUDE_CODE_OAUTH_TOKEN`(`claude setup-token`)传入。 + +其他命令同样用参数即可: + +```bash +porta run ./installer -v ./sandbox --allow-net '*:443' --timeout 120 --max-procs 500 +``` + +`--` 之前是 porta 的参数,之后是命令的参数。 + +## 它阻止什么 + +| | macOS | Linux | +|---|---|---| +| **写入** | 仅 `-v` 挂载、`/tmp`、`/dev` | 相同 | +| **可写挂载内部** | `.git/hooks`、`.git/config`、`.envrc`、shell rc 文件、智能体设置等保持不可写,也无法被重命名移走 | 相同(需要用户命名空间——见下文) | +| **读取** | 除凭据存储外均可读:`~/.ssh`、`~/.aws`、`~/.config/gh`、云与镜像仓库令牌、钥匙串、浏览器配置均关闭;`--read-policy strict` 将读取也限制在挂载内 | 相同(钥匙串除外) | +| **凭据套接字** | SSH agent、gpg-agent、`docker.sock` 除非 `--allow-unix` 否则拒绝 | 相同 | +| **网络** | 开放,或按端口放行 TCP(`--allow-net`),或经 porta 代理按主机放行(`--proxy-allow`),或完全关闭(`--no-net`) | 相同;按端口放行时 UDP 关闭 | +| **其他进程** | 其参数和环境变量不可读;`open(1)` / Launch Services 关闭 | 不可见:独立的 PID 命名空间 | +| **资源** | `--timeout`、`--max-cpu`、`--max-procs`、`--max-file-size`、`--max-memory-mb` | 相同(内存通过 cgroup v2) | +| **环境变量** | 从空开始;只传递 `PATH`、`HOME`、区域设置和终端,其余仅限 `-e` / `--env-pass` 指定的 | 相同 | + +关闭哪些内容由预设决定——[`native/presets/default.toml`](native/presets/default.toml), +可用 `porta explain` 查看——而不是写死在代码里:`--deny-read`、`--protect`、 +`--deny-unix` 在其上追加,`--preset ` 整体替换。附带理由的完整表格见 +[enforcement](docs/enforcement.md)。 + +## 被拒绝时 + +```bash +porta explain ./tool -v ./work --allow-net '*:443' # 用文字描述策略,不执行任何东西 +porta run ./tool -v ./work --why # 运行后:每次拒绝及其所需的参数 +``` + +```text +[porta] the sandbox refused this run 2 times; what each would have needed: + file-write-data /home/me/notes/out.txt + → -v /home/me/notes + network-outbound remote:*:80 + → --allow-net '*:80' + to run again with those granted: + porta run ./tool -v ./work -v /home/me/notes --allow-net '*:80' +``` + +在 macOS 上,任何失败的运行之后都会从内核的拒绝日志中给出这些信息;在 Linux 上, +`--why` 在沙箱外用 `strace` 跟踪运行。 + +## 撤销它做的改动 + +```bash +porta run ./agent -v ./project --snapshot # 先复制挂载目录;结束后列出改动 +porta rollback --yes # 恢复原样 +``` + +在 APFS 上是克隆,复制几乎零成本。快照存放在所有挂载之外,命令无法改写自己的撤销手段。 + +## 对比 + +同一套[逃逸用例](docs/benchmarks/escapes.md)——真实的逃逸手法,按实际突破的数量计分—— +在五个工具下各自以默认配置运行([完整对比](docs/benchmarks/competitors.md),包括 porta +落后的项): + +| | porta | [srt](https://github.com/anthropics/sandbox-runtime) | [Fence](https://github.com/fencesandbox/fence) | [nono](https://github.com/nolabs-ai/nono) | [landrun](https://github.com/Zouuup/landrun) | +|---|---|---|---|---|---| +| macOS:尝试 / 逃逸 | **22 / 0** | 17 / 5 | 14 / 5 | 13 / 5 | — | +| Linux:尝试 / 逃逸 | **28 / 0** | 23 / 2 | 21 / 3 | 24 / 5 | 22 / 7 | + +其他工具有而 porta 没有的:srt 和 Fence 默认关闭网络;Fence 按名称过滤命令;nono +可在运行中申请更多权限。porta 的开销:macOS 约 16 ms,Linux 2–6 ms +([overhead](docs/benchmarks/overhead.md))。 + +## 局限 + +- **网络默认开放。** 用 `--no-net`、`--allow-net` 或 `--proxy-allow` 关闭。 +- **在 Linux 上,部分保护需要用户命名空间**——挂载内的钩子、凭据套接字、隐藏其他进程。 + Ubuntu 23.10+ 会限制它们,直到 `.deb` 或 `sudo porta setup` 加载配置;没有配置时 + porta 照常运行,并说明哪些仍然开放。 +- **共享内核。** 这是进程沙箱,不是虚拟机:内核漏洞不在防护范围内。 + [威胁模型](docs/threat-model.md)列出了防护与不防护的内容。 +- **由作者测试。** 逃逸用例、模糊测试和猴子测试均公开并在 CI 中运行;尚未经过第三方审计。 + +## 其他功能 + +- **无环境权限的 WASM。** `porta run plugin.wasm --profile worker` 运行核心模块或 + WASI 0.2/0.3 组件,不接触宿主文件系统和网络,fuel、内存和时间均有上限;每个导入都 + 需要你授予的能力。 +- **循环本身是 WASM 的智能体。** `porta agent agent.toml` 将决策循环和每个工具放在 + 独立实例中运行,模型凭据留在宿主,崩溃的运行可恢复且不会重复已完成的写入 + ([agent runtime](docs/agent-runtime.md))。 +- **一个 API 加一份日志。** `--proxy-allow api.example.com --proxy-audit egress.jsonl` + 只放行该主机,并记录每一次决策。 + +## 文档 + +- [CLI 参考](docs/cli.md) — 所有命令、参数、`porta.toml` 键和退出码 +- [Enforcement](docs/enforcement.md) — 各平台究竟阻止什么、如何阻止 +- [威胁模型](docs/threat-model.md) — 防护什么,不防护什么 +- [基准测试](docs/benchmarks/README.md) — 逃逸用例、对比、开销 +- [架构](docs/architecture.md) — 策略由 Almide 决定,由 Rust 强制执行 + +基于 [Almide](https://github.com/almide/almide) 和 [Wasmtime](https://wasmtime.dev) 构建。Apache-2.0。