diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 505af3d..c6d4924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,11 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 + - name: Install Node.js (proxy integration tests) + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | diff --git a/AGENTS.md b/AGENTS.md index add87eb..f7502e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,9 +15,11 @@ crates/ phantom-tui/ # Ratatui terminal UI phantom-agent/ # LD_PRELOAD dylib (Linux only, hooks libc send/recv) tests/ - proxy_node_integration.rs # Integration tests: Node.js proxy capture (HTTP + HTTPS) - apps/node-app/ # Test Node.js app (client.js, client-alts.js, proxy-preload.js) - integration/ # Shell-based integration tests + proxy_node_integration.rs # Integration tests: Node.js proxy capture (HTTP + HTTPS) + proxy_curl_https_integration.rs # Integration tests: curl HTTPS via auto-set CA trust env + fault_injection.rs # Integration tests: --fault delay/error rules + apps/node-app/ # Test Node.js app (client.js, client-alts.js, proxy-preload.js) + integration/ # Shell-based integration tests Cargo.toml # Workspace root + binary crate plan.md # Japanese-language technical design document ``` @@ -62,7 +64,18 @@ make test # cargo test --workspace --all-targets --all-features make check # fmt + clippy + build + test (full CI locally) ``` -### CLI Options +### CLI Structure + +The CLI uses clap subcommands with a backward-compatible default: + +- `phantom [RUN-OPTIONS] [-- CMD]` — same as `phantom run …` (legacy form, kept working forever) +- `phantom run [RUN-OPTIONS] [-- CMD]` — capture traffic +- `phantom cert path|print|export [--out FILE]` — manage the persistent MITM CA + +`Cli` holds `Option` plus a flattened `RunArgs`; `args_conflicts_with_subcommands = true`. +When adding a new subcommand, extend `enum Command` in `src/main.rs` and dispatch in `main()`. + +### Run Options | Flag | Default | Description | |---|---|---| @@ -70,10 +83,26 @@ make check # fmt + clippy + build + test (full CI locally) | `-o, --output ` | `tui` | `tui` (interactive) or `jsonl` (stdout stream, auto-exits with child) | | `-p, --port ` | `8080` | Proxy capture port | | `--insecure` | off | Disable TLS verification for backend servers (self-signed certs) | -| `-d, --data-dir ` | `~/.local/share/phantom/data` | Storage directory | +| `-d, --data-dir ` | `~/.local/share/phantom/data` | Storage directory (CA lives in `/ca`) | +| `--fault ` | — | Fault injection rules (repeatable) | | `--agent-lib ` | — | Path to `libphantom_agent.so` (ldpreload backend) | | `-- ` | — | Command to spawn and trace automatically | +### CA Persistence and Trust Environment + +The MITM CA is persisted at `/ca/phantom-ca.{key,cert}.pem` (key is 0600; a +`.gitignore` is written into `ca/`). `phantom_capture::ensure_ca(&ca_dir)` creates or loads it; +`ProxyCaptureBackend::with_ca_dir(dir)` makes the proxy use it (without it the CA is ephemeral). + +`spawn_proxy_child` in `src/main.rs` configures the child environment: + +| Variable | Value | Note | +|---|---|---| +| `HTTP_PROXY` / `http_proxy` | proxy URL | always set (overrides inherited, with notice) | +| `HTTPS_PROXY` / `https_proxy` | proxy URL | **non-Node only** — Node's preload tunnels HTTPS itself; setting it would trigger axios's own env proxying (removed for Node) | +| `SSL_CERT_FILE`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, `DENO_CERT` | combined bundle | `/phantom-ca-bundle.pem` = previously trusted roots (user/system bundle) + phantom CA; always set | +| `NO_PROXY`, `no_proxy`, `ALL_PROXY`, `all_proxy`, `npm_config_*proxy*` | *removed* | inherited values create capture blind spots or leak traffic to other proxies | + --- ## Node.js Transparent Proxy Injection @@ -106,24 +135,16 @@ When the command after `--` is `node` or `nodejs`, phantom automatically: ## JSONL Output Schema -When `--output jsonl` is used, one JSON object is written per line to stdout. All fields are always present unless marked optional. +When `--output jsonl` is used, one JSON object is written per line to stdout. +**Full field reference and compatibility policy: [`docs/jsonl-schema.md`](docs/jsonl-schema.md)** — this +is the single source of truth; keep it, not a copy of its table, up to date here. -| Field | Type | Description | -|---|---|---| -| `trace_id` | string | W3C-compatible 128-bit trace ID (hex, 32 chars) | -| `span_id` | string | 64-bit span ID (hex, 16 chars) | -| `timestamp_ms` | number | Unix epoch milliseconds — request start time | -| `duration_ms` | number | Round-trip latency in milliseconds | -| `method` | string | HTTP verb: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, … | -| `url` | string | Full request URL (scheme + host + path + query) | -| `status_code` | number | HTTP response status code | -| `protocol_version` | string | HTTP version string, e.g. `"HTTP/1.1"` | -| `request_headers` | object | Lower-cased header names → values | -| `response_headers` | object | Lower-cased header names → values | -| `request_body` | string? | UTF-8 decoded body; omitted when empty | -| `response_body` | string? | UTF-8 decoded body; omitted when empty | -| `source_addr` | string? | Client socket address, e.g. `"127.0.0.1:54321"` | -| `dest_addr` | string? | Server socket address, e.g. `"93.184.216.34:443"` | +Summary: `schema_version` (currently `2`) identifies the record shape; within +a version, fields are only ever added, never removed/renamed/retyped. Bodies +carry `*_body_encoding` (`"utf-8"`|`"base64"`), `*_body_truncated`, and +`*_content_encoding` (set when a compressed body was transparently decoded +for the record — the wire bytes forwarded to the real client/server are +never altered). --- @@ -170,6 +191,26 @@ Both tests: - Parse JSONL output and assert method, path, status code per trace. - Identify traces by `x-phantom-client` custom header in `request_headers`. +### curl / CA Integration Tests (`tests/proxy_curl_https_integration.rs`) + +| Test | Description | +|------|-------------| +| `test_curl_https_verifies_phantom_ca_without_insecure_flag` | Proxied curl verifies the MITM'd cert via auto-set `CURL_CA_BUNDLE` — no `curl -k` | +| `test_ca_certificate_is_stable_across_runs` | `phantom cert path`/`print`; CA path and bytes identical across invocations | + +Auto-skips when `curl` is not on PATH. + +### Other Runtime Compatibility Tests + +| File | Test | Description | +|------|------|-------------| +| `tests/proxy_gzip_integration.rs` | `test_gzip_response_is_decoded_for_storage_but_wire_is_unmodified` | Content-Encoding decode: recorded body is plaintext, wire bytes untouched | +| `tests/proxy_python_integration.rs` | `test_proxy_captures_python_stdlib_client` | Python 3 `urllib.request` (stdlib only) — HTTP + HTTPS, auto-skips if `python3` missing | +| `tests/proxy_go_integration.rs` | `test_proxy_captures_go_net_http_client` | Go `net/http` — HTTP only (see `docs/compatibility.md` for why HTTPS/loopback are excluded); auto-skips if `go` missing | +| `tests/redaction_integration.rs` | `test_redact_masks_header_and_body_field`, `test_without_redact_flag_values_are_untouched` | `--redact` end-to-end through the real proxy | + +Full runtime support matrix and known limitations: [`docs/compatibility.md`](docs/compatibility.md). + ### Run a Single Unit Test Function ```sh diff --git a/Cargo.lock b/Cargo.lock index 996bcca..7d72da2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,26 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log 0.4.29", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -189,6 +209,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 4.0.3", +] + [[package]] name = "brotli" version = "8.0.2" @@ -197,7 +228,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor", + "brotli-decompressor 5.0.0", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -227,12 +268,24 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -324,6 +377,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -356,7 +418,7 @@ version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" dependencies = [ - "brotli", + "brotli 8.0.2", "compression-core", "flate2", "memchr", @@ -472,6 +534,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -590,6 +658,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -641,6 +719,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -668,6 +752,21 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -829,6 +928,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -871,6 +980,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1173,6 +1293,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1445,6 +1579,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nom" version = "7.1.3" @@ -1498,6 +1642,79 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -1605,9 +1822,11 @@ name = "phantom" version = "0.1.0" dependencies = [ "anyhow", + "base64", "cargo-husky", "clap", "dirs", + "flate2", "phantom-capture", "phantom-core", "phantom-storage", @@ -1640,7 +1859,9 @@ name = "phantom-capture" version = "0.1.0" dependencies = [ "base64", + "brotli 6.0.0", "bytes", + "flate2", "http", "http-body-util", "hudsucker", @@ -1652,8 +1873,10 @@ dependencies = [ "rustls", "serde", "serde_json", + "tempfile", "tokio", "tracing", + "zstd", ] [[package]] @@ -1682,6 +1905,7 @@ dependencies = [ name = "phantom-tui" version = "0.1.0" dependencies = [ + "arboard", "crossterm", "phantom-core", "ratatui", @@ -1707,6 +1931,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1756,6 +1993,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick_cache" version = "0.6.18" @@ -2341,6 +2590,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -2817,6 +3080,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -3113,6 +3382,23 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x509-parser" version = "0.16.0" @@ -3282,3 +3568,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 72526db..0b7e2a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4", features = ["derive"] } dirs = "6" anyhow = "1" +base64 = "0.22" [dev-dependencies] serde_json = { workspace = true } @@ -51,6 +52,7 @@ tempfile = "3" rustls = "0.22" rcgen = "0.13" rustls-pki-types = "1" +flate2 = "1" [dev-dependencies.cargo-husky] version = "1" diff --git a/README.md b/README.md index 6daa252..15ceb38 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,116 @@ # Phantom [![CI](https://github.com/epli2/phantom/actions/workflows/ci.yml/badge.svg)](https://github.com/epli2/phantom/actions/workflows/ci.yml) + +**Zero-instrumentation API observability toolbox for modern web development** — observe, perturb, record, and spec your app's HTTP traffic without changing a line of code. + +Phantom wraps any command in a local MITM proxy, captures every HTTP/HTTPS request and response the process makes, and shows them in an interactive terminal UI — or streams them as JSON Lines for scripts and AI coding agents. + +日本語のガイドは [docs/how-to-use.ja.md](docs/how-to-use.ja.md) を参照してください。 + +## 30-second demo + +```sh +# See every request your app makes — HTTP and HTTPS, zero app changes: +phantom -- node app.js + +# Stream traces as JSONL for scripting or AI analysis: +phantom --output jsonl -- npm test | jq 'select(.status_code >= 400)' + +# Chaos-test your error handling: 500ms delay on every /api call: +phantom --fault delay:500ms:/api -- node app.js +``` + +## Features + +| | | +|---|---| +| **Zero instrumentation** | No SDK, no code changes. Phantom spawns your command with proxy + CA trust environment pre-configured. | +| **HTTPS interception** | Persistent local CA (`phantom cert`). Spawned processes trust it automatically via `CURL_CA_BUNDLE`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `NODE_EXTRA_CA_CERTS`, `DENO_CERT` — no `--insecure` flags needed. | +| **HTTP/1.1 + HTTP/2** | Both captured through the MITM proxy. | +| **Node.js deep integration** | `phantom -- node app.js` auto-injects a preload that routes `http`, `https`, `axios`, `undici`, and `fetch()` through the proxy. | +| **Interactive TUI** | Ratatui-based two-pane viewer with filtering. | +| **JSONL output** | One JSON object per line on stdout; exits when the child exits. Built for `jq` pipelines and AI agents. | +| **Fault injection** | `--fault delay:100ms-500ms`, `--fault error:503:0.5:/api` — verify timeout and error handling during development. | +| **Content-Encoding decoding** | gzip/brotli/zstd/deflate response and request bodies are transparently decoded for storage/display; the wire is never altered. | +| **Redaction** | `--redact` masks common secrets (`authorization`, `cookie`, `password`, `token`, …) in headers and JSON bodies before they ever hit disk. Off by default; opt in before sharing traces. | +| **Local persistence** | Traces stored in an embedded [Fjall](https://github.com/fjall-rs/fjall) LSM-tree store. | +| **LD_PRELOAD backend** (Linux) | Socket-level capture for proxy-unaware tools (plain HTTP). | + +## Installation + +Requires Rust (stable): + +```sh +git clone https://github.com/epli2/phantom +cd phantom +cargo build --release +# binary at target/release/phantom +``` + +Pre-built binaries and Homebrew are planned — see the [roadmap](ROADMAP.md). + +## How it works + +1. **Proxy backend (default):** Phantom starts a MITM proxy on `127.0.0.1:`, then spawns your command with `HTTP_PROXY`/`HTTPS_PROXY` pointing at it and CA trust variables pointing at a combined bundle (your existing roots + the phantom CA). TLS is re-signed on the fly by a CA persisted under the data directory. +2. **Node.js:** for `node` commands a preload script is additionally injected via `--require`, covering clients that ignore proxy environment variables. +3. **LD_PRELOAD backend (Linux):** `libphantom_agent.so` hooks `send`/`recv` at the libc level for plain-HTTP capture without any proxy configuration. + +Captured traces flow into the TUI or JSONL stream and are persisted locally. Everything binds to `127.0.0.1` only; nothing leaves your machine. + +Beyond Node.js, phantom's proxy backend also transparently captures curl, Python, Go, and Java clients — see [docs/compatibility.md](docs/compatibility.md) for exactly what's verified and two known limitations (Go's loopback-proxy quirk, IP-literal HTTPS targets). + +### Trusting the CA outside phantom + +Processes spawned by phantom trust the CA automatically. For anything else (e.g. a browser): + +```sh +phantom cert export # writes phantom-ca.cert.pem + prints OS trust instructions +phantom cert path # print the PEM path (for scripts) +``` + +## JSONL schema + +Each line is one completed request/response pair: + +```json +{"timestamp_ms":1783340616458,"duration_ms":12,"method":"GET","url":"https://api.example.com/users","status_code":200,"request_headers":{"…":"…"},"response_headers":{"…":"…"},"response_body":"…","protocol_version":"HTTP/1.1","trace_id":"13a7…","span_id":"3699…"} +``` + +Full field reference and compatibility policy: [docs/jsonl-schema.md](docs/jsonl-schema.md). + +## CLI + +```text +phantom [OPTIONS] [-- CMD…] # capture (same as `phantom run`) +phantom run [OPTIONS] [-- CMD…] # capture, explicit form +phantom cert path|print|export # manage the HTTPS interception CA + +Options (run): + -b, --backend Capture backend [default: proxy] + -o, --output Output mode [default: tui] + -p, --port Proxy port [default: 8080] + --insecure Skip TLS verification toward backend servers + -d, --data-dir Storage directory + --fault Inject faults (repeatable) + --agent-lib libphantom_agent.so (ldpreload backend) +``` + +Run `phantom --help` for the full guide with examples. + +## Development + +```sh +make check # fmt + clippy + build + test (same as CI) +make test # test suite (Node.js integration tests require node/npm) +``` + +Project conventions and architecture are documented in [AGENTS.md](AGENTS.md). + +## Roadmap + +Phantom is evolving into a full local-first API development toolbox: HAR export/import, request replay, record-and-mock servers, WebSocket/SSE capture, and OpenAPI generation from live traffic. See [ROADMAP.md](ROADMAP.md) for the detailed plan. + +## License + +TBD diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..935c663 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,848 @@ +# Phantom 開発ロードマップ + +> **ドキュメントの目的**: 本書は Phantom を「現代の Web 開発を支える実用ツール」へ進化させるための、コンセプト再定義と段階的開発計画である。**LLM エージェントが単独で読んで迷わず実装着手できる粒度**で記述している。各タスクには ID・依存関係・変更対象ファイル・実装手順・受け入れ基準・テスト方法を明記した。 +> +> 最終更新: 2026-07-05 / 対象リビジョン: `main` = `6e19d28` (Merge PR #5: fault injection) + +--- + +## 目次 + +- [1. 現状分析](#1-現状分析) + - [1.1 実装済み機能](#11-実装済み機能) + - [1.2 Open PR の評価と処置方針](#12-open-pr-の評価と処置方針) + - [1.3 現状の課題(ギャップ分析)](#13-現状の課題ギャップ分析) +- [2. コンセプト再定義](#2-コンセプト再定義) +- [3. アーキテクチャ進化方針](#3-アーキテクチャ進化方針) +- [4. ロードマップ全体像](#4-ロードマップ全体像) +- [5. Phase 0: リポジトリ健全化と基盤整備 (v0.1.x)](#5-phase-0-リポジトリ健全化と基盤整備-v01x) +- [6. Phase 1: コアユーザビリティ — 「信頼できる HTTP デバッガ」 (v0.2)](#6-phase-1-コアユーザビリティ--信頼できる-http-デバッガ-v02) +- [7. Phase 2: Web 開発ワークフロー統合 (v0.3)](#7-phase-2-web-開発ワークフロー統合-v03) +- [8. Phase 3: 仕様生成とエコシステム統合 (v0.4)](#8-phase-3-仕様生成とエコシステム統合-v04) +- [9. Phase 4: 長期研究トラック (v1.0 以降)](#9-phase-4-長期研究トラック-v10-以降) +- [10. 横断的関心事](#10-横断的関心事) +- [11. LLM エージェント向け作業プロトコル](#11-llm-エージェント向け作業プロトコル) +- [付録 A: JSONL スキーマ v2 (案)](#付録-a-jsonl-スキーマ-v2-案) +- [付録 B: CLI 全体シノプシス (目標形)](#付録-b-cli-全体シノプシス-目標形) +- [付録 C: タスク一覧サマリ](#付録-c-タスク一覧サマリ) +- [付録 D: 意思決定記録 (ADR)](#付録-d-意思決定記録-adr) + +--- + +## 1. 現状分析 + +### 1.1 実装済み機能 + +Phantom は Rust 製のゼロ計装 HTTP/HTTPS 可観測ツール。Cargo ワークスペース構成(5 ライブラリクレート + 1 バイナリ)。 + +| 領域 | 実装状況 | 主要ファイル | +|---|---|---| +| MITM プロキシキャプチャ (HTTP/HTTPS) | ✅ hudsucker 0.22 ベース。HTTP/2 対応済み (PR #1) | `crates/phantom-capture/src/proxy.rs` | +| LD_PRELOAD キャプチャ (Linux, 平文 HTTP のみ) | ✅ libc `send`/`recv`/`close` フック | `crates/phantom-agent/src/lib.rs`, `crates/phantom-capture/src/ldpreload.rs` | +| Node.js 透過注入 | ✅ `phantom -- node app.js` で `proxy-preload.js` を `--require` 注入。http/https/axios/undici/fetch 対応 | `src/main.rs`, `tests/apps/node-app/proxy-preload.js` | +| ストレージ (Fjall LSM-tree) | ✅ `traces` / `by_time` / `by_trace_id` の 3 パーティション | `crates/phantom-storage/src/fjall_store.rs` | +| TUI (Ratatui) | ✅ 2 ペイン(リスト+詳細)、URL 部分一致フィルタのみ | `crates/phantom-tui/` | +| JSONL 出力 | ✅ 1 トレース 1 行、子プロセス終了で自動 exit | `src/main.rs` (`run_jsonl_output`) | +| フォールトインジェクション | ✅ `--fault delay:100ms` / `--fault error:503:0.5:/api` (PR #5) | `crates/phantom-capture/src/fault.rs` | +| 統合テスト | ✅ Node.js (http/https/axios/undici/fetch) + fault injection | `tests/proxy_node_integration.rs`, `tests/fault_injection.rs` | +| CI | ✅ fmt / clippy / build / test | `.github/workflows/ci.yml` | +| ドキュメント | △ `AGENTS.md`(充実), `docs/how-to-use.ja.md`, `README.md` はバッジのみでほぼ空 | — | + +### 1.2 Open PR の評価と処置方針 + +#### PR #4: Java 統合テスト (Spring Boot + 4 種の Java HTTP クライアント) + +- **内容**: `HTTP_PROXY` 経由の非侵襲キャプチャを Java (JDK HttpClient / AsyncHttpClient / Jetty / Apache HttpClient 5) で検証する統合テスト 2 本と Maven テストアプリを追加。java/mvn がなければ自動スキップ。 +- **評価**: ✅ **有益。マージすべき**。「Node.js 以外でも動く」ことの回帰保証はコンセプト(ポリグロット対応)の裏付けになる。プロダクトコードに変更はなくリスクが低い。 +- **懸念**: (a) base が古い main (`0ff64ee`、fault injection マージ前) のため rebase が必要。(b) テストアプリが trust-all SSLContext を使っており、Phase 0 の CA 永続化 (P0-3) 完了後は `-Djavax.net.ssl.trustStore` 方式へ更新するフォローアップが望ましい。 +- **処置**: → タスク **P0-1** で rebase & マージ。 + +#### PR #2: MySQL トレース機能 + +- **内容**: LD_PRELOAD エージェントで MySQL プロトコルを解析し、`phantom-core` に `mysql.rs`、storage に `fjall_mysql.rs`、TUI にも MySQL 表示を追加する大型変更(11 ファイル)。 +- **評価**: ⚠️ **現時点ではマージ非推奨**。理由: + 1. **コンセプトの希釈**: 本ロードマップは Phantom を「Web 開発向け HTTP レイヤーのツール」として再定義する(§2)。DB プロトコルは価値があるが、HTTP のコア体験が未完成な段階で第 2 のプロトコルを抱えると、TUI・ストレージ・JSONL スキーマすべてが二重化し保守コストが跳ね上がる。 + 2. **技術的負債**: base が 4 ヶ月前の main (`d77ef69`) で、`src/main.rs` / `app.rs` / `ui.rs` など競合必至のファイルを触っている。また `HttpTrace` と並列の独立型として実装されており、後述の「プロトコル非依存イベントモデル」(P4-4) と設計が衝突する。 + 3. LD_PRELOAD 限定 (Linux のみ・TLS 不可) で、主力のプロキシバックエンドと非対称。 +- **処置**: → タスク **P0-2**。PR に経緯をコメントして **クローズし、GitHub Issue「Protocol plugins: MySQL/PostgreSQL tracing」として再登録**する。設計自体(パケット解析ロジック)は Phase 4 の P4-4 で再利用する。**削除ではなく延期**である。 + +### 1.3 現状の課題(ギャップ分析) + +コードを精査して確認した、実用上のギャップ。番号は後続タスクから参照される。 + +| # | ギャップ | 詳細・根拠 | 対応タスク | +|---|---|---|---| +| G1 | **CA 証明書が起動ごとに使い捨て** | `proxy.rs:119 generate_ca()` が毎回新規生成。ディスク保存もエクスポート手段もないため、Node 以外のクライアントは TLS 検証を無効化(trust-all / `--insecure`)しないと HTTPS を復号できない。**HTTPS MITM ツールとして最大の欠陥** | P0-3 | +| G2 | **`HTTPS_PROXY` 環境変数を設定していない** | `src/main.rs spawn_proxy_child()` は `HTTP_PROXY`/`http_proxy` のみ設定。curl/Python/Go など大半のクライアントは HTTPS リクエストに `HTTPS_PROXY` を参照するため、Node 以外では HTTPS が素通りする | P0-3 | +| G3 | **圧縮ボディを展開しない** | `proxy.rs` に Content-Encoding 処理が存在しない。gzip/br 応答は TUI・JSONL で文字化けバイナリになる。現実の API はほぼ全て圧縮転送するため致命的 | P1-1 | +| G4 | ボディ 1MB で暗黙に切り捨て、痕跡が残らない | `proxy.rs:20 MAX_BODY_SIZE`。切り捨てられたか判別不能 | P1-2 | +| G5 | TUI が最小限 | フィルタは URL 部分一致のみ。ステータス/メソッド絞り込み・ボディ検索・JSON 整形・コピー・スクロール位置表示・ヘルプがない | P1-4 | +| G6 | 蓄積データの活用手段がない | Fjall に永続化はするが、読み出しは TUI 起動時の `list_recent(1000)` のみ。エクスポート・検索・セッション区別が不可能 | P1-3, P2-1, P2-6 | +| G7 | ストリーミング応答 (SSE)・WebSocket が扱えない | ボディを全読みしてから記録する設計のため、SSE は接続が終わるまで何も見えず、WebSocket は不可視 | P2-5 | +| G8 | 機微情報がそのまま保存される | `Authorization`/`Cookie` ヘッダや token を含むボディが平文でディスクに残る | P1-5 | +| G9 | README がほぼ空 | 初見ユーザー/エージェントが価値を把握できない | P0-4 | +| G10 | 配布手段が `cargo build` のみ | バイナリリリース・Homebrew・`cargo install` 手順がない | P3-5 | +| G11 | JSONL スキーマにバージョン概念がない | フィールド追加時に下流(jq スクリプト、AI エージェント)が壊れる | P1-3 | + +--- + +## 2. コンセプト再定義 + +### 2.1 これまでのビジョンの問題 + +`plan.md` は「bpftime (ユーザー空間 eBPF) + Fjall + Candle LLM + Arazzo 自動推論 + Ratatui/Tauri」という野心的な構想を描いている。技術選定の調査価値は高いが、ロードマップとしては次の問題がある。 + +1. **段階的に価値を出せない**: Arazzo 自動推論や bpftime 統合は、それ単体で数ヶ月級のプロジェクトであり、完成するまでユーザーに届く価値がない。 +2. **現実装との乖離**: 現在の Phantom は「MITM プロキシ + TUI + JSONL」であり、plan.md の構想とは別物。乖離したビジョンは開発の意思決定を迷わせる。 +3. **市場の空白と一致しない**: mitmproxy(汎用 MITM)、Charles/Proxyman(GUI デバッガ)、Keploy(record & replay テスト)は存在するが、**「ターミナル完結・ゼロ設定・AI エージェント親和の API 開発ツールボックス」** は空白である。Phantom の既存資産(JSONL 自動 exit、Node 透過注入、fault injection)はまさにこの方向を向いている。 + +### 2.2 新コンセプト + +> **Phantom は、モダン Web 開発者と AI コーディングエージェントのための「ローカルファースト API 開発ツールボックス」である。** +> アプリのコードを 1 行も変えずに、開発中のアプリが行う HTTP 通信を **見る(observe)・試す(perturb)・写す(record/replay/mock)・書き起こす(spec-gen)** ことができる。 + +4 つの動詞がプロダクトの柱である: + +| 柱 | 意味 | 対応機能 | +|---|---|---| +| **Observe** | 通信を透過キャプチャして人間(TUI)と機械(JSONL)の両方に見せる | 既存プロキシ/TUI/JSONL + Phase 1 の品質向上 | +| **Perturb** | 遅延・エラー・帯域制限を注入して障害時の挙動を開発中に検証する | 既存 fault injection + Phase 2 拡張 | +| **Record & Replay** | 通信を記録し、リプレイ/モックサーバー化してオフライン開発・テストに使う | Phase 2 (HAR, replay, mock) | +| **Spec-gen** | 実トラフィックから OpenAPI 等の仕様を書き起こし、ドリフトを検知する | Phase 3 | + +### 2.3 ターゲットユーザーと代表ユースケース + +1. **フロントエンド/BFF 開発者**: 「この画面を開いたとき裏で何が飛んでいるか」を `phantom -- npm run dev` の一発で確認。遅い API・失敗リクエストを TUI で特定。 +2. **バックエンド開発者**: 自サービスが呼ぶ外部 API(決済、認証、LLM API)の実リクエストを確認し、`--fault` でタイムアウト耐性を検証。 +3. **AI コーディングエージェント**(Claude Code 等): `phantom --output jsonl -- ` でアプリの通信を構造化データとして取得し、デバッグ・テスト生成・API 理解に使う。**機械可読であることは Phantom の一級要件**。 +4. **QA / SDET**: 記録したトラフィックをモックサーバー化し、外部依存なしの E2E テストを構築。 +5. **API 設計者**: ドキュメントのない既存システムの実トラフィックから OpenAPI をリバース生成。 + +### 2.4 スコープ境界 + +- **やる**: HTTP/1.1, HTTP/2, WebSocket, SSE のキャプチャと操作。ローカル・単一開発者マシンでの利用。CLI/TUI/(将来)ローカル Web UI。 +- **やらない(当面)**: 本番環境常駐エージェント、分散トレーシング基盤(Jaeger 代替)、カーネル eBPF、マルチユーザー SaaS。 +- **長期研究トラックに隔離**(§9): bpftime、Arazzo 自動推論、ローカル LLM (Candle)、Tauri GUI、DB プロトコル(MySQL/PostgreSQL)。**「Phase 3 まで完了し、かつ利用実績がある」ことを着手条件とする。** + +### 2.5 競合との差別化 + +| ツール | 強み | Phantom が取る差別化 | +|---|---|---| +| mitmproxy | 成熟した MITM、Python アドオン | ゼロ設定(`phantom -- cmd` 一発、CA 自動信頼注入)、Rust 単一バイナリ、AI エージェント向け JSONL、fault injection 内蔵 | +| Charles / Proxyman | GUI の見やすさ | ターミナル完結(SSH 先で動く)、無料 OSS、スクリプタブル | +| Keploy | record & replay テスト生成 | テスト特化ではなく汎用ツールボックス。TUI での探索体験 | +| HTTP Toolkit | ブラウザ/端末の自動インターセプト | CLI ファースト、記録データのローカル永続化と再利用(mock/spec-gen) | + +--- + +## 3. アーキテクチャ進化方針 + +既存の設計原則(`AGENTS.md` 記載)は維持する。追加する方針は以下。 + +### 3.1 CLI をサブコマンド制へ移行(後方互換維持) + +現在は単一コマンド + フラグ。Phase 0 で `clap` のサブコマンドを導入し、**サブコマンド省略時は従来どおり `run` 相当として動く**ようにする(既存ユーザー・既存テスト・既存ドキュメントを壊さない)。目標形は[付録 B](#付録-b-cli-全体シノプシス-目標形)。 + +### 3.2 「キャプチャ」と「活用」の分離 + +これまで: キャプチャ時にしかデータを見られない。 +これから: Fjall ストアを唯一の真実とし、`export` / `replay` / `mock` / `spec` はすべて**ストアからの読み出しで動く独立サブコマンド**にする。これにより各機能が疎結合になり、LLM エージェントがタスク単位で並行実装できる。 + +### 3.3 スキーマバージョニング + +`HttpTrace` の永続化表現と JSONL 出力に `schema_version` を導入(P1-3)。フィールド追加は同一メジャー内で常に「追加のみ・省略可能」とする。 + +### 3.4 クレート追加方針 + +新機能は既存クレートの肥大化を避けて追加する: + +``` +crates/ + phantom-core/ # (既存) 型・トレイト。今後も I/O なしを厳守 + phantom-storage/ # (既存) Fjall + phantom-capture/ # (既存) proxy / ldpreload / fault + phantom-tui/ # (既存) Ratatui + phantom-agent/ # (既存) LD_PRELOAD dylib + phantom-export/ # (新規, P2-1) HAR / curl / JSONL 変換 + phantom-replay/ # (新規, P2-3, P2-4) replay クライアント + mock サーバー + phantom-spec/ # (新規, P3-1) OpenAPI 推論 +``` + +依存方向は常に `main → phantom-* → phantom-core` を維持し、横方向依存(例: `phantom-replay → phantom-export`)は禁止。共有が必要な型は `phantom-core` へ昇格させる。 + +--- + +## 4. ロードマップ全体像 + +| Phase | バージョン | テーマ | 期間目安 | 完了条件(エグジット基準) | +|---|---|---|---|---| +| 0 | v0.1.x | リポジトリ健全化・基盤整備 | 1–2 週 | Open PR ゼロ、CA 永続化で `curl`/Python の HTTPS が `--insecure` なしで復号できる、README 完備 | +| 1 | v0.2 | 信頼できる HTTP デバッガ | 3–4 週 | gzip API を TUI で読める、`?` でヘルプが出る、`status:5xx` で絞れる、機微ヘッダをマスクできる | +| 2 | v0.3 | Web 開発ワークフロー統合 | 4–6 週 | HAR エクスポート → Chrome DevTools で開ける、`phantom mock` で記録から API スタブが立つ、SSE がリアルタイムに見える | +| 3 | v0.4 | 仕様生成とエコシステム統合 | 4–6 週 | 実トラフィックから valid な OpenAPI 3.1 が出る、バイナリ配布(GitHub Releases + Homebrew) | +| 4 | v1.0+ | 研究トラック(bpftime / Arazzo / GUI / DB プロトコル) | 未定 | 着手条件: Phase 3 完了 + 外部ユーザーからの需要シグナル | + +**タスク ID 規約**: `P-<番号>`。付録 C に全タスクの依存グラフを掲載。 + +--- + +## 5. Phase 0: リポジトリ健全化と基盤整備 (v0.1.x) + +### P0-1: PR #4 (Java 統合テスト) の rebase とマージ + +- **目的**: ポリグロット対応(Node 以外でも動く)の回帰保証を得る。Open PR を滞留させない。 +- **依存**: なし +- **規模**: S +- **変更対象**: ブランチ `claude/add-springboot-tests-ZiHDa`(PR #4)。プロダクトコードの変更なし。 +- **実装手順**: + 1. `git fetch origin main claude/add-springboot-tests-ZiHDa` + 2. PR ブランチを最新 `main` (fault injection 込み) に rebase。競合は `tests/` 配下と `Cargo.toml` の dev-dependencies に限られる見込み。プロダクトコードに手を入れないこと。 + 3. ローカルで `cargo test --test proxy_springboot_integration -- --nocapture` を実行(java/mvn 不在なら skip されることを確認)。 + 4. `make check` が通ることを確認して push、CI green を待ってマージ。 + 5. マージ後、`AGENTS.md` の「Testing」節に Java テスト 2 本の行を追記する。 +- **受け入れ基準**: + - [ ] PR #4 がマージ済みで、CI が green。 + - [ ] `AGENTS.md` に Java 統合テストの実行方法が記載されている。 +- **備考**: CI ランナーに Java 17+/Maven がない場合はテストが skip される。それで良い(ローカル検証用)。CI に Java を入れるかは P0-5 で判断。 + +### P0-2: PR #2 (MySQL トレース) のクローズと Issue 化 + +- **目的**: コンセプト外の大型 PR を敬意をもって棚上げし、main との乖離拡大を止める(§1.2 参照)。 +- **依存**: なし +- **規模**: S +- **実装手順**: + 1. PR #2 に次の趣旨のコメントを投稿する: 「HTTP コア体験を優先するロードマップ(ROADMAP.md §1.2, §9 P4-4)に基づき一旦クローズする。パケット解析の設計は Phase 4 のプロトコルプラグイン化で再利用する。」 + 2. PR をクローズする(ブランチは削除しない)。 + 3. GitHub Issue「Protocol plugin architecture: MySQL/PostgreSQL tracing (deferred, see ROADMAP P4-4)」を作成し、PR #2 へのリンクと、P4-4 の設計方針(イベントモデル一般化が前提)を記載する。 +- **受け入れ基準**: + - [ ] PR #2 がクローズされ、経緯コメントがある。 + - [ ] 対応する Issue が存在し、PR #2 とロードマップにリンクしている。 + +### P0-3: CA 証明書の永続化・信頼ストア自動注入・`HTTPS_PROXY` 設定 【最重要】 + +- **目的**: G1・G2 の解消。**Node 以外のあらゆるクライアント(curl, Python, Go, Java, ブラウザ)で、TLS 検証を無効化せずに HTTPS を復号できるようにする。** これが直らない限り「HTTPS 対応」は実質 Node 専用機能である。 +- **依存**: なし(P0-6 のサブコマンド化と同時に進めると `phantom cert` を自然に置ける) +- **規模**: M +- **変更対象**: + - `crates/phantom-capture/src/proxy.rs` — `generate_ca()` を「ロードまたは生成」に変更 + - `src/main.rs` — `spawn_proxy_child()` の環境変数注入、`cert` サブコマンド + - `docs/how-to-use.ja.md`, `README.md` — 信頼手順の記載 +- **実装手順**: + 1. **CA の永続化**: `proxy.rs` の `generate_ca()` を `load_or_generate_ca(data_dir: &Path)` に変更する。 + - 保存先: `/ca/phantom-ca.key.pem`(秘密鍵, パーミッション 0600)と `/ca/phantom-ca.cert.pem`。 + - 両ファイルが存在すれば `rcgen::KeyPair::from_pem()` / `CertificateParams::from_ca_cert_pem()` で復元、なければ生成して書き出す。 + - 有効期限は生成時から 10 年。CommonName は現行どおり `Phantom Proxy CA`。 + - `ProxyCaptureBackend::new()` のシグネチャに `data_dir` を渡す必要がある。`src/main.rs` から `cli.data_dir` を引き回すこと。 + 2. **子プロセスへの信頼注入**: `spawn_proxy_child()` で以下の環境変数を追加設定する(いずれも CA cert の PEM パスを指す)。これにより主要スタックが**検証を無効化せず** Phantom CA を信頼する: + - `HTTPS_PROXY` / `https_proxy` = `http://127.0.0.1:` ← G2 修正 + - `NO_PROXY` / `no_proxy` は**設定しない**(ユーザーの既存値を尊重、上書きもしない) + - `SSL_CERT_FILE`(OpenSSL 系/Ruby/一部 Go)= CA PEM パス + - `CURL_CA_BUNDLE`(curl)= CA PEM パス + - `REQUESTS_CA_BUNDLE`(Python requests)= CA PEM パス + - `NODE_EXTRA_CA_CERTS`(Node.js)= CA PEM パス + - `DENO_CERT`(Deno)= CA PEM パス + - 注意: 既にユーザーが同名変数を設定している場合は**上書きせず、警告を `eprintln!` する**。 + 3. **`phantom cert` サブコマンド**(P0-6 のサブコマンド基盤の上に実装): + - `phantom cert path` — CA cert の絶対パスを stdout に出力(スクリプト用)。 + - `phantom cert export [--out FILE]` — PEM を指定先へコピー(デフォルト: カレントの `phantom-ca.cert.pem`)。 + - `phantom cert print` — PEM 本文を stdout へ。 + - OS 信頼ストアへの自動インストールは**行わない**(システム改変は明示操作であるべき)。代わりに `cert export` の出力末尾に、macOS (`security add-trusted-cert`)・Ubuntu (`update-ca-certificates`)・Windows (`certutil`) の手動コマンド例を eprintln で案内する。 + 4. **proxy-preload.js の簡素化検討はしない**(既存の Node 経路は動いているため触らない。`NODE_EXTRA_CA_CERTS` は preload 非対応クライアントの保険となる)。 + 5. テスト: + - 単体: `load_or_generate_ca` が (a) 初回生成、(b) 再起動時に同一 cert を復元、(c) 壊れた PEM の場合に再生成しエラーにしない、をカバー。 + - 統合: `tests/proxy_curl_https_integration.rs` を新設。rustls のモックHTTPSバックエンド(既存テストの流儀を踏襲)に対し `phantom --output jsonl -- curl https://127.0.0.1:/api/health` を実行し、**`--insecure` なしで** trace が取れることを確認。curl 不在なら skip。 +- **受け入れ基準**: + - [ ] Phantom を 2 回起動しても CA cert のフィンガープリントが同一。 + - [ ] `phantom -- curl https://httpbin.org/get`(または統合テストのモック)が `--insecure` なしで復号キャプチャできる。 + - [ ] `phantom -- python -c "import requests; requests.get('https://...')"` が検証エラーにならない(手動確認で可)。 + - [ ] `phantom cert path` が存在するファイルパスを返す。 + - [ ] 既存の Node 統合テストが全て green のまま。 +- **セキュリティ注意**: 秘密鍵は 0600。`--data-dir` がプロジェクト内を指す場合に鍵がコミットされないよう、`ca/` ディレクトリに `.gitignore`(中身 `*`)を書き出すこと。 + +### P0-4: README の全面執筆 + +- **目的**: G9 の解消。初見の人間と LLM エージェントが 30 秒で価値を理解し 3 分で動かせる入口を作る。 +- **依存**: なし(P0-3 完了後に HTTPS 手順を反映するのが望ましい) +- **規模**: S +- **変更対象**: `README.md` +- **実装手順**: 以下の構成で英語で書く(日本語版は `docs/how-to-use.ja.md` が既にあるためリンクする): + 1. タグライン: "Zero-instrumentation API observability toolbox for modern web development — observe, perturb, record, and spec your app's HTTP traffic without changing a line of code." + 2. 30 秒デモ(コードブロック 3 つ): `phantom -- node app.js` / `phantom --output jsonl -- npm test | jq ...` / `phantom --fault delay:500ms:/api -- node app.js` + 3. 機能一覧表(§1.1 を英訳・簡約) + 4. Installation(現状は `cargo build --release`。P3-5 後に更新) + 5. How it works(プロキシ/preload/LD_PRELOAD の 3 行説明 + 図) + 6. JSONL schema へのリンク(`AGENTS.md` の表を `docs/jsonl-schema.md` に切り出して参照) + 7. Roadmap(本ファイルへのリンク)・License +- **受け入れ基準**: + - [ ] README に上記 7 セクションが存在する。 + - [ ] 記載コマンドがすべて現行 CLI で実際に動く(コピペ検証すること)。 + +### P0-5: CI 強化(macOS 追加・統合テスト分離) + +- **目的**: プロキシは「クロスプラットフォーム」を謳うため、最低限 macOS でのビルド+単体テストを CI で保証する。統合テスト(Node/Java)は時間がかかるため別ジョブ化する。 +- **依存**: P0-1 +- **規模**: S +- **変更対象**: `.github/workflows/ci.yml` +- **実装手順**: + 1. 既存ジョブを `lint`(fmt+clippy, ubuntu)と `test`(matrix: `ubuntu-latest`, `macos-latest`)に分割。 + 2. `integration` ジョブ(ubuntu のみ)を追加: `actions/setup-node@v4` (Node 22) を入れて `cargo test --test proxy_node_integration --test fault_injection -- --nocapture`。Java テストは `actions/setup-java@v4` (temurin 17) + Maven キャッシュを設定して実行(所要時間が 10 分を超えるようなら `schedule` + `workflow_dispatch` に降格して良い)。 + 3. `Swatinem/rust-cache@v2` を全ジョブに導入。 +- **受け入れ基準**: + - [ ] macOS で build + 単体テストが green。 + - [ ] Node 統合テストが CI で実行されている(skip でなく)。 + +### P0-6: CLI サブコマンド基盤の導入(後方互換) + +- **目的**: §3.1。Phase 1 以降の `export` / `replay` / `mock` / `spec` / `cert` を置ける構造を先に作る。 +- **依存**: なし(P0-3 と同一 PR でも可) +- **規模**: M +- **変更対象**: `src/main.rs`(必要なら `src/cli.rs` に分離) +- **実装手順**: + 1. `clap` の `Subcommand` を導入し、`enum Command { Run(RunArgs), Cert(CertArgs) }` を定義。既存の全フラグ(`--backend`, `--output`, `--port`, `--insecure`, `--data-dir`, `--agent-lib`, `--fault`, `-- CMD`)は `RunArgs` へ移す。 + 2. **後方互換**: サブコマンドが指定されない場合(`phantom --port 9090 -- node app.js` 等)は `Run` として解釈する。実装方法: トップレベル `Cli` 構造体に `#[command(subcommand)] command: Option` と `#[command(flatten)] run: RunArgs` を両方持たせ、`command == None` なら `run` を使う。`clap` の `args_conflicts_with_subcommands = true` を設定すること。 + 3. ヘルプ文(`long_about` / `after_long_help`)は `run` サブコマンド側へ移し、トップレベルにはサブコマンド一覧と 3 行の概要を置く。 + 4. 既存統合テストのコマンドライン(サブコマンドなし形式)を**変更せずに**全て通す。これが後方互換の回帰テストになる。 + 5. `docs/how-to-use.ja.md` の CLI リファレンスを更新。 +- **受け入れ基準**: + - [ ] `phantom -- node app.js`(旧形式)と `phantom run -- node app.js`(新形式)が同一動作。 + - [ ] `phantom --help` にサブコマンド一覧、`phantom run --help` に詳細ヘルプが出る。 + - [ ] 既存統合テストが無変更で green。 + +--- + +## 6. Phase 1: コアユーザビリティ — 「信頼できる HTTP デバッガ」 (v0.2) + +**テーマ**: 「表示されたものが正しく読める・探せる・機械が使える」。日常のデバッグで mitmproxy の代わりに選べる品質にする。 + +### P1-1: Content-Encoding の透過デコード + +- **目的**: G3 の解消。gzip/brotli/zstd/deflate の応答ボディを人間可読で保存・表示する。 +- **依存**: なし +- **規模**: M +- **変更対象**: `crates/phantom-capture/src/proxy.rs`, `crates/phantom-core/src/trace.rs`, `crates/phantom-capture/Cargo.toml` +- **実装手順**: + 1. 依存追加: `flate2`(gzip/deflate)、`brotli`、`zstd`。workspace deps に追加してから参照。 + 2. `proxy.rs` のボディ収集後(`collect_body` の呼び出し側)で、レスポンスヘッダの `content-encoding` を見てデコードする関数 `fn decode_body(encoding: &str, bytes: &[u8]) -> Option>` を実装。対応: `gzip`, `deflate`, `br`, `zstd`, `identity`。複合(`gzip, br`)は右から順に解く。 + 3. デコード成功時: `HttpTrace` にはデコード済みボディを格納し、新フィールド `content_encoding: Option` に元のエンコーディング名を記録する(付録 A 参照)。デコード失敗時: 元バイト列をそのまま格納し `tracing::warn!` を出す(エラーで通信を壊さない)。 + 4. **重要**: プロキシが下流へ返すレスポンス本体は**改変しない**(デコードは記録用コピーに対してのみ行う)。圧縮のままクライアントへ流す。 + 5. リクエストボディの `content-encoding` も同様に処理(稀だが gRPC-web 等で存在する)。 + 6. デコードは 1MB 制限(G4)の**後**ではなく**前**に適用するか?→ **圧縮バイトを最大 1MB まで収集 → デコード → デコード結果も `MAX_BODY_SIZE` で再度打ち切り**とする(zip bomb 対策としてデコード後サイズ上限は必須)。 + 7. 単体テスト: gzip した JSON を復号できる/壊れた gzip で panic しない/zstd 済みボディがデコード後上限で切られる、の 3 ケースを `proxy.rs` の `#[cfg(test)]` に追加。 + 8. 統合テスト: Node モックバックエンドに `Content-Encoding: gzip` 応答を追加し、JSONL の `response_body` が平文 JSON であることを assert。 +- **受け入れ基準**: + - [ ] gzip 応答の API を TUI で開くと JSON が読める。 + - [ ] JSONL に `content_encoding: "gzip"` が入り、`response_body` は展開済み。 + - [ ] クライアントが受け取るレスポンスはバイト単位で無改変(統合テストで body 一致を assert)。 + +### P1-2: ボディ切り捨てとバイナリの明示化 + +- **目的**: G4 の解消。「1MB で黙って切れる」「バイナリが文字化けする」を仕様として可視化する。 +- **依存**: P1-1(同じ箇所を触るため直後に実施) +- **規模**: S +- **変更対象**: `crates/phantom-core/src/trace.rs`, `crates/phantom-capture/src/proxy.rs`, `src/main.rs`(JSONL)、`crates/phantom-tui/src/ui.rs` +- **実装手順**: + 1. `HttpTrace` に追加: `request_body_truncated: bool`, `response_body_truncated: bool`(デフォルト false、serde default で後方互換)。 + 2. `run` に `--max-body ` フラグを追加(デフォルト `1mb`。`512kb`/`5mb` のような表記を受け付けるパーサを実装、`0` で無制限)。`MAX_BODY_SIZE` 定数を設定値に置き換える。 + 3. バイナリ判定: ボディ先頭 8KB に NUL バイトが含まれる、または UTF-8 として不正な割合が 10% を超える場合はバイナリとみなす。JSONL では `request_body_encoding` / `response_body_encoding` フィールド(`"utf-8"` | `"base64"`)を追加し、バイナリは base64 で出力する(付録 A)。 + 4. TUI 詳細ペイン: 切り捨て時は `[body truncated at 1.0 MB — rerun with --max-body 0]`、バイナリ時は `[binary body, 24.3 KB, image/png]` のプレースホルダ表示。 +- **受け入れ基準**: + - [ ] 2MB の応答で `response_body_truncated: true` が JSONL に出る。 + - [ ] PNG 応答が JSONL で valid な base64 + `"base64"` マーカーになる(jq でパースが壊れない)。 + +### P1-3: JSONL スキーマ v2 と `phantom export jsonl` の下地 + +- **目的**: G6・G11 の解消。スキーマを versioned な公開契約に格上げする。 +- **依存**: P1-1, P1-2(新フィールドを v2 に含めるため) +- **規模**: S +- **変更対象**: `src/main.rs`(`JsonlTrace`)、`docs/jsonl-schema.md`(新規)、`AGENTS.md` +- **実装手順**: + 1. `JsonlTrace` に `schema_version: u32`(値 `2`)を先頭フィールドとして追加。P1-1/P1-2 の新フィールドも追加。 + 2. `docs/jsonl-schema.md` を新規作成し、付録 A の表を正とする完全なスキーマ文書を書く。`AGENTS.md`・`--help` のスキーマ記述をこの文書への参照+要約に差し替える(3 箇所に同じ表を持たない)。 + 3. 互換性ポリシーを文書に明記: 「同一 `schema_version` 内ではフィールド追加のみ。削除・型変更・意味変更は version increment を伴う」。 +- **受け入れ基準**: + - [ ] 全 JSONL 行に `schema_version: 2` が含まれる。 + - [ ] `docs/jsonl-schema.md` が存在し、実装と一致する(統合テストで全フィールドの存在を assert)。 + +### P1-4: TUI 強化(第 1 弾) + +- **目的**: G5 の解消。「見る」体験を実用レベルへ。 +- **依存**: なし(P1-1 が入っているとボディ表示の価値が上がる) +- **規模**: L(内部で 4 つの独立 PR に分割可能。以下の 1.〜4. がそれぞれ 1 PR) +- **変更対象**: `crates/phantom-tui/src/app.rs`, `ui.rs`, `event.rs`, `lib.rs` +- **実装手順**: + 1. **構造化フィルタ** (`/` で入力): 現行の URL 部分一致に加え、`status:404` `status:4xx` `status:>=500` `method:post` `host:api.example.com` `path:/users` を空白区切り AND で解釈するミニクエリ言語を実装する。キーワードなしトークンは従来どおり URL 部分一致。パーサは `app.rs` に純関数 `fn parse_filter(input: &str) -> FilterExpr` として実装し、単体テストを最低 8 ケース書く(不正入力はプレーン部分一致にフォールバック)。 + 2. **ヘルプオーバーレイ**: `?` でキーバインド一覧をモーダル表示、`Esc`/`?` で閉じる。フィルタ構文のチートシートも載せる。 + 3. **詳細ペインの強化**: (a) `j/k` スクロールとスクロール位置インジケータ、(b) レスポンスボディが `content-type: application/json` のとき `serde_json` で pretty-print(失敗したら原文)、(c) タブ切替(`h/l` または `[`/`]`)で Request / Response / Headers / Timing のセクション移動。 + 4. **コピー & エクスポートキー**: 選択トレースに対し `c` = curl コマンドとしてクリップボードへ(依存: `arboard`。クリップボード不可の環境では stderr に出力)、`w` = 単一トレースを JSON ファイルとして `./phantom-trace-.json` に書き出し。curl 生成ロジックは後で P2-1 と共有するため `phantom-core` ではなく新設の変換モジュールに置く(P2-1 実装時に `phantom-export` へ移動)。 + 5. すべての新キーはヘルプオーバーレイと `docs/how-to-use.ja.md` の表に反映する。 + 6. TUI の状態遷移は既存規約どおり `App` のメソッドとして実装し、レンダリング関数は純関数を維持する。`app.rs` の単体テストでフィルタ・スクロール・タブ状態を検証。 +- **受け入れ基準**: + - [ ] `/status:5xx method:get` で該当トレースのみ表示。 + - [ ] `?` でヘルプが出る。 + - [ ] JSON 応答が整形表示される。 + - [ ] `c` で有効な curl コマンドが得られる(手動で再実行して同じレスポンスが返ること)。 + +### P1-5: 機微情報のリダクション + +- **目的**: G8 の解消。トークンや Cookie が平文でディスク・JSONL・クリップボードに漏れることを防ぐ手段を提供する。 +- **依存**: P1-3(スキーマに `redacted` 表現を追加するため) +- **規模**: M +- **変更対象**: `crates/phantom-core/src/trace.rs`(または新設 `redact.rs`)、`src/main.rs`, `crates/phantom-capture/src/proxy.rs` +- **実装手順**: + 1. `run` にフラグ追加: `--redact`(既定リストを有効化)、`--redact-header `(繰り返し可、追加指定)、`--redact-body-field `(繰り返し可)。 + 2. 既定リスト: ヘッダ `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-api-key`; ボディの JSON キー `password`, `token`, `access_token`, `refresh_token`, `client_secret`, `api_key`。 + 3. リダクション適用箇所: **トレースがチャネルに送られる前**(`proxy.rs` の `HttpTrace` 構築直後)に 1 回だけ適用する。値は `"[REDACTED]"` に置換(長さ情報も残さない)。ストレージ・TUI・JSONL・エクスポートすべてに一貫して効く。 + 4. ボディの JSON キー置換は、パース可能な JSON のみ対象にトップレベルから再帰的にキー一致(大文字小文字無視)で置換する。パース不能ボディは触らない。 + 5. デフォルトは **off**(ローカルデバッグでは生値が見たい)。ただし `docs/how-to-use.ja.md` に「トレースを共有・コミットする場合は必ず `--redact` を使う」と明記。 + 6. 単体テスト: ヘッダ置換 / ネスト JSON の置換 / 非 JSON ボディ無変更 / 大文字ヘッダ名の一致。 +- **受け入れ基準**: + - [ ] `--redact` 付きで `authorization` ヘッダが JSONL・TUI ともに `[REDACTED]`。 + - [ ] `--redact-body-field secret` でボディ内 `"secret"` キーの値が置換される。 + +### P1-6: 非 Node ランタイムの透過対応マトリクス整備 + +- **目的**: P0-3 の環境変数注入が実際に各言語で機能することを検証・文書化し、動かないものを直す。「Node 専用ツール」の印象を払拭する。 +- **依存**: P0-3 +- **規模**: M +- **変更対象**: `tests/`(新規統合テスト)、`docs/how-to-use.ja.md`, `README.md` +- **実装手順**: + 1. `tests/apps/python-app/client.py` を新設: `urllib.request` と(あれば)`requests` で HTTP/HTTPS 各 1 リクエスト。`tests/proxy_python_integration.rs` から起動し、python3 不在なら skip。**期待**: P0-3 の `HTTPS_PROXY` + `REQUESTS_CA_BUNDLE`/`SSL_CERT_FILE` だけで復号キャプチャできること。 + 2. 同様に `tests/apps/go-app/`(`net/http`、`go` 不在なら skip)。Go は `HTTPS_PROXY` と `SSL_CERT_FILE` を尊重する。 + 3. curl 統合テスト(P0-3 で作成済みならスキップ)。 + 4. 結果を `docs/compatibility.md`(新規)にマトリクスとして記録: 行 = ランタイム/ライブラリ、列 = HTTP キャプチャ / HTTPS キャプチャ / 必要条件。「未検証」も正直に書く。 +- **受け入れ基準**: + - [ ] Python / Go / curl の統合テストが存在し、ローカルで green(CI では処理系があれば実行)。 + - [ ] `docs/compatibility.md` が存在し README からリンクされる。 + +--- + +## 7. Phase 2: Web 開発ワークフロー統合 (v0.3) + +**テーマ**: 記録したトラフィックを「資産」に変える。Record & Replay / Perturb の柱を完成させる。 + +### P2-1: `phantom export` サブコマンド(HAR / JSONL / curl) + +- **目的**: G6 解消の中核。記録済みトレースを標準形式で取り出し、Chrome DevTools・他ツール・スクリプトへ渡せるようにする。 +- **依存**: P0-6(サブコマンド基盤)、P1-3(スキーマ v2) +- **規模**: L +- **変更対象**: 新規クレート `crates/phantom-export/`、`src/main.rs`、`crates/phantom-core/src/storage.rs`(クエリ拡張) +- **実装手順**: + 1. `phantom-core` の `TraceStore` トレイトに範囲クエリを追加: `fn query(&self, opts: &QueryOptions) -> Result, StorageError>`。`QueryOptions { since: Option, until: Option, url_contains: Option, method: Option, status_range: Option<(u16, u16)>, limit: usize }`。Fjall 実装は `by_time` パーティションの prefix scan で `since`/`until` を効かせ、残りはフィルタで良い(データ量はローカル規模)。 + 2. 新クレート `phantom-export` を作成(§3.4 の依存規則に従う)。実装する変換: + - `to_har(traces: &[HttpTrace]) -> serde_json::Value` — HAR 1.2 準拠。`log.creator = {name: "phantom", version: env!("CARGO_PKG_VERSION")}`。`startedDateTime` は ISO 8601、`time` は `duration_ms`。ボディは `postData.text` / `content.text`(base64 の場合 `encoding: "base64"`)。HAR 1.2 スキーマの必須フィールド(`cache: {}`, `timings` は send/wait/receive に分配できないため `wait = duration` で他 0)を漏らさないこと。 + - `to_curl(trace: &HttpTrace) -> String` — P1-4 で作った curl 生成をこのクレートへ移設して共有。 + - `to_jsonl(trace: &HttpTrace) -> JsonlTrace` — `src/main.rs` の変換ロジックをこのクレートへ移設(main は薄く保つ)。 + 3. CLI: `phantom export --format har|jsonl|curl [--out FILE] [--since 1h] [--url-contains STR] [--method GET] [--status 400-599] [--limit N] [--data-dir DIR]`。`--since` は `30m`/`2h`/`7d` の相対表記と RFC3339 を受理。`--out` 省略時は stdout。 + 4. テスト: HAR 出力を `serde_json` で再パースし必須フィールドを assert する単体テスト。Chrome DevTools への手動インポート確認を PR 説明に記載。 +- **受け入れ基準**: + - [ ] `phantom export --format har --out session.har` の成果物が Chrome DevTools の Network タブにインポートできる。 + - [ ] `--since 1h --status 500-599` で絞り込みが効く。 + - [ ] `src/main.rs` の JSONL 変換が `phantom-export` へ移動しても既存統合テストが green。 + +### P2-2: HAR インポートと TUI での閲覧 + +- **目的**: ブラウザで記録した HAR(DevTools からの書き出し)を Phantom の TUI・エクスポート・(将来の)mock で扱えるようにし、双方向の interop を完成させる。 +- **依存**: P2-1 +- **規模**: M +- **変更対象**: `crates/phantom-export/`(`from_har`)、`src/main.rs`(`phantom import`) +- **実装手順**: + 1. `phantom-export` に `from_har(json: &serde_json::Value) -> Result, ExportError>` を実装。trace_id/span_id は新規採番。復元できないフィールド(`source_addr` 等)は `None`。 + 2. CLI: `phantom import [--data-dir DIR]` — パースしてストアへ insert し、件数を表示。 + 3. `phantom import file.har && phantom view` で閲覧できる…が `view` は未定義のため、本タスクで **`phantom view`**(キャプチャなしでストアを TUI 閲覧する読み取り専用モード)も追加する。実装は `run_tui` に空のダミーチャネルを渡すだけで成立する(`lib.rs` 参照)。 +- **受け入れ基準**: + - [ ] Chrome からエクスポートした実 HAR がエラーなくインポートできる(不正エントリは warn してスキップ)。 + - [ ] `phantom view` で過去トレースが閲覧できる(プロキシは起動しない)。 + +### P2-3: `phantom replay` — 記録リクエストの再送 + +- **目的**: 「さっきの失敗リクエストをもう一度」「API 修正後に同じ呼び出しで確認」を 1 コマンドにする。 +- **依存**: P2-1(クエリ基盤) +- **規模**: M +- **変更対象**: 新規クレート `crates/phantom-replay/`、`src/main.rs` +- **実装手順**: + 1. `phantom-replay` に `reqwest`(rustls 構成)ベースの再送機能を実装: `async fn replay(trace: &HttpTrace, opts: &ReplayOptions) -> Result`。記録どおりの method/url/headers/body を送り、結果を新しい `HttpTrace`(新規 trace_id、`request_headers` に `x-phantom-replay-of: <元 span_id>` を付与)として返す。 + 2. 除外ヘッダ: `host`, `content-length`, `connection`, `accept-encoding` は reqwest に任せて再設定する。 + 3. `ReplayOptions { override_host: Option, override_scheme: Option, timeout: Duration }` — `--host localhost:3000` で本番記録をローカル実装に投げ直せるのが主要ユースケース。 + 4. CLI: `phantom replay [--span-id ID | --last N | --url-contains STR ...(P2-1 と同じフィルタ群)] [--host HOST] [--save] [--output jsonl|summary]`。`--save` 指定時のみ結果をストアへ保存。デフォルト出力は ` → → ()` の比較サマリ 1 行/件。 + 5. 安全装置: フィルタが 20 件超に一致し、かつ `--yes` がない場合は件数を表示して中断する(誤って本番へ大量再送しない)。 +- **受け入れ基準**: + - [ ] `phantom replay --last 1` が直近トレースを再送し、新旧ステータスの比較を表示する。 + - [ ] `--host` で向き先を差し替えられる。 + - [ ] 21 件一致 + `--yes` なしで中断する。 + +### P2-4: `phantom mock` — 記録からのモックサーバー生成 + +- **目的**: 外部 API 依存を切り離したオフライン開発・決定論的テストを可能にする(Keploy 的価値の中核)。 +- **依存**: P2-1 +- **規模**: L +- **変更対象**: `crates/phantom-replay/`(mock モジュールを同居。§3.4 の表どおり)、`src/main.rs` +- **実装手順**: + 1. `axum` ベースの HTTP サーバーを実装。起動時にストアから対象トレース群(P2-1 のフィルタ)をロードし、ルーティングテーブルを構築する。 + 2. **マッチング仕様**(この順で最初に一致したものを返す): + 1. method + path + query 完全一致 + 2. method + path 完全一致(query 無視) + 3. method + パス構造一致(パスセグメント単位で、記録側セグメントが数値/UUID/16 進 24 文字以上なら任意値にマッチ — P3-1 のパラメータ推定と同じヒューリスティクスを共有) + 4. 不一致 → `501 Not Implemented` + JSON ボディ `{"phantom_mock": "no recorded response", "method": ..., "path": ...}` + 3. 同一キーに複数レコードがある場合は「記録順に順番に返し、尽きたら最後を繰り返す」(シーケンシャルモード。ステートフル API の連続呼び出しを再現するため)。 + 4. レスポンスは記録どおりの status/headers/body。`content-encoding` は既に展開済み(P1-1)なので、当該ヘッダと `content-length` を除去して平文で返す。 + 5. オプション: `--port 9000`(デフォルト)、`--latency recorded|none|`(デフォルト `none`。`recorded` は `duration_ms` を sleep)、`--cors`(全許可 CORS ヘッダ付与 + OPTIONS 自動応答)。 + 6. 起動ログに「ロードしたルート一覧(method path → status, N variants)」を表示する。 + 7. 統合テスト: 記録 → `phantom mock` 起動 → reqwest で叩いて記録どおりの応答が返る/未知パスが 501、をエンドツーエンドで検証。 +- **受け入れ基準**: + - [ ] `phantom mock --url-contains api.example.com --port 9000` で、記録済み `GET /users/42` に対し `GET /users/99` もパラメータ一致で応答する。 + - [ ] `--latency recorded` で記録レイテンシが再現される(±20ms 許容で統合テスト)。 + - [ ] `--cors` 指定でブラウザからの preflight が通る。 + +### P2-5: SSE / ストリーミング応答と WebSocket のキャプチャ + +- **目的**: G7 の解消。モダン Web 開発(LLM ストリーミング、リアルタイム UI)で SSE/WS は不可避。「全読みしてから記録」の設計限界を破る。 +- **依存**: P1-3(スキーマ拡張を伴うため) +- **規模**: L(SSE と WS で PR を分けること) +- **変更対象**: `crates/phantom-capture/src/proxy.rs`, `crates/phantom-core/src/trace.rs`, `crates/phantom-tui/`, `docs/jsonl-schema.md` +- **実装手順(SSE / ストリーミング)**: + 1. レスポンスヘッダが `content-type: text/event-stream`、または `transfer-encoding: chunked` かつボディ収集が `--stream-timeout`(新フラグ、デフォルト 10s)を超えた場合、**部分トレース**として扱う。 + 2. 実装方式: ボディを tee するストリームラッパーを作り、(a) クライアントへは即時パススルー、(b) 記録側はチャンク到着ごとにバッファへ追記。**レスポンスヘッダ受信時点で `HttpTrace` を `status: "open"` で一度 emit** し、ストリーム終了(または `MAX_BODY_SIZE` 到達)時に完全版を同一 span_id で再 emit する。 + 3. スキーマ追加(v2 内の追加なので互換): `stream: bool`, `stream_complete: bool`, `chunk_count: Option`。JSONL モードでは open/complete の 2 行が出る(同一 `span_id` で識別可能)ことを `docs/jsonl-schema.md` に明記。 + 4. TUI: ストリーミング中のトレースに `⟳` インジケータ、完了で消す。ボディは SSE の場合 `data:` 行単位で改行表示。 +- **実装手順(WebSocket)**: + 1. hudsucker の `WebSocketHandler` トレイトを実装し、フレーム(Text/Binary/Close)をキャプチャする。 + 2. 新イベント型はあえて作らず、`HttpTrace` を「ハンドシェイク 1 件(101 応答)」として記録し、各メッセージは新フィールド `ws_messages: Option>`(`WsMessage { direction: "send"|"recv", opcode, timestamp_ms, payload(1 メッセージ 64KB 上限, base64 可) }`)として同一トレースへ追記していく。上限 1000 メッセージ/接続、超過分はカウントのみ。 + 3. ストレージ更新: `TraceStore` に `update(&self, trace: &HttpTrace)` を追加(同一キー上書きで実現可能)。 + 4. TUI: 詳細ペインにメッセージのタイムライン表示(方向矢印 + ペイロード先頭 1 行)。 +- **受け入れ基準**: + - [ ] SSE エンドポイント(統合テストにモック追加)で、最初のイベント受信から 1 秒以内に TUI に行が現れる。 + - [ ] JSONL で open → complete の 2 行が同一 span_id で出る。 + - [ ] `wss://` エコーサーバー(テスト内モック)の送受信フレームが記録される。 + +### P2-6: セッション管理とタグ付け + +- **目的**: 「昨日の記録と今日の記録が混ざる」問題を解消し、export/mock/replay の対象指定を人間的にする。 +- **依存**: P2-1 +- **規模**: M +- **変更対象**: `crates/phantom-core/src/trace.rs`, `crates/phantom-storage/src/fjall_store.rs`, `src/main.rs`, `crates/phantom-tui/` +- **実装手順**: + 1. `HttpTrace` に `session: String` を追加(serde default = `"default"`)。 + 2. `run` に `--session ` を追加。省略時は `default`。加えて `--fresh` フラグ(そのセッションの既存データを起動時に削除)。 + 3. ストレージ: 新パーティション `by_session`(キー `{session}\0{timestamp_be}{span_id}`)を追加し、`QueryOptions` に `session: Option` を追加。既存パーティションは触らない(マイグレーション不要、旧データは `default` 扱い)。 + 4. `phantom sessions list`(件数・期間つき)と `phantom sessions delete ` を追加。 + 5. `export` / `replay` / `mock` / `view` の全フィルタに `--session` を追加。 + 6. TUI: フッターに現行セッション名を表示。フィルタ構文に `session:NAME` を追加(P1-4 のパーサ拡張)。 +- **受け入れ基準**: + - [ ] `phantom run --session checkout-flow -- node app.js` → `phantom export --session checkout-flow --format har` が該当分のみ出力。 + - [ ] `phantom sessions list` が正しい件数を表示。 + - [ ] 旧データ(session フィールドなし)が壊れず `default` として見える。 + +### P2-7: フォールトインジェクション拡張 + +- **目的**: Perturb の柱を完成させる。カオステストの表現力を実務水準へ。 +- **依存**: なし(既存 `fault.rs` の拡張) +- **規模**: M +- **変更対象**: `crates/phantom-capture/src/fault.rs`, `src/main.rs`, `tests/fault_injection.rs` +- **実装手順**: + 1. 新 spec 追加(既存の `delay:` / `error:` 文法と一貫させる): + - `abort:0.1:/api` — 確率 10% で応答前に接続を切断(空応答/RST 相当。hudsucker では 502 を返さずストリームを drop) + - `timeout:30s:/slow` — 応答を保留し続ける(クライアント側タイムアウトの検証用) + - `bandwidth:56kbps` — レスポンスボディをチャンク分割 + sleep でスロットリング + - `rewrite-status:200=503:0.5` — 上流の実応答を得た後にステータスだけ差し替え(error: と違い実処理は走る) + 2. `--fault-file ` を追加。YAML 形式: + ```yaml + rules: + - match: { url_contains: "/api/payment", method: POST } + fault: { type: delay, min_ms: 200, max_ms: 800 } + - match: { url_contains: "/api" } + fault: { type: error, status: 503, probability: 0.1 } + ``` + CLI の `--fault` 文字列は内部でこの構造にパースされる(単一の `FaultRule` 表現に統一)。 + 3. 適用中のルールを TUI ヘッダに表示(例: `faults: 2 active`)。fault が発動したトレースには `fault_injected: Option`(発動ルールの説明文字列)を記録し、TUI で行を黄色表示、JSONL にも出す(スキーマ追加)。 + 4. 統合テスト: 各新 spec につき 1 ケース(`abort` はクライアントエラーになること、`bandwidth` は所要時間下限で検証)。 +- **受け入れ基準**: + - [ ] `--fault abort:1.0` で curl が `(52) Empty reply` 相当で失敗する。 + - [ ] `--fault-file` の YAML が CLI 指定と同じ挙動になる。 + - [ ] JSONL の `fault_injected` でどのルールが発動したか判別できる。 + +### P2-8: Deno / Bun の透過注入 + +- **目的**: モダン JS ランタイムのカバレッジ拡大。 +- **依存**: P0-3 +- **規模**: S +- **変更対象**: `src/main.rs`(`is_node_command` 周辺)、`tests/` +- **実装手順**: + 1. Bun: `bun` は Node 互換の `--require`(`--preload`)を持つ。`is_node_command` を `enum JsRuntime { Node, Bun, Deno }` を返す `detect_js_runtime` に改め、Bun には `--preload