Skip to content

M10: add optional cross-platform Hot Folder processing service - #1063

Open
lukashold wants to merge 99 commits into
storytold:mainfrom
lukashold:codex/hot-folder
Open

lukashold wants to merge 99 commits into
storytold:mainfrom
lukashold:codex/hot-folder

Conversation

@lukashold

@lukashold lukashold commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Latest integration follow-up (b45ce0c)

Integrates main 9e5b70d1c231d7db558e278a2622b519ca063494, retaining its timestamp-stable batch fixture and regression. Resolves the two Windows print-dialog CI failures from head 8218c730: even a synthetic printer source answers asynchronously, changing the dialog layout and the Print/Save action while fixed-frame tests are clicking.

Normal print-dialog tests now wait with a bounded deadline for discovery and let the layout settle. Their source deliberately answers late. Existing tests that intentionally observe loading or a late answer retain their explicit gates. PDF content/page selection, cut-stack layout, driver options and failure assertions are preserved. Removing the synchronization reproduces the missing Save as PDF control; restoring it passes all 13 print tests. All 9 batch tests, strict scoped Clippy, formatting, assets, layers and 32 WASM crates also passed. The concurrent equivalent fix from 5c926984 is retained in history; the final merged tree is identical to the locally validated tree.

Prior-head evidence: 8218c730 passed Linux and FreeBSD tests, all four service lifecycle platforms, Windows ARM64 build and installation, WASM, Nix, packaging lint and dependency checks. Its Windows suite failed the two print tests addressed above. These results are not claims of current-head CI success. Maintainer review and release-package qualification remain outstanding.


This adds a standalone PdfCraft Hot Folder task manager and an optional boot service. Enabled tasks can process scans while the editor is closed, with searchable output, scheduled execution and durable recovery after interruption.

The branch retains the scan/orientation/cropping work from #1012 and integrates main through afa8e184. Historical OCR remains separate in #1059. The shared optimization corrections and lossless API are reviewed separately in #1282 and are preserved here alongside the current optimizer presets.

Processing and recovery

  • Named tasks; run now, pause and retry; once/interval/daily/weekly/monthly schedules; stable-file detection, subfolders, duplication/import/export, logs and failure filtering.
  • Local and OS-mounted network folders, explicit FTPS or deliberately selected plain FTP, Outlook attachments through Microsoft Graph device authorization, and ordered open PdfCraft project manifests.
  • OCR with primary-system-language selection and English fallback; rotation, deskew, paper-edge cropping, page ranges, recognition regions, preprocessing, spread splitting and conservative white-page removal with optional archives.
  • Up to 16 saving steps: PDF, image-only PDF, DOCX, RTF, HTML, text, PNG, JPEG, TIFF, XLSX, EPUB and JSON analysis. Group by file, folder or settled run; naming templates and collision-safe output.
  • Supervised task processes, cancellation and time/memory limits isolate unavailable shares and crashed workers. Transient errors retry with bounded backoff; malformed unchanged inputs remain blocked by fingerprint instead of being silently omitted from groups.
  • Bounded discovery handles more than 10,000 retained inputs. Durable write-ahead receipts commit grouped outputs together. Every required output is checked against the published bytes/hashes before an original is moved/deleted, including after restart. Cancellation, missing/replaced outputs or incomplete publication never authorize source cleanup.
  • Completed receipt recovery no longer requires installed OCR models. New work still reports a missing-model error and remains retryable. Windows confinement accepts only the authorized original/canonical root; conservative alias matching can reject overlaps but cannot grant access or probe another task's offline share.
  • The task manager has English, German, French, Dutch, Spanish, Norwegian Bokmål and Swedish catalogs, with primary-system-language selection and English fallback.

Optional PDF optimization

Each PDF saving step exposes the same Off / Lossless / Reduce / Custom settings in the manager, engine and CLI/MCP task schema. Off remains the default.

Lossless excludes image re-encoding/downsampling and retains text, tags, forms, links and image streams. Reduce uses the shared 150 ppi/JPEG quality 60 defaults. Custom exposes separate color/grayscale resolution thresholds and Retain/ZIP/JPEG quality controls; downsampling and JPEG re-encoding can lose image detail. Lossless applies to the PDF entering this final step and cannot reverse separately selected rasterization or image-only conversion.

Only a strictly smaller candidate replaces the processed PDF; equal/larger candidates retain the original processed bytes. The UI and headless status report the actual published before/after sizes. Inputs are never overwritten. Signed inputs are refused before page selection, rasterization or grouping. Explicit Keep/Move/Delete policies still apply only after all outputs and their durable proofs succeed.

Writer integration: the generation-aware full-save correction from #1143 is backported from da54b14f085c930350d9b23839bd404cf4c3a625 (COS implementation and five regressions). The added Hot Folder regression exercises Off/Lossless/Reduce/Custom publication while preserving live extension data and preventing stale page references from acquiring replacement content. Final installer acceptance must still be repeated on the released package.

