DropboxTarget: path-addressed like OneDrive/WebDAV; create_folder_v2 creates
the whole intermediate path in one call; upload-session chunking above
LARGE_FILE_THRESHOLD (start/append_v2/finish).
BoxTarget: Box addresses items by numeric ID, not path, so ensure_dir/exists/
put all walk (and lazily create) the folder chain from root ("0") by listing
each level's children. Always uploads via Box's session API regardless of
file size rather than its simple multipart endpoint, since the session API
takes plain PUT bodies (consistent with every other target here) and needs
no new reqwest feature; Box computes and returns each part's digest, so no
local hashing is needed either.
Both follow the same documented ceiling as OneDriveTarget's put_chunked
(M4.1): the upload-session id lives only for one put() call, not persisted
across a durable-queue retry after a restart.
Tests: in-process std::net mock servers for both providers (same pattern as
the existing MockNextcloud/MockGraph), each exercising a real multi-chunk
upload end-to-end.
resolve_language(Some("EN"), false) was passed through unchanged instead of
being normalized to lowercase "en" - the English-only-model guard only fired
when the requested language *differed* from English, not when it matched with
different casing. Also fixes cargo fmt drift in mod.rs/npu.rs left over from
the M4.2 worktree (stopped mid-verification per session instruction before
running fmt).
Recordings are now native-quality (~50-100 MB) and put() used to buffer
the whole file into memory with a single PUT/upload call; OneDrive/Graph
also caps a single PUT at 250 MB. This replaces that with disk-streamed,
chunked uploads on both sync backends (FR-SYNC-2/5):
- Both WebDavTarget::put and OneDriveTarget::put now stream the file off
disk in fixed-size buffers (tokio::fs::File + BufReader) instead of
tokio::fs::read()'ing it whole — memory use is O(chunk), not O(file
size), for every upload, large or small.
- Files at or below LARGE_FILE_THRESHOLD (8 MiB) still take a single
streamed PUT; only large artifacts (in practice, .wav recordings) take
the chunked path, so small transcript/notes/summary uploads are
unaffected.
- WebDAV (Nextcloud/ownCloud): implements the chunking-v2 protocol —
MKCOL an upload collection keyed deterministically off the remote
path's sha256 (stable across retries), PUT lexically-sortable parts,
MOVE the virtual `.file` to assemble server-side. A retry PROPFINDs
the collection first and skips any part already landed, so a
partially-uploaded large file resumes instead of restarting at byte 0
— genuine mid-file resume, not just chunk-level retry. Generic WebDAV
targets (Seafile, Synology, unbranded servers) have no equivalent
server-side assembly endpoint, so they fall back to one streamed PUT
above the threshold too — memory-safe, just not chunk-resumable
(documented ponytail simplification).
- OneDrive/Graph: implements the createUploadSession + Content-Range
byte-range PUT flow (chunks capped at 10 MiB, a multiple of Graph's
required 320 KiB granularity). A failed chunk is retried a few times
within the same put() call. ponytail-documented ceiling: the session
URL isn't persisted on the sync_jobs row, so a retry from a *later*
pump cycle (e.g. after an app restart) starts a fresh session and
re-uploads from byte 0 rather than resuming — true cross-attempt
resume would need a sync_jobs column, left as a follow-up.
- Progress is reported via the existing ProgressSink after each chunk,
not just once at the end, so the UI sees steady movement on a large
upload (FR-SYNC-11).
- OneDriveTarget's Graph URLs are now built from a `graph_base()` helper
(overridable via WA_GRAPH_BASE_URL, unset in production) so the
chunked session flow can be exercised against a local mock in tests.
Tests (all against small, in-process, dependency-free mock servers —
std::net only, no new crates, same pattern as oauth::LoopbackRedirect):
- nextcloud_chunked_upload_assembles_and_resumes: multi-chunk upload
assembles byte-for-byte, then a resumed upload with one chunk
pre-seeded skips exactly that chunk.
- small_file_skips_chunking_even_on_a_chunking_capable_target: a small
file never takes the chunked path.
- onedrive_chunked_upload_via_graph_session_mock: multi-chunk Graph
upload session assembles byte-for-byte with per-chunk progress.
- parse_chunk_sizes_extracts_href_and_length_ignoring_namespace_prefix
and chunk_name_is_zero_padded_and_lexically_sortable: pure unit tests
for the PROPFIND-response scraper and chunk-naming scheme.
Skip-if-unchanged (SHA-256, in storage::upsert_sync_job) and the
exponential-backoff retry queue (storage::claim_due_sync_jobs) are
untouched — this only changes how SyncTarget::put moves bytes.
Verified: cargo fmt clean; cargo clippy --features sync and --features
mcp both clean (-D warnings); cargo test --lib (default features:
audio, cpu-transcription, diarization, pst, sync, npu) 165 passed, 0
failed, 7 ignored (unrelated hardware-gated tests); cargo test --features
sync --lib: 18 passed, 0 failed, 1 ignored (webdav_round_trip, opt-in
live-server test per existing convention).
Needed for AsyncReadExt/AsyncSeekExt/BufReader used to stream .wav
recordings off disk in fixed-size chunks instead of buffering the
whole file in memory (T9.2 refinement, FR-SYNC-5).
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).