RPC API: destructive ops on arbitrary paths, inconsistent param names, silent input mangling #35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bundling three related JSON-RPC API issues found during testing. They share a common theme — the RPC layer trusts inputs more than it should and surfaces results inconsistently. Happy to split into separate issues if preferred.
1.
deck.delete/deck.createoperate on arbitrary paths (no deck-marker check)Both methods accept any filesystem path and act on it without verifying it is (or will be) a slide deck. Only OS permissions stand between the caller and arbitrary directory deletion or creation.
Reproduction
Root cause
crates/hero_slides_lib/src/deck.rs:411-413Handler at
crates/hero_slides_server/src/rpc.rs:1800-1803only checksis_dir()before callingdeck_delete— no.slidesmarker, no registry lookup.crates/hero_slides_lib/src/deck.rs:394-398Same pattern in
deck_duplicate()(line 461).Suggested fix
deck.delete: require the target dir to contain.slides(already written bydeck_create), or look up in a deck registry. Reject otherwise.deck.create: usefs::create_dir(single-level) on the deck dir; requireparent_pathto already exist, or gate multi-level creation behind an explicitcreate_parents: trueflag.2. Inconsistent
pathvsdeck_pathparameter namingThe parameter for the deck location is split across the API:
pathdeck.scan,deck.get,deck.delete,deck.rename,deck.pdf,deck.pdfAsync,deck.getTheme,deck.saveTheme,deck.listThemesdeck_pathdeck.generateAsync,deck.generateJobStatus,deck.generateJobLogs,deck.fixInstructions,deck.rewriteInstructions,deck.instructTheme,deck.instructInstructions,deck.runAgent,deck.agentStatus, allslide.*methodsReferences:
crates/hero_slides_server/src/rpc.rs:148-217. This led to severalMissing parametererrors during testing.Suggested fix
Standardize on one name across the entire API.
deck_pathis the more common choice and disambiguates fromslide.path-style params if that's ever added.3. Silent input sanitization / weak validation
Several RPC handlers silently mangle or accept invalid input rather than rejecting it.
deck.create(rpc.rs:1724-1731) sanitizes thenameparameter, returningcreated: truefor a name the caller didn't ask for:slide.insertwithat: -1returnsMissing 'at' parameterinstead of a range error (rpc.rs:1406-1409):slide.saveContentaccepts empty-string content with no validation. Combined with the fact that the first save of a slide doesn't trigger versioning, this means a slide can be silently wiped.Suggested fix
deck.create: reject names that contain disallowed characters with a clear error (Invalid character '/' in name), or echo back a sanitized name with a warning field.slide.insert: parseatwithas_i64()first, then range-check (at >= 0) so the error reflects reality.slide.saveContent: at minimum, validate non-empty (or accept empty but document it). Separately, ensure versioning fires on overwrite of existing slides.Files
crates/hero_slides_lib/src/deck.rs(deck_create, deck_delete)crates/hero_slides_server/src/rpc.rs(handlers, method registration)Implementation Spec for Issue #35
Objective
Harden the JSON-RPC API surface against destructive operations on arbitrary paths, standardize parameter naming across deck/slide methods, and replace silent input mangling with explicit validation errors.
Scope decisions
hero_slides_server/src/rpc.rs,hero_slides_server/src/generate_job.rs,hero_slides_lib/src/deck.rs,hero_slides_lib/src/error.rs,hero_slides_server/openrpc.json, the dashboard JS inhero_slides_ui/static/js/dashboard.js, and the API reference doc.pathfield semantics insideServerState.decks; thebg.*namespace already usespathfor a different purpose (deck path) and is left unchanged; the generated client fileopenrpc.client.generated.rsis produced by theopenrpc_client!macro fromopenrpc.jsonat build time and is regenerated automatically.deck_pathis a wire-format breaking change for any external caller that uses the oldpathparameter againstdeck.scan,deck.get,deck.delete,deck.rename,deck.duplicate,deck.pdf,deck.pdfAsync,deck.pdfJobStatus,deck.pdfJobLogs,deck.getTheme,deck.saveTheme,deck.listThemes. Recommendation: dual-accept transition. For one release, every renamed method accepts BOTHdeck_path(preferred) andpath(deprecated, logs a warning). The OpenRPC spec advertisesdeck_pathonly. Internal UI is migrated todeck_pathin the same change. A follow-up issue removes thepathalias after one release.deck.scan: this method takes a root directory to scan (not a deck), so semanticallydeck_pathis a misnomer. Recommendation: rename its param toroot_path(withpathaccepted as deprecated alias). Calling out separately because it is not symmetric with the other rename.deck.create: the existingparent_pathparam is correct (it is not a deck path) and stays as-is. Only the silent name sanitization is changed.Files to Modify/Create
crates/hero_slides_lib/src/deck.rs—deck_createrequires existing parent and refuses pre-existing target;deck_deleterequires.slidesmarker;deck_duplicatereuses the safer create path.crates/hero_slides_lib/src/error.rs— addNotADeck { path: String }variant for the marker check (or reuseInvalidInput— see Step 1).crates/hero_slides_server/src/rpc.rs— add aparam_string()helper for dual-name extraction, rewritehandle_deck_createto reject mangled names, rewritehandle_slide_insertto useas_i64()+range-check, rewritehandle_slide_save_contentto validate non-empty, migrate everypath-based deck handler to read both names with deprecation warning logging.crates/hero_slides_server/src/generate_job.rs— same dual-name extraction inhandle_deck_pdf_async,handle_deck_pdf_job_status,handle_deck_pdf_job_logs.crates/hero_slides_server/openrpc.json— renamepathtodeck_path(orroot_pathfordeck.scan) on the listed methods; regenerated client picks this up automatically.crates/hero_slides_ui/static/js/dashboard.js— everyrpc('deck.X', { path: ... })call switched to{ deck_path: ... }(and{ root_path: ... }fordeck.scan).crates/hero_slides_ui/docs/api-reference.md— update parameter names in the reference table.Implementation Plan
Step 1: Library-level safety for deck create/delete/duplicate
Files:
crates/hero_slides_lib/src/deck.rs,crates/hero_slides_lib/src/error.rsDependencies: none
Tasks:
error.rs, reuse the existingInvalidInput(String)variant. No new variant required (keeps the public error enum stable). All new failure modes below returnHeroSlidesError::InvalidInput("...")with a clear message.deck.rs::deck_create(lines 394-405):std::fs::create_dir_all(deck_path)?with: validate thatdeck_path.parent()exists and is a directory (InvalidInput("parent directory does not exist: <parent>")otherwise), and thatdeck_pathdoes not already exist (InvalidInput("path already exists: <p>")otherwise), then callstd::fs::create_dir(deck_path)?(single-level create).std::fs::create_dir_all(deck_path.join("output"))?withstd::fs::create_dir(deck_path.join("output"))?since the parent (deck_path) just got created.deck.rs::deck_delete(lines 411-414):remove_dir_all, require eitherdeck_path.join(".slides").is_file()OR (defensive fallback) the deck exists in-registry via the caller. The library function does not have access to server state, so the check is the marker file. ReturnHeroSlidesError::InvalidInput(format!("not a deck (missing .slides marker): {}", deck_path.display()))if absent.deck.rs::deck_duplicate/copy_dir_recursive(lines 445-473):copy_dir_recursivecallsstd::fs::create_dir_all(dst)?. The top-level dst fromdeck_duplicateshould not already exist; the existing precondition indeck_duplicatedoes not check that. Add: before callingcopy_dir_recursive, returnInvalidInput("path already exists: <new_path>")ifnew_path.exists(). Internal recursion still usescreate_dir_allfor nested subdirs (which is correct for the recursion) — leave that alone.Step 2: Add
param_stringdual-name helper in rpc.rsFiles:
crates/hero_slides_server/src/rpc.rsDependencies: none (independent of Step 1)
Tasks:
Step 3: Fix
deck.createsilent sanitizationFiles:
crates/hero_slides_server/src/rpc.rs(handle_deck_create, lines 1712-1791)Dependencies: none
Tasks:
"deck name must not be empty"._,-. Reject if any char is outside the set with error"deck name contains disallowed characters; allowed: a-z A-Z 0-9 _ -"..or-(filesystem-hostile) with"deck name must not start with '.' or '-'".' 'in input outright (caller must pass snake_case). This makes the contract explicit.namereflecting the validated input (no longer a different value than the caller sent).handle_deck_rename(lines 1816-1863) andhandle_deck_duplicate(lines 1865-1924). Extract the validation into a private fnvalidate_deck_name(s: &str) -> Result<String, String>to share.Step 4: Migrate
pathparams todeck_path(with deprecation alias)Files:
crates/hero_slides_server/src/rpc.rs,crates/hero_slides_server/src/generate_job.rsDependencies: Step 2
Tasks:
rpc.rs, rewrite parameter extraction in each of the following handlers usingparam_string(params, "deck_path", Some("path"), "<method>"):handle_deck_get(line 472)handle_deck_delete(line 1793)handle_deck_rename(line 1816)handle_deck_duplicate(line 1865)handle_deck_pdf(line 593)handle_deck_get_theme(line 1601)handle_deck_save_theme(line 1620)handle_deck_list_themes(line 1641)handle_deck_scan(line 405), useparam_string(params, "root_path", Some("path"), "deck.scan")— semantically correct name.generate_job.rs, same migration in:handle_deck_pdf_async(line 565)handle_deck_pdf_job_status(line 587)handle_deck_pdf_job_logs(line 599)handle_deck_get_themeresponse (line 1615), echo backdeck_pathinstead ofpathin the JSON body, and also update the duplicated key inhandle_deck_deleteresponse (line 1813) to include bothpath(for backwards compat in payload) ANDdeck_path. Document thatpathfield in the response is also deprecated; consumers should readdeck_path. The minimal change: keeppathfield in responses unchanged this release, adddeck_pathalongside. Same forhandle_deck_rename,handle_deck_duplicateresponse payloads.Step 5: Fix
slide.insertnegative index errorFiles:
crates/hero_slides_server/src/rpc.rs(handle_slide_insert, lines 1400-1432)Dependencies: none (parallel with Steps 1-4)
Tasks:
as_u64()+as u32pattern at lines 1406-1409 with:slide_insertatdeck.rs:938documents thatpositionis 1-indexed; ifat == 0should also be rejected, addif at_i < 1check instead. Recommendation:at_i < 1becauseinsert_slidedocuments 1-indexed positions.Step 6: Fix
slide.saveContentempty-content acceptanceFiles:
crates/hero_slides_server/src/rpc.rs(handle_slide_save_content, lines 1176-1204)Dependencies: none (parallel)
Tasks:
content(line 1187), add:Step 7: Update OpenRPC spec (
openrpc.json)Files:
crates/hero_slides_server/openrpc.jsonDependencies: Step 4 (semantics must match)
Tasks:
namefield of the path param from"path"to"deck_path"and update thedescriptionaccordingly.deck.scan, rename parampath->root_path.deck.create, update thenameparam description to reflect strict validation (no longer "will be sanitized to lowercase snake_case").slide.saveContent, update thecontentparam description to "must be non-empty".slide.insert, update theatparam description to "1-indexed position; must be >= 1".description: "The legacy parameter namepathis accepted but deprecated and emits a server-side warning; it will be removed in a future release."openrpc.client.generated.rs) is rebuilt by theopenrpc_client!macro on next compile; no manual edit needed.Step 8: Update dashboard JS callers
Files:
crates/hero_slides_ui/static/js/dashboard.jsDependencies: Step 4 (server must accept new name; safe because Step 4 also keeps the old name)
Tasks:
Replace the following call sites verbatim:
rpc('deck.listThemes', { path: deckPath })->{ deck_path: deckPath }.rpc('deck.get', { path })->{ deck_path: path }.rpc('deck.get', { path })->{ deck_path: path }.rpc('deck.pdfAsync', { path })->{ deck_path: path }.rpc('deck.pdfJobLogs', { path, lines: 2000 })->{ deck_path: path, lines: 2000 }.rpc('deck.pdfJobStatus', { path })->{ deck_path: path }.rpc('deck.delete', { path })->{ deck_path: path }.rpc('deck.rename', { path, new_name })->{ deck_path: path, new_name }.rpc('deck.duplicate', { path, new_name })->{ deck_path: path, new_name }.rpc('deck.scan', { path })->{ root_path: path }.rpc('deck.getTheme', { path: selectedDeckPath })->{ deck_path: selectedDeckPath }.rpc('deck.saveTheme', { path: selectedDeckPath, content })->{ deck_path: selectedDeckPath, content }.rpc('deck.get', { path: versionDeckPath })->{ deck_path: versionDeckPath }.rpc('deck.scan', { path: scanPath })->{ root_path: scanPath }.{ method: 'deck.pdfJobLogs', params: { path: ..., lines: 5000 } }->params: { deck_path: ..., lines: 5000 }.deck.create) as-is —parent_pathis unchanged.Step 9: Update API reference doc
Files:
crates/hero_slides_ui/docs/api-reference.mdDependencies: Step 7
Tasks:
deck_path(orroot_pathfordeck.scan).pathas accepted-but-deprecated.Step 10: Tests
Files: new file
crates/hero_slides_lib/tests/deck_safety_test.rs; existingcrates/hero_slides_rhai/tests/integration_test.rsmay need adjustment if name strictness breaks any test fixtureDependencies: Step 1, Step 3
Tasks:
hero_slides_lib:deck_deleteon a directory without.slidesmarker returnsInvalidInput.deck_createon a path whose parent does not exist returnsInvalidInput.deck_createon a pre-existing path returnsInvalidInput.deck_duplicateto a target that already exists returnsInvalidInput.hero_slides_server(or integration test):handle_slide_insertwithat: -1returns a range error mentioning>= 1, notMissing 'at'.handle_slide_save_contentwithcontent: ""returns the empty-content error.handle_deck_createwithname: "Foo Bar!"returns disallowed-character error (no longer silently sanitized).handle_deck_getaccepts{deck_path: ...}and (legacy){path: ...}and produces the same result; warning is logged.hero_slides_rhai/tests/integration_test.rsfor any test that creates a deck viadeck_createwith a path whose parent does not yet exist — if so, fix the test to mkdir the parent first.Acceptance Criteria
deck.deleteon an arbitrary writable directory without a.slidesmarker is rejected with a clear error and the directory is not removed.deck.createon a parent path that does not exist is rejected (no silent multi-level mkdir).deck.createwith a name containing whitespace, punctuation, or other disallowed characters is rejected; valid names succeed and the responsenamefield equals the input.deck_path(preferred) and the legacypath(with a deprecation warning logged).deck.scanacceptsroot_path(preferred) and the legacypathwith deprecation warning.slide.insertwithat: -1returns a range error referencing the bound, not a missing-parameter error.slide.saveContentwith emptycontentreturns an explicit empty-content error.dashboard.jsuse the new parameter names; manual smoke test of the dashboard (list, scan, create, rename, duplicate, delete, get, theme load/save, PDF) all succeed.openrpc.jsonreflects the new param names; the generated client compiles.hero_slides_rhai/tests/integration_test.rsstill pass (after any necessary parent-directory fixups).Risks / Notes
openrpc_client!macro emits typed Rust functions whose argument names matchopenrpc.json. Renaming params will rename generated function signatures. There are currently no internal Rust callers of the SDK methods affected (verified by grep onhero_slides_sdk), so this is safe internally; downstream users ofhero_slides_sdkwill see a compile error and must rename arguments. Acceptable as a coordinated breaking change.pathfor deck path*: these were not listed in the issue and are deliberately left untouched to keep this change focused. A follow-up issue should normalize them too.path: keeping the legacy field in responses (alongsidedeck_path) preserves callers reading the response. Removing it later is a separate breaking change; not in this issue.deck.scansemantic rename: choosingroot_pathoverdeck_pathis more accurate but introduces a third parameter name. The dual-accept transition handles this cleanly, but reviewers should explicitly sign off on this naming.deck.createis a behavior change: callers that previously relied on auto-sanitization (e.g., passing "My Deck") will now see an error. The dashboard does not auto-sanitize on the client side; UX should be updated to either pre-sanitize before calling or surface the error to the user. Recommend adding client-side normalization indashboard.jsline 4208 before the RPC call (e.g., replace spaces with_, strip disallowed chars, lowercase) and pass the normalized name; this keeps the user-visible behavior unchanged while making the wire contract strict.pathaliases after one release. Tag deprecation log lines with a consistent string ("deprecated parameter") so they are easy to grep in logs.Test Results
Command:
cargo test --workspacefailed to compilehero_slidesbinary due to a pre-existing error incrates/hero_slides/src/main.rs:323(DeckScanInputfieldpathrenamed toroot_path) — this file is unrelated to and unmodified by this change. Fell back tocargo test -p hero_slides_lib -p hero_slides_server.Aggregate: 82 total / 81 passed / 1 failed / 0 ignored
Per-crate:
Failures
hero_slides_lib::deck::tests::test_scan_decks: Caused by this change: the test callsdeck_create(&root.join("sub").join("deck_b"))before creatingsub/, which worked under the oldcreate_dir_allbehavior. The newdeck_createenforces "parent must exist" as part of the safety hardening in this PR. The test setup needs to be updated to createsub/before callingdeck_create.New tests added by this change
crates/hero_slides_lib/tests/deck_safety_test.rs— 6 tests for deck_create / deck_delete / deck_duplicate safety.crates/hero_slides_server/src/rpc.rs(in-module) — 8 tests forvalidate_deck_nameandparam_stringhelpers.Implementation Summary
All three issue groups implemented per the posted spec, on branch
development.Files modified
crates/hero_slides_lib/src/deck.rsdeck_createrequires existing parent and refuses pre-existing target; usesfs::create_dir(single-level).deck_deleterequires.slidesmarker.deck_duplicaterefuses pre-existing target.crates/hero_slides_lib/Cargo.tomltempfile = "3"to[dev-dependencies].crates/hero_slides_server/src/rpc.rspub(crate) param_stringhelper (dual-name extraction with deprecation log). Newvalidate_deck_namehelper.handle_deck_create,handle_deck_rename,handle_deck_duplicateuse strict validation.handle_slide_insertusesas_i64+ range check (at >= 1).handle_slide_save_contentrejects empty content. All deck-* path-taking handlers migrated toparam_string("deck_path", Some("path"), ...).handle_deck_scanmigrated toroot_pathwithpathlegacy. Response payloads include bothpathanddeck_pathfor transition.crates/hero_slides_server/src/generate_job.rshandle_deck_pdf_async,handle_deck_pdf_job_status,handle_deck_pdf_job_logs. Importsparam_stringfromcrate::rpc.crates/hero_slides_server/openrpc.jsonpath->deck_pathon 11 methods;path->root_pathondeck.scan. Updated descriptions fordeck.create(strict name validation),slide.saveContent(must be non-empty),slide.insert(1-indexed). Added deprecation notes.crates/hero_slides/src/main.rsDeckScanInput { path: ... }->DeckScanInput { root_path: ... }to match the regenerated SDK client.crates/hero_slides_ui/static/js/dashboard.jsdeck_pathorroot_path).crates/hero_slides_ui/docs/api-reference.mdFiles created
crates/hero_slides_lib/tests/deck_safety_test.rsTest results
cargo build --workspace: clean.cargo test -p hero_slides_lib -p hero_slides_server:deck_safety_testAll 14 newly-added tests pass. One pre-existing test (
test_scan_decks) was updated to create thesub/parent before callingdeck_create, since the new precondition requires the parent to exist — assertion logic untouched.Acceptance criteria
deck.deleteon a directory without.slidesmarker is rejected; directory is not removed.deck.createon a non-existent parent is rejected (no silent multi-level mkdir).deck.createwith a name containing whitespace, punctuation, or other disallowed characters is rejected; valid names are stored verbatim.deck_path(preferred) and the legacypath(with deprecation log).deck.scanacceptsroot_path(preferred) and the legacypath.slide.insertwithat: -1orat: 0returns a range error referencing the bound, not a missing-parameter error.slide.saveContentwith emptycontentreturns the explicit empty-content error.dashboard.jsuse the new parameter names.openrpc.jsonreflects the new param names; the generated client compiles; downstream caller incrates/hero_slides/src/main.rswas updated.Notes / follow-ups
deck.readBackgroundwas discovered usingpath:in the dashboard JS but is NOT in the issue scope (the server handler still only acceptspath). Left as-is. Worth normalizing in a follow-up.bg.*methods also usepathfor a deck path; out of scope here.pathanddeck_pathfor transition; removing the legacypathfield is a separate breaking change for a future release.pathlegacy aliases on RPC inputs should be removed in a follow-up release; deprecation log lines use the consistent prefix<method>: parameter 'path' is deprecated, use 'deck_path'for easy grepping.cargo test --workspaceinitially failed at compile-time oncrates/hero_slides/src/main.rsbecause of the SDK rename — fixed in this change.No commits have been made; the orchestrator will create a single commit on
developmentafter this summary is posted.