Installation and service

The companion is optional in Windows MSI and macOS PKG, and a separate Linux DEB/RPM. It registers a real boot service: Windows SCM under LocalService, Linux systemd under a dedicated account, or a macOS LaunchDaemon under a configured non-root account. Closing the manager leaves the service running. Nothing is processed until a task is enabled; the drag-and-drop DMG installs only the editor.

Credentials use protected service-account storage and are excluded from task exports. Administration APIs reject unrelated executable/configuration paths. A stable Windows service SID and repeated SCM recovery preserve access across account changes. MSI upgrades retain task data and an administrator-selected service account; they do not silently reset its logon. No MCP server or network listener starts implicitly.

Latest CI follow-up (fe18b08c)

The previous head passed the renderer cancellation regression on FreeBSD but failed catalog font coverage on all three native platforms and an incompatible early-upgrade assertion in Windows MSI validation.

  • Rephrase the Bulgarian caption without U+045D, preserving its meaning and bundled-font coverage. All 11 font integration tests pass locally.
  • Keep late MSI upgrades to preserve the existing SCM account. Explicitly remove only PdfCraft's legacy image-menu keys before registry writes; selected menu components then recreate them. Validate cleanup rows for every supported image extension and their execution order in the compiled MSI.
  • The updated compiled-MSI check fails on the old MSI (missing cleanup table) and passes on the corrected synthetic MSI. The Windows lifecycle test now checks upgrade menu opt-out and obsolete subkey removal alongside service-account, task-data and executable preservation.
  • Scoped strict Clippy, formatting, assets, layers and WASM passed. No service was installed locally. Current-head Windows lifecycle/ARM64 and full native CI remain pending; local MSI inspection is not release-package acceptance.

Validation of this integration

Previous integration head: 06aca3df7dcf481497320b62d0fcb1d7180056fd, integrating main afa8e1843cfed613f3d403119f6c2649bce824c9 (0.8.0).

  • Fix the FreeBSD cancellation test's scheduler-sensitive two-second spin with a blocking startup handshake. Cancellation must still complete while parser initialization is held; both threads are released/joined before result assertions. The prior failure was FreeBSD run 38689653547. Current-head FreeBSD CI remains to be checked.

  • Preserve lossless image streams when integrating the new fragment-merging optimizer and Standard/Mobile presets. Lossless mode disables fragment merging and rejects conflicting custom settings. A 120-strip fixture fails with merging enabled and passes with the correction.

  • Refuse Hot Folder task/source/project calls under unsupported separate automation read/write roots before I/O; the supported shared --root behavior remains. Service mutations respect either confinement mode. The regression uses valid read/write paths and creates a configuration when the guard is removed.

  • Keep fixed replay timestamps and injected wall clocks independent. The regression fails with the stale clock settings retained.

  • Accept an OCR checkbox caption such as □Haupt only when its explicit box mark overlaps the matching box and the caption extends right on the same line. Synthetic positive/negative geometry cases and real-model scan regressions cover this. The skewed 150 dpi fixture uses a legible 10 pt telephone label; field-name and geometry assertions remain intact, with no general OCR-accuracy claim.

  • Reconcile save tests with repaired page trees, signature tests with the current password minimum, history tests with logical page labels, and printer-option tests with the layout paper sent separately from driver defaults. Keep optional service packaging and upstream installer upgrade choices. Reconcile duplicate parity/catalog entries and regenerate attribution. New Vietnamese/Lithuanian catalog additions await native review.

  • Selected native suites: 1,435 passing cases and two ignored across automation, Hot Folder, engine, forms, optimizer, renderer and UI. The combined run used the old 8 pt form fixture and had that one failure; the corrected 10 pt fixture then passed its focused rerun. Other automation integration suites also passed earlier in this refresh.

  • Scoped cargo clippy --all-targets -- -D warnings, cargo fmt --all -- --check, and cargo xtask wasm passed (32 WASM crates).

  • Assets (272 repository, 6 bundled, 25 fetched), layer boundaries (39 crates), and parity checks passed.

  • Safe PowerShell server/stop-wait harnesses passed; merged MSI XML parsed with unique IDs. No local service installation was performed.

  • FreeBSD execution and current-head cross-platform CI remain for GitHub Actions. Optional craft-fonts and privileged Windows symlink cases were not qualified locally.

Earlier evidence is not current-head validation: integration 116cd60c passed 3,173 workspace tests and a real four-profile CLI preservation run. Head 2ec3883b passed Windows/Linux CI, service lifecycle, packaging and ARM64 checks; its FreeBSD cancellation-test failure is addressed here. Fork Actions is disabled, so upstream PR workflows provide current CI results. No Actions settings were changed. Maintainer review and final package qualification remain outstanding.

Platform acceptance and remaining limits

