fix(store): enforce portable keys across methods - #216
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: blocked before merge. Reviewed September 3, 2026, 10:05 AM ET / 14:05 UTC. ClawSweeper reviewWhat this changesThe PR makes every FileStore key-taking method reject noncanonical aliases such as parent segments and backslashes, adds cross-method regression coverage, and documents the strict portable-key contract. Merge readiness⛔ Blocked before merge - 1 item remains Keep this PR open as a merge-ready repair. The prior compatibility-policy blocker is resolved by the explicit strict-key decision in the discussion, and the patch consistently restores FileStore’s documented portable-key identity without changing Root confinement. Priority: P1 Review scores
Verification
How this fits togetherFileStore is the package’s keyed file abstraction for application data, while Root provides the lower-level confined filesystem operations. Application-supplied keys pass through FileStore validation before reads, writes, existence checks, or deletion reach Root. flowchart LR
A[Application key] --> B[FileStore method]
B --> C[Open store root]
C --> D[Portable key validation]
D -->|canonical key| E[Root confinement]
D -->|invalid alias| F[invalid-path error]
E --> G[Read write exists or remove]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the strict documented FileStore key policy with the stated 0.7.3 compatibility note, retaining Root as the explicit opt-in surface for broader confined existing-object path semantics. Do we have a high-confidence way to reproduce the issue? Yes—source inspection establishes the base-path discrepancy: selected async operations delegated raw aliases to Root while FileStore path resolution and synchronous methods rejected them. The added regression suite provides a focused current-head verification path. Is this the best way to solve the issue? Yes—the shared strict parser is the narrowest maintainable repair because it makes all FileStore methods enforce one documented portable key grammar while preserving Root’s separate confinement contract. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e940b0da8717. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (1 earlier review cycle)
|
|
Maintainer decision: approve strict portable-key rejection for this
The supported upgrade is explicit and bounded: callers should pass canonical portable FileStore keys. Callers that intentionally need broader confined existing-object pathname semantics can use The compatibility impact is called out in No compatibility shim or mode should be added for these undocumented aliases. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Landed as Verified final head
Fresh packed-consumer proof covered physical macOS 26.6.2 arm64 and Debian 12 Linux arm64 local containers, exact Node.js 22.0.0/22.23.2/24.20.0, native The compatibility policy is intentional and recorded in this PR: FileStore callers must use canonical portable keys; callers intentionally needing broader confined existing-object path semantics use Production became smaller: +4/-9 lines, net -5, with FileStore still under its 500-line budget. |
Summary
FileStoremethod, in private and non-private stores;invalid-pathinstead of letting selected async methods delegate them toRoot;Root's broader confined existing-object compatibility;Root cause
FileStorealready documented everyrelativePath, including reads,exists, andremove, as a portable store key. Its central assertion nevertheless returned raw inputs containing.., backslashes, or absolute syntax so some async methods could delegate them toRoot. Methods that also calledresolveStorePath()—includingpath(), all sync methods, writes, andjson()construction—rejected those same spellings.That split produced method- and platform-dependent key identity:
a/../bon POSIX and returned the bytes ofb, whilepath()and sync reads rejected the key;exists, andremoveaccepted a literal POSIX filenamea\b, while Windows interpreted the same spelling as a separator and strict FileStore methods rejected it;No root escape was demonstrated. This is a portable key identity and method-parity defect; filesystem confinement remains owned by
Root.History confirms the distinction is intentional: the portable FileStore contract was added separately from Root's legal existing-object compatibility. A later canonicalization change introduced the raw delegation branch and changed POSIX backslashes from separator conversion to literal lookup without naming that behavior as a compatibility surface.
Repair
assertRelativePath()now reusessplitSafeRelativePath()and then applies FileStore's stricter canonical checks. It rejects empty, dot/parent, empty-segment, backslash, absolute/rooted, drive-relative, NUL, non-NFC, padded, and trailing-dot/space spellings while preserving accepted nested/NFC names,.hidden,a..b, internal spaces, and non-drive colons such as timestamps.The implementation removes the exceptional delegation path and its redundant drive check: production delta is +4/-9, net -5 lines.
src/file-store.tsremains below its 500-line budget.Compatibility and boundaries
For an existing root, async reads,
exists, andremovenow rejecta/../b,a\b, and the other nonportable aliases withinvalid-path, matchingpath(), writes, sync methods, andjson()construction.Evaluation order is intentionally unchanged:
exists/removereportnot-foundandIfExistsreaders returnnull, even for an invalid key;copyInstill reads its source before destination-key validation;writeJsonstill serializes before destination-key validation.This does not change
Root, confinement/link/identity policy, pruning, native code, exports/types, standalonejsonStore({ filePath }), tilde handling, Windows device/ADS/case policy, or dependencies.Validation
CI=1 pnpm check: 201 test files passed / 2 skipped, 7,080 tests passed / 80 skipped;git diff --check: passed;The regression suite covers every keyed async method (
path,open, all read forms,exists,remove, all write forms,writeStream,copyIn, andjson) and every keyed sync method, for both confirmed aliases withprivate: falseandtrue. It verifies invalid reads disclose no sentinel bytes, invalid mutations preserve byte-exact fixtures, missing-root precedence stays stable, accepted Unicode/nested keys round-trip, JSON helpers work with locking on/off, and directRootcontrols retain broader confined spellings while rejecting escapes.Fresh packed-consumer proof
Candidate package SRI:
sha512-+ZAByrjWo95ZkmK0Df7jJ+jVGuhvZiPXZvu/XMs0zV41FZuViy9M6DbsNk0x9eLrUoxIvtFussd76FnxAdYjuQ==A fresh physical consumer ran on macOS 26.6.2 arm64, and fresh local containers ran Debian 12 arm64. Each environment exercised exact Node.js 22.0.0, 22.23.2, and 24.20.0 in native modes
offandrequire.Every lane exercised all 22 keyed methods across both private modes for
a/../banda\b, checked exact fixture preservation, pinned missing-root precedence, and ran successful nested Unicode controls. Nativeoffloaded no package.nodebinding. Everyrequirelane actually loaded its matching@openclaw/fs-safe-darwin-arm64or@openclaw/fs-safe-linux-arm64-gnubinary; the key-validation behavior itself remains JavaScript-owned.The baseline packed-main/released behavior was captured in both native modes before this repair. Linux results are local-container proof, not remote/AWS proof. Windows filesystem behavior will be covered by exact-head CI, including the new cross-platform regression suite; no separate physical Windows packed consumer is claimed.