Repository navigation
fix: unwrap API envelope in memory tauriCommands and fix file path scope - #172
Conversation
After the controller registry migration (tinyhumansai#138), memory RPC methods return ApiEnvelope responses but the frontend expected flat data. This caused "map is not a function" errors on the Intelligence page. - Unwrap envelope in memoryListDocuments, memoryListNamespaces, aiListMemoryFiles, and aiReadMemoryFile in tauriCommands.ts - Fix resolve_existing_memory_path and resolve_writable_memory_path to scope against workspace root instead of memory/ subdirectory — workspace files (MEMORY.md, SOUL.md) live at the root Closes tinyhumansai#170
📝 WalkthroughWalkthroughUpdates to response unwrapping logic in TypeScript command handlers for memory operations, handling varying envelope formats. Simultaneously refactors Rust path resolution functions to canonicalize paths relative to the workspace directory instead of memory root parent directory. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/memory/ops.rs (1)
352-368:⚠️ Potential issue | 🔴 CriticalCheck the canonical parent before
create_dir_all.Line 362 mutates the filesystem before Line 367 enforces the workspace boundary. If any existing component under the workspace is a symlink to an external directory,
create_dir_all(parent)can create directories outside the workspace and only then return the escape error.Suggested fix
async fn resolve_writable_memory_path(relative_path: &str) -> Result<PathBuf, String> { validate_memory_relative_path(relative_path)?; let workspace_dir = current_workspace_dir().await?; let canonical_workspace = workspace_dir .canonicalize() .map_err(|e| format!("resolve workspace dir {}: {e}", workspace_dir.display()))?; let full_path = workspace_dir.join(relative_path); let parent = full_path .parent() .ok_or_else(|| "memory path must include a file name".to_string())?; + let mut existing_ancestor = parent; + while !existing_ancestor.exists() { + existing_ancestor = existing_ancestor + .parent() + .ok_or_else(|| "memory path escapes the workspace directory".to_string())?; + } + let canonical_existing_ancestor = existing_ancestor + .canonicalize() + .map_err(|e| format!("resolve memory parent {}: {e}", existing_ancestor.display()))?; + if !canonical_existing_ancestor.starts_with(&canonical_workspace) { + return Err("memory path escapes the workspace directory".to_string()); + } std::fs::create_dir_all(parent) .map_err(|e| format!("create memory path {}: {e}", parent.display()))?; let resolved_parent = parent .canonicalize() .map_err(|e| format!("resolve memory parent {}: {e}", parent.display()))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/ops.rs` around lines 352 - 368, In resolve_writable_memory_path: avoid creating directories before verifying they won't escape the workspace by canonicalizing an existing ancestor first; after validate_memory_relative_path and computing workspace_dir/canonical_workspace/full_path and parent, walk up from parent to find the nearest existing ancestor, canonicalize that ancestor and assert it starts_with canonical_workspace, then call std::fs::create_dir_all(parent), and finally canonicalize the created parent (resolved_parent) and re-check starts_with canonical_workspace to be safe; reference resolve_writable_memory_path, parent, resolved_parent, canonical_workspace, create_dir_all.
🧹 Nitpick comments (2)
src/openhuman/memory/ops.rs (1)
336-349: Add debug/trace logs to the new workspace-root path resolution flow.These helpers now enforce the read/write trust boundary, but failures only surface as generic strings. Logging the requested relative path, canonical workspace, and reject reason will make field debugging much easier.
As per coding guidelines, "Add substantial debug logging on new/changed flows using log/tracing at debug or trace level in Rust".
Also applies to: 352-368
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/ops.rs` around lines 336 - 349, In resolve_existing_memory_path add trace/debug logging around the workspace-root resolution: before canonicalizing log the incoming relative_path and workspace_dir (call sites: validate_memory_relative_path and current_workspace_dir), after computing canonical_workspace and resolved log both canonical paths, and on each error branch include the underlying error and a clear reject reason (e.g., canonicalize workspace failure, canonicalize memory path failure, or path escapes workspace) using the project’s tracing/log macros so failures surface with the relative path, canonical workspace, resolved path, and error details; apply the same pattern to the similar flow in the neighboring function handling lines ~352-368.app/src/utils/tauriCommands.ts (1)
220-228: Centralize the response-shape normalizer and log fallback paths.The four branches duplicate anonymous envelope types and silently swallow unknown shapes with
[]/''. A small typed normalizer (RpcOutcome<T>+ApiEnvelope<T>) with namespaced debug logs when a fallback path is taken would keep the contract in one place and make future drift obvious.As per coding guidelines, "Prefer interface for defining object shapes in TypeScript" and "Add substantial debug logging on new/changed flows using namespaced debug logs in React/app code".
Also applies to: 235-243, 341-351, 358-366
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/utils/tauriCommands.ts` around lines 220 - 228, Create a single typed normalizer to centralize the envelope-unwrapping logic (define RpcOutcome<T> and ApiEnvelope<T> interfaces and a function like normalizeRpcResponse<T>(resp: unknown): RpcOutcome<T>) and replace the duplicated ad-hoc branches around the openhuman.memory_list_documents call (and the other occurrences at the noted ranges) with calls to that normalizer; inside the normalizer perform tight type checks, return a consistent typed result, and emit namespaced debug logs (e.g., "app:tauriCommands:normalizeRpcResponse") whenever you take a fallback path or encounter an unexpected shape so the fallback to []/"" is logged and the contract is enforced in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/utils/tauriCommands.ts`:
- Around line 220-228: memoryListDocuments (and the other three similar helpers)
are reading the registry envelope directly but must first unwrap the RpcOutcome
wrapper; update each helper that calls callCoreRpc to check if the returned resp
is an object with a top-level "result" property and, if so, replace resp with
resp.result before performing the existing envelope checks for
"data"/"files"/"content". Concretely: in memoryListDocuments (and the three
analogous functions near the later diffs), after awaiting callCoreRpc, do a safe
check like if (resp && typeof resp === 'object' && !Array.isArray(resp) &&
'result' in resp) resp = (resp as Record<string, unknown>).result; then continue
the existing logic that unwraps resp.data / resp.files / resp.content so callers
receive the inner ApiEnvelope payload instead of the RpcOutcome wrapper; follow
the same pattern already used in memoryGraphQuery.
---
Outside diff comments:
In `@src/openhuman/memory/ops.rs`:
- Around line 352-368: In resolve_writable_memory_path: avoid creating
directories before verifying they won't escape the workspace by canonicalizing
an existing ancestor first; after validate_memory_relative_path and computing
workspace_dir/canonical_workspace/full_path and parent, walk up from parent to
find the nearest existing ancestor, canonicalize that ancestor and assert it
starts_with canonical_workspace, then call std::fs::create_dir_all(parent), and
finally canonicalize the created parent (resolved_parent) and re-check
starts_with canonical_workspace to be safe; reference
resolve_writable_memory_path, parent, resolved_parent, canonical_workspace,
create_dir_all.
---
Nitpick comments:
In `@app/src/utils/tauriCommands.ts`:
- Around line 220-228: Create a single typed normalizer to centralize the
envelope-unwrapping logic (define RpcOutcome<T> and ApiEnvelope<T> interfaces
and a function like normalizeRpcResponse<T>(resp: unknown): RpcOutcome<T>) and
replace the duplicated ad-hoc branches around the
openhuman.memory_list_documents call (and the other occurrences at the noted
ranges) with calls to that normalizer; inside the normalizer perform tight type
checks, return a consistent typed result, and emit namespaced debug logs (e.g.,
"app:tauriCommands:normalizeRpcResponse") whenever you take a fallback path or
encounter an unexpected shape so the fallback to []/"" is logged and the
contract is enforced in one place.
In `@src/openhuman/memory/ops.rs`:
- Around line 336-349: In resolve_existing_memory_path add trace/debug logging
around the workspace-root resolution: before canonicalizing log the incoming
relative_path and workspace_dir (call sites: validate_memory_relative_path and
current_workspace_dir), after computing canonical_workspace and resolved log
both canonical paths, and on each error branch include the underlying error and
a clear reject reason (e.g., canonicalize workspace failure, canonicalize memory
path failure, or path escapes workspace) using the project’s tracing/log macros
so failures surface with the relative path, canonical workspace, resolved path,
and error details; apply the same pattern to the similar flow in the neighboring
function handling lines ~352-368.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1a09cc54-d70b-4e64-92e3-a58e5a4808d6
📒 Files selected for processing (2)
app/src/utils/tauriCommands.tssrc/openhuman/memory/ops.rs
fix: resolve race condition and message persistence bugs in conversat…
…ope (tinyhumansai#172) After the controller registry migration (tinyhumansai#138), memory RPC methods return ApiEnvelope responses but the frontend expected flat data. This caused "map is not a function" errors on the Intelligence page. - Unwrap envelope in memoryListDocuments, memoryListNamespaces, aiListMemoryFiles, and aiReadMemoryFile in tauriCommands.ts - Fix resolve_existing_memory_path and resolve_writable_memory_path to scope against workspace root instead of memory/ subdirectory — workspace files (MEMORY.md, SOUL.md) live at the root Closes tinyhumansai#170
…solve race condition and message persistence bugs in conversat…\n
…ope (tinyhumansai#172)\n\nAfter the controller registry migration (tinyhumansai#138), memory RPC methods\nreturn ApiEnvelope responses but the frontend expected flat data.\nThis caused "map is not a function" errors on the Intelligence page.\n\n- Unwrap envelope in memoryListDocuments, memoryListNamespaces,\n aiListMemoryFiles, and aiReadMemoryFile in tauriCommands.ts\n- Fix resolve_existing_memory_path and resolve_writable_memory_path\n to scope against workspace root instead of memory/ subdirectory —\n workspace files (MEMORY.md, SOUL.md) live at the root\n\nCloses tinyhumansai#170\n
tinymemory v1.20.0 ships tinyhumansai/tinymemory#176 (the hosted families, TreeSummary::preview, contract 4.2) on top of v1.18.0 and v1.19.0, so the module and the contract this host compiles against are one release again: - vendor/tinymemory sits on the v1.20.0 tag; - the registry record names v1.20.0 and its 11 archives, with digests copied from the release's checksum.toml; - the seven CI fetches pin the ubuntu-22.04-x86_64 archive and its digest (checked against the published asset); - ARTIFACT_CAPABILITIES_PIN is 1.20.0; the capability families are unchanged since v1.17.0, so the list stays; - the tinymemory module-pin exemption is gone, as its own reason asked once tinyhumansai#170, tinyhumansai#172, tinyhumansai#173 and tinyhumansai#174 shipped in a release. The module-pin gate now passes for tinymemory. It still fails on main's pins for other modules, which this PR does not touch.
ci: run the layout migration e2e in the storage lanes
Summary
ApiEnveloperesponses in memorytauriCommands.tsfunctions so the UI receives flat dataops.rsto allow reading files at workspace rootProblem
ApiEnvelope({ data: { ... }, meta: { ... } }) but frontend expects flat arrays/strings — causesmap is not a functioncrashes on the Intelligence pageresolve_existing_memory_pathscoped toworkspace/memory/but workspace files (MEMORY.md,SOUL.md) live atworkspace/root — reading them was rejected with "memory path escapes the memory directory"Solution
tauriCommands.ts: Unwrap envelope inmemoryListDocuments,memoryListNamespaces,aiListMemoryFiles,aiReadMemoryFilewith defensive checks for both envelope and flat formatsops.rs: Changeresolve_existing_memory_pathandresolve_writable_memory_pathto scope against workspace root instead ofmemory/subdirectory. Path traversal still blocked byvalidate_memory_relative_pathSubmission Checklist
cargo check+cargo fmt+yarn typecheckpassImpact
Related
Summary by CodeRabbit