The earlier artifact-backed server acceptance run 38167184192 passed all 11 scenarios and clean uninstall on Windows Server 2022 (20348) and 2025 (26100). It installed the actual x64 MSI from build 38164423890, source 0bfebc1de485ea8000184911faeb860335d91230, SHA-256 cf4e9da483ace9c7bacb208e0f2733a9896e27bf5fe4011743548c35ae11a2b4. That is earlier packaged-worker evidence, not a claim that a new MSI for the current optimization tree has already been accepted. It covers LocalService OCR, malformed-input isolation, automatic output/SMB retry, active-worker hard kill, bounded stop, SCM recovery and upgrade preservation of tasks/account/authenticated SMB access.

SMB acceptance used authenticated encrypted loopback shares. Server 2019, specific NAS devices, domain/gMSA deployments and real AD/GPO rollouts remain unqualified. Separate synthetic MSI/DEB/PKG lifecycle checks passed on Windows Server 2022/2025, Ubuntu 24.04 and macOS 15; those do not qualify every final OCR executable or architecture. Signed/notarized release acceptance and RPM lifecycle tests remain separate.

This is a clean-room implementation without ABBYY code/assets or proprietary task/project compatibility. Blank-page detection is white-only; OCR and layout quality remain bounded by PdfCraft's engine, with no ABBYY parity or general accuracy claim. Outlook requires public-client registration and consent; remote protocol checks use fixtures. FTP/Outlook/project attachments support PDF/PNG/JPEG/TIFF; local folders also support BMP.

The integrated baseline repairs retain their provenance: upstream now supplies Preferences scrolling and selection handling; the Bulk Properties harness adjustment from #1015 remains. Release and packaging lint use main's shared craft-common-actions workflows unchanged; the repository's packaging/*/package.* scripts build and package the Hot Folder app, and packaging/lint-hot-folder.sh adds its installer checks through the shared lint's extra-checks-script input. Under pdfcraft-cli mcp, Hot Folder service changes and credential changes need --allow-hot-folder-admin.

See docs/hot-folder*.md for limits and setup. No private documents or mailbox data are included.

AI assistance: implemented with OpenAI Codex for @lukashold.

Summary by CodeRabbit

  • New Features
    • Added a standalone Hot Folder app for scheduled PDF and image processing, with local, FTP/FTPS, Outlook, and project sources; configurable preprocessing, OCR, output formats, and optimization.
    • Added optional Hot Folder background-service installation for Windows, macOS, and Linux.
    • Added scan correction with automatic rotation, deskewing, and paper-edge cropping, with or without text recognition.
    • Expanded offline OCR with multilingual recognition and system-locale language selection.
    • Added lossless PDF optimization, EPUB and XLSX exports, and read-only PDF content analysis.
    • Added automation controls for Hot Folder tasks, sources, and service management.
  • Bug Fixes
    • Improved handling of PDF object generations, scanned-checkbox labels, and image optimization safeguards.
    • Made optimization succeed cleanly when selected hidden-information categories are absent.

lukashold and others added 8 commits October 11, 2026 06:59
Recognize English, German, French, Spanish and horizontal Japanese locally. Correct quarter-turn orientation and skew before OCR, preserve scan streams, and write searchable Unicode text with undo and stale-result protection.

Co-authored-by: Codex <noreply@openai.com>
Add conservative paper-edge detection and adjustable safety padding to scan enhancement, shared with OCR and alignment. Preserve existing text and original scan streams with undo. Ignore scanner frames during deskew analysis, and cover rotated/off-origin crops and combined OCR geometry with synthetic tests.

Integrate main through bbbe600 and refresh translation asset hashes.

Co-authored-by: Codex <noreply@openai.com>
Expose Italiano in the shared language list and derive both automation schemas from it. Cover all six Italian accented vowels through real-model OCR, save/reopen and unchanged scan pixels, plus UI selection and single/batch automation.

Requested by @magdale76 in storytold#1012. Validated with 2,147 passing tests including the corrected UI selector and the existing Windows update-test filename workaround; the existing symlink test requires unavailable privileges and remains skipped.

Co-authored-by: Codex <noreply@openai.com>
Fold Dutch, Danish, Norwegian Bokmål, Swedish, Finnish and Portuguese into the existing OCR language list following @mlkonrad's comparison in storytold#1012. Keep the shared model unchanged; leave Icelandic unclaimed. Add real-model save/reopen checks for accented words in all six languages.

Default the OCR picker to the primary OS/browser locale, independently of available UI translations, with English fallback. Preserve manual document-language choices and deterministic automation defaults. Cover locale variants, unsupported locales and UI selection.

Validation: 2,149 tests passed; seven pre-existing ignored and one Windows symlink-privilege test skipped. Four update tests passed under a neutral executable filename due to Windows installer detection. Format, strict workspace Clippy, 30 WASM crates, layers, assets and parity passed. German OCR dialog rendered and inspected.

Co-authored-by: Codex <noreply@openai.com>
Add scheduled OCR tasks, durable source/output recovery, seven task-manager languages and optional boot services with platform installers.

