docs/architecture/decisions/0005-use-a-standalone-lowband-wap-cli.md
ADR 0005: Use a Standalone Lowband WAP CLI
Date: 2026-07-29 Status: accepted
Context
Developers repeatedly need to send bounded WAP requests, preserve response bytes, inspect WMLC, and report protocol/decode failures without starting the desktop UI. Existing repository tools cover Docker/Kannel smoke paths and library tests, but there is no small general-purpose client.
The implementation must keep WSP, WDP, and WBXML behavior in transport-rust, reuse the same
fetch policy and codecs as Waves, remain deterministic in CI, and distinguish a resource URL from
the selected WAP proxy/gateway endpoint.
Repository inventory
transport-rustalready ownsfetch_deck_in_process, thegateway-bridgedandwap-net-coreprofiles, connectionless WSP/WDP, destination policy, response bounds, and the pinned WML 1.3 WBXML decoder. The browser host calls this same facade.- Canonical byte fixtures live in
transport-rust/wbxml_samples/andtransport-rust/tests/fixtures/transport/; focused WSP/WDP replays live undertransport-rust/tests/network/interop/. scripts/transport-wap-smoke.sh,make smoke-transport-wap, the ignored Kannel Rust smoke tests, andscripts/native-tauri-kannel-e2e.shexercise progressively larger local stacks. They are valuable release checks, but are not a general request inspector.- The desktop transport contract is generated from exported Rust types into
browser/contracts/transport.tsandbrowser/contracts/generated/transport-host.ts. A CLI-only routing option should remain Rust-only unless the desktop contract also needs it. - The public preview resources are
wap://home.wap.shrimpworks.dev/,wap://forms.wap.shrimpworks.dev/, andwap://interop.wap.shrimpworks.dev/; the established static deep link iswap://home.wap.shrimpworks.dev/examples/index.wml. Their WDP/WSP peer is configured separately as159.89.254.0:9200. - Ordinary contributor verification is already available through
make lint-rust-transport,make test-rust-transport,pnpm --dir browser run contracts:check, andpnpm verify:change.
Curl investigation
As of 2026-07-29, curl’s documented URL schemes are protocols compiled into a curl/libcurl build;
an unsupported scheme returns CURLE_UNSUPPORTED_PROTOCOL. The public libcurl API exposes
transfer handles, callbacks, and supported-protocol discovery, but not a protocol-handler
registration ABI:
Curl’s official guidance for adding a protocol describes contributing protocol code, tests, and documentation to curl itself, with an expectation of wide public use and long-term curl-project maintenance. It does not describe a loadable plugin:
CURLOPT_OPENSOCKETFUNCTION can replace socket creation or supply a connected socket, but
libcurl still executes one of its built-in protocol implementations. It cannot delegate WSP,
WDP, or WBXML semantics to Lowband:
Therefore, a true wap:// curl integration would currently mean carrying a curl fork or pursuing
an upstream protocol implementation. A wrapper that rewrites wap:// to HTTP would only exercise
the repository’s gateway bridge and would conceal the native WSP/WDP path.
Options considered
| Criterion | Curl/libcurl protocol implementation | Curl wrapper | Standalone Rust CLI over Lowband |
|---|---|---|---|
| Protocol fidelity | Potentially high, but duplicates or tightly adapts Lowband inside curl | Low; bridge-only | High; calls the production Lowband facade |
| Raw-byte visibility | Possible after substantial curl integration | HTTP-side bytes only | Existing successful normalized response retains original payload bytes |
| Existing codec reuse | Awkward across curl’s C internals | Decode would need a second tool | Direct reuse of WSP/WBXML Rust code |
| Portability/install weight | Custom curl build and distribution | Requires curl plus wrapper/tool | One Rust binary; no new runtime dependency |
| CI testability | Requires curl’s protocol harness plus repository harness | Easy but incomplete | Deterministic local fixture servers |
| Security/policy parity | Separate policy implementation risk | Curl policy differs from Waves | Existing destination policy, limits, and error taxonomy |
| Maintenance | Curl fork/upstream maintenance | Small, but misleading capability | Small Rust adapter maintained with transport |
Decision
Build wapcurl, a deliberately curl-like standalone Rust binary in transport-rust.
The binary is a thin adapter:
- Parse a bounded GET-only command contract.
- Select native
wap-net-core, HTTP, or the explicit gateway bridge. - Call
fetch_deck_in_process_with_options. - Render metadata, decoded inspection, raw bytes, hex, a file, or JSON.
- Map Lowband’s stable error taxonomy to documented nonzero exit codes.
The Rust facade now accepts FetchTransportOptions, which keeps the resource URL separate from
an optional gateway endpoint. This is a Rust-only seam and does not change the generated browser
contract. The browser, deployment, and engine surfaces are unchanged.
Safety and scope
- Default destination policy remains
PublicOnly; local/private targets require--allow-private. - The default is one attempt.
--retryis capped at two additional attempts. - Whole-request timeout is 5 seconds by default and constrained to 100–30,000 milliseconds.
- HTTP redirects remain capped at 10 and payloads at 524,288 bytes by the shared transport.
- There is no automatic native-to-bridge fallback.
- Verbose trace goes to stderr and redacts credentials, sensitive query values, authorization, proxy authorization, and cookies.
waps://does not imply working WTLS; existing Lowband warnings and limitations remain.- The CLI is a developer probe, not an interactive browser, crawler, cookie jar, or deployment tool.
Consequences
Positive:
- CLI and desktop diagnostics share one protocol implementation and error taxonomy.
- Native public services and deterministic local HTTP/gateway fixtures are both testable.
- No curl fork, plugin ABI, new parser, async runtime, or external decoder is introduced.
- Machine-readable output remains clean because protocol trace is on stderr and opt-in.
Costs and limitations:
- GET is the only CLI method in this slice, even though native Lowband also has constrained POST support.
- Raw response bytes are available after a successful normalized fetch; failure responses do not currently retain rejected payload bytes in the public response contract.
- Native WAP is the existing constrained connectionless WSP/WDP profile, not full WSP connection-mode, WTP, or WTLS.
- Custom per-invocation payload and redirect limits are not yet exposed; the shared safe fixed limits apply.
Rejected alternatives
Carry a curl fork
Rejected because it creates a second protocol integration and distribution stream while the repository already has the required Rust codecs and policy. Upstream curl support could be revisited only if WAP use and long-term maintainers satisfy curl’s protocol-admission bar.
Ship a shell wrapper around curl
Rejected because it can only exercise an HTTP bridge, cannot speak native WSP/WDP, and would either omit WBXML inspection or invoke a parallel decoder.
Add protocol logic to TypeScript
Rejected because it violates the repository’s layer contract and would duplicate untrusted-input handling outside Lowband.