apply_llm_provider_args now resets llm_endpoint to Ollama's local
default when switching to a non-Anthropic provider without an explicit
endpoint and the stored endpoint is still Anthropic's fixed hosted
URL. Prevents a future per-use provider switch (SummaryPanel) that
only sends `provider` from silently leaving llm_endpoint pointed at a
third party for a provider that has no business talking to it.
set_llm_provider now accepts provider "anthropic": the apiKey argument
is written straight to crate::llm::credentials (OS credential store)
and never assigned into Settings, so it structurally cannot reach
settings.json/wa.db (FR-SEC-1). apply_llm_provider_args pulls the
settings-mutation logic into a small pure function specifically so
that guarantee is unit-testable without touching a real keyring.
llm_provider_from_settings gets an "anthropic" arm; api.anthropic.com
joins the settings-derived egress allowlist through the exact same
generic is_local()-based path privacy_self_check_json already uses for
every other provider — no anthropic-specific allowlist code, and the
host only appears once the provider is actually selected (never
unconditionally).
Also fixes pre-existing cargo-fmt drift on one unrelated line this
file's formatter pass touched (CONTENT_RANGE header call).
One-time "data leaves your device" acknowledgment flag for hosted
(non-local) AI providers, persisted like consent_acknowledged so the
banner doesn't nag on every use. serde(default) so it's false for any
settings.json written before this field existed.
Implements AnthropicProvider::status/summarize/suggest_tags against the
real Anthropic Messages API (POST /v1/messages, x-api-key + anthropic-
version headers, SSE streaming for summarize, non-streaming for the
short tag reply), replacing the stub that returned "isn't built yet".
- Adds llm::credentials (mirrors sync::credentials) so hosted API keys
live in the OS credential store, never settings/DB/logs (ADR-0011).
- Fixes OpenAiCompatProvider::api_key() to actually read credential_ref
from the store instead of the hardcoded None left by the T10a.2 stub.
- Splits build_messages/build_tag_messages into system/user halves
(build_system_and_user/tag_system_and_user) since Anthropic's system
prompt is a top-level field, not a messages[0] entry like OpenAI's shape.
- is_local() is unconditionally false for AnthropicProvider (no
unauthenticated mode, unlike OpenAiCompatProvider).
- Adds a loopback raw-socket mock HTTP server (mirrors sync::oauth's
LoopbackRedirect pattern — no HTTP-mock crate in the dependency tree)
and tests proving request/response shape for both AnthropicProvider
and OpenAiCompatProvider against mocked endpoints.
Written to satisfy "an in-process MCP client can call the tools", but
its dependency (reqwest 0.13 via rmcp's client-http feature) pulled in
a very heavy native build (aws-lc-rs) that hadn't finished verifying
when told to stop building. Removing rather than leaving an unverified
test + dependency in the tree. The acceptance criterion it targeted is
still exercised at the unit level: http_transport::tests (loopback
bind/refusal) and mcp::scope::tests (tool-handler scope logic) both
pass under --features mcp; the full wire-protocol round trip is a
follow-up (see final report).
Per instruction to stop iterating on builds: reqwest 0.13's client-http
feature (needed only for an in-process MCP-client e2e test) was pulling
a very slow/heavy native build (aws-lc-rs, even with the TLS backend
stripped down) and the last attempt was still in progress -- unverified.
Reverting Cargo.toml to the last state that was actually confirmed
green (cargo fmt/clippy -D warnings/test, both default and --features
mcp, all passing) rather than ship an unverified dependency change.
The HTTP transport itself (bind_loopback/token gate/serve loop) keeps
its own unit tests, which did pass under --features mcp.
The e2e test only ever talks plain http://127.0.0.1 (never https://), so
a TLS backend is unnecessary weight -- reqwest 0.13's `rustls` feature
pulls in aws-lc-rs, a large C codebase that was taking a very long time
to compile (and once contributed to a pagefile-exhaustion build failure
alongside concurrent cargo invocations). Building reqwest013 with
default-features=false and no TLS feature is enough for the plain-HTTP
client the test needs.
rmcp's transport-streamable-http-client-reqwest is written against
reqwest 0.13.2+, a semver-incompatible major vs. the reqwest 0.12 the
rest of WA (llm/sync) depends on -- Cargo can't unify those, so
`reqwest::Client` from our own dependency isn't the same type rmcp's
`impl StreamableHttpClient for reqwest::Client` is written for. Adds
`reqwest013` (a Cargo package-rename of reqwest 0.13) purely so
tests/mcp_server_test.rs can construct the exact client type rmcp
expects, without touching reqwest 0.12 anywhere else in the crate.
A real rmcp client speaks Streamable HTTP to the real RmcpServer stack
(bind_loopback + serve + WaMcpHandler) over an OS-assigned loopback
port, using an in-memory SqliteStore -- not just isolated unit pieces.
Asserts: an unauthenticated POST is rejected (401) before it ever
reaches the MCP service; an authenticated client's list_tools/call_tool
round-trip is schema-valid; and the call appends an mcp_access_log row
even though default scope (`none`) denies it data (FR-MCP-5 logs the
ask, not just the answer). mint_and_store/delete touch the real OS
credential store, same pattern as sync::tests::webdav_round_trip.
Widens visibility from pub(crate) to pub so tests/mcp_server_test.rs
(an external integration-test crate, per CLAUDE.md's "cross-service
tests in /tests") can drive the real HTTP transport end-to-end. This
does not weaken the loopback guarantee: bind_loopback still refuses
non-loopback regardless of caller -- only its visibility changed.
Regenerated by `cargo check --features mcp` after promoting hyper/
hyper-util/http/http-body-util/bytes/tower-service/tokio-util to direct
deps and widening rmcp's feature set (see the earlier Cargo.toml
commits).
Fixes a dead_code warning (clippy -D warnings would fail on it) --
server.rs was calling listener.local_addr() itself before handing the
listener to http_transport::serve(), leaving the handle's own
local_addr field unread.
Replaces the four not_implemented() stubs. mcp_status/set_mcp_enabled
reach the process-wide RmcpServer singleton (behind `#[cfg(feature =
"mcp")]`, with a not_implemented fallback for builds without it);
set_mcp_scope/mcp_access_log are plain settings/Store I/O and work in
every build regardless of the `mcp` cargo feature. The token is only
ever returned once, right when set_mcp_enabled mints it -- it is never
re-readable afterwards, same as any other freshly-issued secret.
Builds its own tokio runtime (there is no Tauri app in this mode) and
tracing goes to stderr, not stdout -- stdout is the MCP JSON-RPC channel.
Connects to the same wa.db as the GUI instance via SqliteStore::connect,
builds a WaMcpHandler with no AppHandle (None), and runs it to
completion via mcp::stdio_transport::serve_once. A build without the
`mcp` feature prints an error and exits(1) instead of silently opening
the GUI.
http_transport.rs needs tokio_util::sync::CancellationToken directly;
rmcp pulls tokio-util transitively but that doesn't make it `use`-able
from our own crate without a direct Cargo.toml entry.
Checked before whispassist_lib::run() builds the Tauri app -- the flag
routes straight to run_mcp_stdio() (next commit) and returns instead of
opening a window.
The --mcp-stdio child process (main.rs, next commits) has no running
Tauri app/window to emit "mcp://access" events to -- only the GUI
instance's HTTP transport does. The mcp_access_log DB row is still
written unconditionally either way (FR-MCP-5); only the live event is
skipped when there's no AppHandle.
start() mints/stores a fresh token, then either binds the loopback HTTP
listener (bind_loopback + http_transport::serve) or, for stdio, just
records "enabled" and hands back the whishassist.exe --mcp-stdio command
line the agent's client config should spawn -- there is nothing to run
in-process for stdio (see mcp::stdio_transport). stop() tears down the
HTTP listener if any and deletes the stored token. instance() is a
process-wide singleton (OnceLock) so separate Tauri command invocations
(set_mcp_enabled/mcp_status/set_mcp_scope) share the same running state.
serve_once() runs one MCP session over the current process's stdin/
stdout to completion -- the entry point main.rs's --mcp-stdio flag calls
into (next commit). No bearer-token check here: spawning this process
at all requires the same local-user privilege as any other command, so
process-spawn capability is the trust boundary for stdio, not a header.
Mirrors sync::credentials: mint_and_store() generates a fresh 32-byte
random token per set_mcp_enabled call and writes it via `keyring`
(Windows Credential Manager); the token is never persisted to
settings.json/wa.db/logs. verify() is a constant-time compare so token
checking doesn't leak timing information about a partial match
(NFR-SEC-5).
bind_loopback() refuses to bind anything that doesn't resolve to a
loopback address -- unit tested directly (non-loopback IP, unparseable
host, and a real 127.0.0.1:0 bind). serve() runs a hyper HTTP/1 accept
loop in front of rmcp's StreamableHttpService (a bare tower_service, not
an axum app); every request needs `Authorization: Bearer <token>`
(constant-time compared via mcp::token::verify) before it ever reaches
the MCP service -- an unauthenticated request never reaches rmcp at all.
rmcp gains transport-streamable-http-server + transport-io (the actual
serving code -- the base "server" feature only got tool routing) plus
client + transport-streamable-http-client-reqwest, used solely by this
crate's own in-process MCP-client tests (WA never opens an outbound MCP
connection at runtime, so this is not new egress, FR-MCP-7). hyper/
hyper-util/http-body-util/http/bytes/tower-service are the low-level glue
to run HTTP/1 over a loopback TcpListener in front of rmcp's
StreamableHttpService (a bare tower_service::Service, not an axum app).
All are already in Cargo.lock transitively via reqwest/tauri -- no new
crates, just promoted to direct deps under the existing `mcp` feature.
WaMcpHandler wires list_recent_meetings/get_transcript/get_action_items/
get_feature_brief to Store, re-checking ExposeScope + the recordings
gate independently in every handler (FR-MCP-3) and logging every read
via record_mcp_access + an "mcp://access" event (FR-MCP-5) before scope
is even evaluated, so the audit trail is "what was asked for", not just
"what was returned". get_feature_brief calls through the existing
commands::get_feature_brief stub per the M1/M2 integration contract --
it always errors right now since M1's brief storage isn't implemented
in this worktree yet; the `selected`-scope exposed-flag check is left
as a marked TODO for when M1 lands.
Pure functions (no DB, no cargo feature gate) so scope control is
testable without a running server: meetings_visible/brief_visible/
recording_gate_ok/meeting_allowed. Documents the conservative choice for
`selected` scope on meetings/transcript/action-items -- there's no
per-meeting selection flag in the schema yet (only feature_briefs.exposed
does), so `selected` behaves like `none` there until a real "pick which
meetings" mechanism exists, rather than silently behaving like `all`.