Co-authored-by: Codex <noreply@openai.com>
Satisfy the existing cargo-deny wildcard policy without changing dependency resolution. Full dependency audit and workspace tests pass.

Co-authored-by: Codex <noreply@openai.com>
lukashold and others added 4 commits October 11, 2026 17:39
Preserve zero/one/many CLI scripts and object reports with -InputObject, with a regression and optional real read-only CLI smoke.
Keep the test settings compatible with standalone Hot Folder, fail on modified or removed originals, and include final MSI checksums in release artifacts.
Cover the actual Server acceptance registration with a no-space executable and quoted configuration path, while rejecting extra arguments and mismatched paths.

Co-authored-by: Codex <noreply@openai.com>
…ilures

Return a single validated ProductCode and release every dependent COM object before msiexec. Cover actual synthetic MSI databases, exclusive reopening, malformed identifiers and cleanup error precedence without installing on the development host.

Co-authored-by: Codex <noreply@openai.com>
CI-only snapshot of review tree f2c1096.
Local focus, quality, WASM32 and actual CLI checks passed. The strict local
workspace run remains unqualified because Windows lacked permission to
create its required symlink fixture: 2568 passed, 1 failed, 7 ignored.

Run unchanged strict CI before any update to PR storytold#1063. This separate test
branch is not a release or a qualified Hot Folder update.

Prepared with Codex for @lukashold.
Test snapshot only; do not update the Hot Folder feature branch or PR storytold#1063. Strict hosted Windows symlink qualification remains pending on this tree.

Completed receipts no longer depend on installed OCR models. Path access remains strictly confined; conservative Windows alias matching can only reject overlaps. Preserve the original recovery and source-cleanup assertions.

Verified local evidence: 21 focused tests, workspace quality gates, unfiltered FullLocal 2575 passed / 0 failed / 7 existing ignored, 32 WASM packages, and actual CLI lossless/source/render checks. The Windows developer-mode local run does not qualify real-symlink coverage; CI must keep PDFCRAFT_REQUIRE_SYMLINK_TESTS=1.

Review tree: 1bb0b25
Local evidence SHA256: 8a4b616abef1c219c83fcffdb5672cd0deecc0110b47d5fd4e1be62d236e9af2
Previous failed CI evidence retained: https://github.com/lukashold/pdfcraft/actions/runs/38219224255
lukashold and others added 10 commits October 13, 2026 21:14
Backport COS reference-generation handling and five regressions from PR storytold#1143 at da54b14. Add a Hot Folder automation regression for Off/Lossless/Reduce/Custom output and source preservation.

Validation: cargo fmt --all -- --check and git diff --check pass. Runtime tests and workspace gates remain pending because this Windows environment lacks MSVC link.exe. This commit does not qualify release or installer acceptance.

AI assistance: OpenAI Codex for @lukashold.
Merge main b5467d9, retain optional service packaging, align OCR baselines and duplicate suppression, and validate the integrated workflow on Windows.

AI assistance: OpenAI Codex for @lukashold.
Resolve the dependency lockfile against main and record passing targeted Windows tests and project gates.

AI assistance: OpenAI Codex for @lukashold.
Use native path separators and case rules in server-test confinement; cover roots, trailing separators, siblings and traversal. Isolate print-dialog fixtures from installed native drivers. Integrate main dc4d86b and refresh merged catalog attribution.

Validation: Linux before/after regression, Windows safe harness, 1385 selected tests plus 271 renderer tests, scoped strict Clippy, format, assets, layers, parity and 32 WASM crates.

AI assistance: OpenAI Codex for @lukashold.
Gate discovery to reproduce the Windows CI race and mark the synthetic queue as explicitly selected, matching the real combo. Keep driver-option assertions unchanged.

Validation: gated regression fails before the fix; all 11 print tests, scoped strict Clippy, fmt, assets, layers, parity and 32 WASM checks pass.

AI assistance: OpenAI Codex for @lukashold.
The worker marker precedes the host monitor join and SCM Stopped report. Poll both within ten seconds; retain probe-state checks and propagate query failures. Add synthetic ordering and timeout regressions without starting a local service.

Validation: old stop observation reproduced failure; synthetic regression, service crate tests, strict Clippy, fmt, assets, layers, parity and 32 WASM checks pass.

AI assistance: OpenAI Codex for @lukashold.
Merge main d3a91c5, preserving D-Bus development headers alongside qpdf and Poppler. Align Hot Folder service requirements with 0.8.0 and retain upstream save validation and stale-watcher protection.

Validation: 164 automation/Hot Folder tests, 374 editor tests (one ignored), 83 support tests, both safe PowerShell harnesses, scoped strict Clippy, fmt, assets, layers, parity and 32 WASM crates pass.

AI assistance: OpenAI Codex for @lukashold.
Integrate upstream main, retain lossless optimization and root confinement, and reconcile OCR form detection and integration fixtures.

AI assistance: OpenAI Codex for @lukashold.
Preserve late service upgrades while clearing legacy image-menu keys before writing selected menus. Validate compiled MSI cleanup and extend service lifecycle coverage for menu opt-out.

AI assistance: OpenAI Codex for @lukashold.
Resolve conflicts with main:
- i18n (kk, lt, sl, tr, vi): keep main's snapshot-notice and coverage rows,
  add the branch's scan-correction rows only where main has no entry.
- ATTRIBUTION.toml: main's layout; lt/vi sources note the Hot Folder
  additions; catalog hashes recomputed, ATTRIBUTION.md regenerated.
- canvas.rs, tests/print.rs, tests/automation.rs: main's versions.
- parity, docs/roadmap.md, docs/gaps.md, ROADMAP.md: keep both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lukashold and others added 20 commits October 23, 2026 20:06
Main moved CI onto the shared craft-common-actions gate (storytold#4269, storytold#4270).
ci.yml takes main's git-sources and gate jobs; the gate installs
libdbus-1-dev as well, since the Hot Folder's Secret Service credential
store pulls in libdbus-sys. The root-only credential provisioning check,
which the shared gate cannot run, moves into its own Linux job unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main dropped its git-sources job from release.yml because the shared
craft-common-actions release workflow now runs that check (storytold#4292). This
branch keeps its own full release workflow (disclosed for maintainer
decision in the PR) and does not call the shared one, so it keeps its own
git-sources job; release.yml stays as on the branch. The hostile-arguments
sweep from storytold#4277 merges cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main stopped hashing translation catalogs in ATTRIBUTION.toml (storytold#4284).
ATTRIBUTION.toml takes main's version and keeps the branch's additions:
the native-review notes on nine catalogs (lt, vi, sq, ca, fi, hi, ko, id,
ro) and the three fetched PP-OCR model entries. ATTRIBUTION.md is
regenerated with cargo xtask assets --write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's UI tests now wait for background work by time through the shared
support::step_until (storytold#4242). The control open test took the same approach
on this branch with its own loop; it now uses main's helper, so the branch
no longer differs from main in that test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
replacing_an_in_flight_request_does_not_delay_the_replacement read the busy
slots after set_queue to find A cancelled. Once cancelled, A's worker stops
waiting, renders and frees its slot, so on the slow FreeBSD CI VM the slot
was already gone and the test reported "request A was not cancelled". The
test now takes A's cancel flag before replacing the queue. Checked by adding
a 200 ms pause after set_queue: the old assertion fails, the new one holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's integration batch A brought the optimizer's target PDF version,
object-stream and LZW-to-Flate options (storytold#3227, storytold#4181) next to this
branch's lossless profile:
- Settings::lossless() retains the version, keeps object streams and turns
  lzw_to_flate off, since re-encoding LZW streams would change encoded
  image data; doc_optimize refuses lzw_to_flate with profile lossless.
- doc_optimize's description keeps main's new options and the branch's
  profile, remove_unused_xobjects and absent-discard wording.
- optimize README and tests keep both sides; the parity entry takes the
  union of evidence and both notes.
- docs/roadmap.md takes main's M10-M13 rows with the Hot Folder notes;
  M10 drops "OCR languages beyond Latin" from Missing, which the branch
  covers.
- Eight catalogs keep the Hot Folder rows and main's Today button row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's integration batches B and C met the branch's OCR and optimizer work:
- OcrSettings keeps the branch's scan corrections and gains main's
  native_text_only; the run's render config hides comments for it.
- Edit's automatic OCR on a scanned page (storytold#1609) only reads text: it turns
  rotation, deskew and cropping off, which the dialog may have left on and
  which would otherwise change the page in view unasked.
- A cancelled run still applies nothing, before main's check that drops an
  automatic result for a changed document.
- OcrPage::with_words builds a result for given document bytes, so main's
  new UI tests keep the branch's stale-bytes guard in apply_ocr.
- optimize pixels() takes main's colour-transform-aware JPEG decoding
  (storytold#3012), which checks the JPEG's own size, with the branch's dimension
  validation and decode budget; README notes both.
- Roadmap rows, ROADMAP log, and catalog review notes keep both sides;
  ATTRIBUTION.md is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges cleanly: vendored hayro-postscript (storytold#4363) and the multiline field
glyph-width fix (storytold#4447).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test stages PdfCraft.app and the Hot Folder app in RUNNER_TEMP to build
the package. pkgbuild components are relocatable, so Installer may update a
bundle with the same identifier where it already is: on af25b50, with no
packaging change since the last green run, the first install left
/Applications without the expected app and the job failed right after
"The install was successful". Remove the staged bundles once the product
is built. The release packaging is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's r4 integration batch merges cleanly except ATTRIBUTION: nine
catalog sources take main's newer notes and keep the branch's
native-review note; ATTRIBUTION.md is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved to 0.14.0 and keeps a styled run inside its sentence's
paragraph in the Office exporters (storytold#4158, storytold#4411):
- The pdfcraft-hot-folder-service requirements in automation and the Hot
  Folder app follow the workspace version; only the two Hot Folder crates
  change in Cargo.lock.
- The branch's XLSX and EPUB exporters take a paragraph's runs: one
  inline-string cell with a formatted <r> per run, and one XHTML paragraph
  with <em>/<strong> per run. A regression test ("Before *Chevron*
  after.") fails when only the first run is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved PdfCraft to egui 0.37 (storytold#4591):
- Cargo.lock takes main's and adds only the Hot Folder crates and their
  existing dependencies; no new versions.
- ci.yml and the branch's own release.yml take the new craft-fonts ref;
  release.yml otherwise stays the branch's (disclosed in the PR).
- The MSI keeps the Hot Folder components and the util namespace next to
  main's craft-font licence group; NOTICE keeps the PaddleOCR entry and
  main's egui icon-font notice; roadmap rows keep the Hot Folder notes.
- egui 0.37 deprecates Id::new: the branch's dialog-card test lookups and
  the Hot Folder close prompt use Id::unique, as main does.
- kittest now fails a frame with unnamed widgets. The Hot Folder manager
  names every field and picker from its caption (labelled_by), and names
  pickers without one (language, source, save grouping, schedule, weekday)
  through widget_info with existing translated labels.

Seven Chinese/Japanese UI tests need craft-fonts' CJK faces under egui
0.37; they pass locally with CRAFT_FONTS_DIR at the pinned b076417.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's startup update prompt and self-updating (storytold#4654) merge cleanly
apart from Cargo.lock, which takes main's and adds only the Hot Folder
crates and their existing dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges cleanly: redaction of vector paths behind clipping (storytold#4577).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main pins craft-update to craft-libs 495e597 (storytold#4721, storytold#4728). Cargo.lock
takes main's and adds only the Hot Folder crates and their existing
dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's 11 safe dependency updates (storytold#4737) merge cleanly apart from
Cargo.lock, which takes main's and adds only the Hot Folder crates and
their existing dependencies; no new versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's craft-menu menus (storytold#4752) and CJK test skips without craft-fonts
(storytold#4706) merge cleanly apart from Cargo.lock, which takes main's and adds
only the Hot Folder crates and their existing dependencies. The branch's
ocr.enhance command stays in the command registry the menus are built from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…task file is refused

Two remaining review points:
- Windows: the MSI's task and secrets directories no longer grant the
  shared LocalService SID (S-1-5-19), which any LocalService process holds
  (CWE-732). The service reaches them through its own SID,
  NT SERVICE\PdfCraftHotFolder, which prepare-account enables before the
  first start and the lifecycle test already checks. test-msi.ps1 asserts
  the narrower DACLs; the credential check still accepts older installs
  and now has the installer's exact DACL as a passing case.
- Linux: the desktop launcher runs as the desktop user, who cannot enter
  the service's 0700 state directory. Path::exists then said "missing"
  and the manager showed an empty task list that a save would replace.
  The manager now uses try_exists: a file it cannot look up is a load
  error that blocks saving and says the file belongs to the service and
  needs administrative permission (translated in all seven languages).
  The service directory's ownership and mode are unchanged. The
  regression test fails with the old fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main split the engine's lib.rs into modules (storytold#4730). lib.rs takes main's
module list with the branch's Hot Folder and ocr_pdf modules; the branch's
lib.rs changes move to where main put their surroundings:
- edit.rs: Edit::DeskewPage, its label and permission class, and the runner
  arms for DeskewPage and AddOcrText through ocr_pdf.
- session.rs: the fixed transaction timestamp (field, with_timestamp, the
  clock setters clearing it, now() preferring it). local_offset and
  utc_offset become pub(crate), as main does for the session state its
  tests read, so the clock-switching test keeps compiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main split several large files into modules and moved ui-egui files into
directories (storytold#4889 and the split series). Each conflict takes main's layout
and carries the branch's changes to where main put their code:
- cos document/: generation-aware reads (try_get_reference, ObjRef-keyed
  /Length cache, full-save comparisons at the live generation, Root lookup
  by whole reference) in objects.rs, reader.rs and state.rs.
- engine edit/ and session/: DeskewPage (enum, label, permission class,
  runner), AddOcrText through ocr_pdf, and the fixed transaction timestamp;
  local_offset is pub(crate) for the clock-switching test.
- automation a11y/ocr.rs, dispatch.rs, documents.rs, lib.rs: scan
  correction arguments and results, ocr_enhance, the Hot Folder tools and
  modules, and the lossless optimization profile.
- render raster/tests.rs: the cancel-flag and hang-guard test fixes.
- export publications.rs imports Item and items from main's layout module.
- parity takes main's evidence plus the branch's, with optimize_ui.rs at its
  new workflows/ path; ATTRIBUTION.md is regenerated.

A scripted check confirmed every line the branch adds still exists after
the merge (the one exception is that remapped parity path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Harrison-Booth

Copy link
Copy Markdown
Contributor

Provenance and surface check (read of the diff against main, plus a scoped test run; the PR is too large for GitHub's diff view, so I diffed the branch locally). Comment only. This PR adds network clients, a boot-service installer and credential storage, so the questions for the owner come first.

Questions for the owner

  1. hot_folder_service is a registered automation/MCP tool with actions install, uninstall, enable, start, stop and prepare-account. It refuses these under --root or separate automation roots (tested), but an unconfined pdfcraft-cli mcp or run session can install a boot service. Is that intended, or should these actions be CLI-only, or need a separate opt-in?
  2. hot_folder_source (save_ftp_password, forget_credentials, outlook_begin/poll) is also an MCP tool and is not gated by anything beyond the usual MCP opt-in. Same question.
  3. deny.toml widens three existing RUSTSEC ignores (RUSTSEC-2026-0370/0371/0372, zvariant, zbus_names, zvariant_derive) to cover the new keyring Secret Service backend (zbus 4). That adds a second reason to carry unmaintained crates. Is a different keyring backend acceptable instead?
  4. release.yml is rewritten by about 570 lines in a PR from an outside contributor, in the workflow that holds the Apple, Windows (Azure) and signing secrets. It also adds curl ... sh.rustup.rs | sh in one job and unpinned actions/upload-artifact@v4, download-artifact@v4, dtolnay/rust-toolchain@stable and Swatinem/rust-cache@v2 in several (other steps are SHA-pinned). Someone with release ownership should read that file line by line.

Network surface (all explicit, none at import or startup per docs/hot-folder-sources.md; I read the code to confirm the endpoints)

  • FTP/FTPS (hot_folder_sources/ftp.rs): TcpStream to a user-configured host. Explicit TLS is the default, certificates are checked against OS roots, plain FTP only with security: "plain". Passive data connections go to the control connection's peer address.
  • Outlook (outlook.rs): ureq to fixed hosts graph.microsoft.com and login.microsoftonline.com, redirects disabled, @odata.nextLink checked against the host. Uses a device-code flow with a client ID the user must register themselves.
  • Boot service (crates/hot-folder-service): Windows service, systemd/launchd units; only through explicit install actions, and the crate says nothing is installed by linking it.
  • Credentials: keyring for interactive use; service mode uses permission-protected files under <config>/secrets with owner/ACL checks.

Dependencies: 82 new packages in Cargo.lock, all from crates.io, no git dependencies. Notable direct additions: ureq (rustls, ring), rustls, rustls-native-certs, keyring, windows-service, ctrlc, sysinfo, processkit, rten-imageproc, rten-tensor, rten onnx_format, plus rcgen/x509-parser for tests. I did not run cargo deny (not installed here) and did not check each crate's licence.

Models and assets: no binary files in the diff. The three OCR assets (PP-OCRv6 recognition ONNX, PP-LCNet orientation ONNX, dictionary) are listed in ATTRIBUTION.toml as Apache-2.0 PaddleOCR files from Hugging Face / GitHub at pinned commits, with SHA-256 and licence hashes, and fetched by cargo xtask models, not committed. I did not download them or verify the hashes against the source.

Clean-room: docs/hot-folder.md cites ABBYY's public Hot Folder user guide as the reference and says no ABBYY code or assets are included; the code states that ABBYY project files are not supported. Behaviour from public docs only, as far as I can tell from the text; I cannot verify what the author read.

Ran: cargo test -p pdfcraft-hot-folder-service with CARGO_BUILD_JOBS=2 on Linux: 6 passed. Not run: the engine, automation, OCR and app tests, the Windows and macOS service code and installers, and the CI changes.

lukashold and others added 8 commits October 27, 2026 21:26
Main fetches the ocrs detection and recognition models as ONNX and finds a
complete ONNX pair first, else a complete legacy RTEN pair (storytold#5039):
- Models::in_dir keeps that order and also requires the branch's
  multilingual recognition, orientation and dictionary files. Main's
  discovery tests stage those files, and a new test refuses a pair without
  them.
- The Windows service lifecycle test stages its placeholder models as
  text-detection.onnx and text-recognition.onnx, which main's test-msi.ps1
  now requires.
- The branch's own release workflow checks the Linux packages for
  models/text-detection.onnx instead of any .rten file.
- Cargo.lock takes main's and adds only the Hot Folder crates; the roadmap
  keeps main's M10 text with the Hot Folder note; ATTRIBUTION.md is
  regenerated.

Main's parallel page-image export (storytold#5026) merges cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main uses craft_ui::ComboBox for every dropdown and bans egui::ComboBox
through clippy.toml (storytold#4988):
- The Hot Folder manager depends on craft-ui (already a workspace
  dependency) and uses its ComboBox for all nine pickers; their accessible
  names keep working through the same InnerResponse.
- The Recognize Text dialog's Output row takes main's one-option dropdown
  with the branch's mode text (Corrected scan, Searchable Image, or
  Searchable Image (Exact)); main's "the page image is not changed" tip
  shows only for exact output, the one mode where it is true.
- Cargo.lock takes main's plus the Hot Folder crates and craft-ui edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anch

Main's MSI installs for one account by default or for everyone, writes
context-sensitive (HKMU) registry rows and checks that with
check-install-policy.ps1 (storytold#4892). The Hot Folder registers a Windows
service and protects ProgramData, which only an installation for everyone
can hold:
- The HotFolder feature is enabled only with ALLUSERS=1 (level 1 needs
  INSTALLHOTFOLDER=1 AND ALLUSERS=1; level 0 when ALLUSERS<>1). Its
  checkbox and description, and the maintenance Add/Remove buttons, show
  only for an installation for everyone.
- Install and InstallUser publish the same Hot Folder events, conditioned
  on ALLUSERS=1, as the policy's same-operation check requires. The
  maintenance buttons drop the elevation icon (the policy pairs shielded
  buttons with per-user twins); elevation itself is unchanged.
- Its registry rows and the legacy image-menu cleanup use HKMU (HKLM for
  everyone), so the policy holds and a per-user installation never needs
  HKLM access. test-msi.ps1 asserts the feature conditions and the
  context-sensitive cleanup root.
- The late MajorUpgrade (afterInstallExecute) and REINSTALLMODE=amus stay,
  so upgrades keep the service account.
- install-policy.json lists the Util extension and pdfcraft-hot-folder.exe
  so the install-policy fixture builds; the service lifecycle test installs
  with ALLUSERS=1; packaging-lint keeps the branch's lint job and adds main's
  Windows installation policy job; the installation doc explains the scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's installer puts an installation for everyone under
Program Files\ArtCraft\PdfCraft. The lifecycle test's upgrade check still
hashed Program Files\PdfCraft\pdfcraft-hot-folder.exe and failed after
install, recovery and upgrade had passed on windows-2022 and -2025. It now
reads the folder from the InstallDir value the installer records. The
installation doc names the new path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the branch's OCR command arms where main moved the accessibility, script and
preferences commands into their own groups. Main now denies wildcard imports and
functions over cognitive complexity 30: the Hot Folder sources and service name their
imports, and one Hot Folder step is split into its phases (staging remote sources,
selecting settled inputs, preparing, saving, recording receipts and finishing
sources) with unchanged behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch no longer carries its own release.yml: main's call to the shared
craft-common-actions release workflow already runs packaging/*/package.*, which build
and package the Hot Folder app themselves, and uploads all of dist/release. The
packaging lint is main's shared job too, with the Hot Folder's own checks (macOS
installer scripts, Distribution template, server packaging checks) in
packaging/lint-hot-folder.sh through its extra-checks-script input. The Hot Folder
crates follow the workspace version to 0.15.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anges

pdfcraft-cli mcp now refuses every hot_folder_service action except status, and
hot_folder_source's save_ftp_password, forget_credentials, outlook_begin and
outlook_poll, unless the user starts it with --allow-hot-folder-admin. The check
lives in Automation (with_hot_folder_admin), so nested dispatch and the compact
tool_call path are covered too. pdfcraft-cli run and reading the service and
credential status are unchanged. Both new tests fail with the check disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lukashold

Copy link
Copy Markdown
Contributor Author

@Harrison-Booth thanks for the careful read. Answers in order:

  1. hot_folder_service over MCP: not intended to be open by default. pdfcraft-cli mcp now refuses every action except status unless the user starts it with --allow-hot-folder-admin (5da74e3). The check lives in Automation (with_hot_folder_admin), so nested dispatch and the compact tool_call path go through it too. pdfcraft-cli run scripts are unchanged, since the user writes and starts those.
  2. hot_folder_source over MCP: same gate. save_ftp_password, forget_credentials, outlook_begin and outlook_poll need --allow-hot-folder-admin. credential_status and outlook_folders still work without it, and neither returns a secret. Both new tests fail with the check disabled.
  3. keyring / zbus 4: I'd like to keep the Secret Service backend. The three advisories are "unmaintained", not vulnerabilities, and the same crates are already in the tree through accesskit_unix. Secret Service is the standard protected store on Linux desktops. The alternatives are worse for users: linux-native (kernel keyutils) loses passwords at reboot, and plain files are weaker than a keyring for interactive use (service mode already uses permission-checked files). If you'd rather drop it anyway, the protected-file store is a small switch, so say the word.
  4. release.yml: agreed, and it's gone. The branch now uses main's 43-line call to the shared craft-common-actions release workflow unchanged (a23b7c8). That workflow runs packaging/*/package.*, which build and package the Hot Folder app themselves, and uploads all of dist/release, so no secrets-bearing workflow changes remain in this PR. packaging-lint.yml is back to main's shared job too, plus one extra-checks-script (packaging/lint-hot-folder.sh) for the Hot Folder installer scripts. The remaining branch workflow additions (hot-folder-services.yml, hot-folder-server.yml, the CI job) pin every action by SHA and have no curl | sh.

🤖 Addressed by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants