Repository navigation
Conversation
Widen the skill-install safety surface to pub so embedders can reuse the real checks instead of keeping comment-aligned copies of them: - is_safe_path (path traversal / absolute-path / prefix-root rejection) and validate_skill_name_segment (single path-safe segment) were pub(crate); they are now pub with docs marking the embedder-reuse contract. The in-tree plugin installer already consumes both. - INSTALLED_FROM_MARKER stays pub; its doc now states the contract. - InstalledFromMarker (the .installed-from JSON schema) becomes a public struct with public fields so readers of the marker file deserialize through the shared schema instead of redefining it. - The symlink/hard-link entry rejection and the running size cap were inline in scan_tarball/extract_into; extracted unchanged into pub entry_type_is_link and pub add_entry_size, which both passes now share (scan measures header-declared sizes, extract the bytes actually read, so a forged header cannot skip the real bound). Pinvou's plugin/skill importer keeps a re-implementation whose header says it is aligned with this module by comment; it will switch to this API. Unit tests added for the link-entry and size-cap predicates; traversal and name-segment vectors already existed. No behavior change. Signed-off-by: asto <asto18089@126.com>
|
Stacked consumer: Pinvou/pinvou-agent#653 pins |
Review (fresh, from scratch)Verdict: approve-grade content — no blockers, no behavior change (proven), no smuggled changes. One MAJOR wording fix requested in the contract docs/PR body (no code change required); minors listed below. After the wording fix this is merge-ready. All required gates green ( What was verified
MAJOR — contract wording overclaims adoption/unifiability (docs + body, no code change)
Suggested fix (wording only): in the module doc, replace the parenthetical with something like "(the in-tree plugin installer consumes Minors (non-blocking)
Clean billsPerformance (allocation-free, branch-only helpers on pre-existing passes), panic paths (none outside |
The exported entry_type_is_link judgment replaces the plugin installer's last inline is_symlink/is_hard_link disjunction, so the in-tree reuse claim in the skills::install module contract is true for it too. The plugin size gate keeps its own accumulation: it must raise PluginInstallError::OversizedBundle, not InstallError::OversizedTarball. Behavior identical (same expression, pure predicate); plugins::install suite stays green (24 passed). Signed-off-by: asto <asto18089@126.com>
Review of #81 found the contract wording overclaiming on several axes; this aligns each claim with what the code and the ecosystem actually do: - Module doc: the in-tree plugin installer consumes the judgments verbatim; importers with deliberately different verdict semantics (zip's normalize-and-accept traversal) register divergence with tests rather than restate an "aligned" copy. Drop the private-module path pointer external readers cannot resolve. - InstalledFromMarker: the marker history is v1 spec/url/checksum, v2 adding schema_version/source_checksum/content_digest/installed_name/ registry_version; url/installed_name/registry_version are intentionally not modeled by the struct. - source_checksum(): the v1 fallback leg is returned verbatim and can be empty; empty reads as "changed" (fail-safe) — now stated. - is_safe_path / validate_skill_name_segment: replace "single source of truth" / "cannot drift" (no mechanism enforces embedder adoption) with reviewable-home wording; stricter embedder allowlists are policy, not drift. - entry_type_is_link: separate the predicate from caller policy (subtree filtering), and state that zip keeps the unix-mode face pinned by tests. - InstallError: it is now part of the embedder contract (downcast pattern); drop the internal-only audience statement. - skills/mod.rs: list the new contract names in the downstream-consumer re-export set so the short skills::* path reaches them. - entry_type_is_link test: pin every non-link EntryType variant (GNULongLink in particular) so a tar-crate reclassification cannot silently widen the judgment. skills::install 22 passed, plugins::install 24 passed, fmt/clippy clean. Signed-off-by: asto <asto18089@126.com>
Fix wave pushed (
|
Review of #81 found the contract wording overclaiming on several axes; this aligns each claim with what the code and the ecosystem actually do: - Module doc: the in-tree plugin installer consumes the judgments verbatim; importers with deliberately different verdict semantics (zip's normalize-and-accept traversal) register divergence with tests rather than restate an "aligned" copy. Drop the private-module path pointer external readers cannot resolve. - InstalledFromMarker: the marker history is v1 spec/url/checksum, v2 adding schema_version/source_checksum/content_digest/installed_name/ registry_version; url/installed_name/registry_version are intentionally not modeled by the struct. - source_checksum(): the v1 fallback leg is returned verbatim and can be empty; empty reads as "changed" (fail-safe) — now stated. - is_safe_path / validate_skill_name_segment: replace "single source of truth" / "cannot drift" (no mechanism enforces embedder adoption) with reviewable-home wording; stricter embedder allowlists are policy, not drift. - entry_type_is_link: separate the predicate from caller policy (subtree filtering), and state that zip keeps the unix-mode face pinned by tests. - InstallError: it is now part of the embedder contract (downcast pattern); drop the internal-only audience statement. - skills/mod.rs: list the new contract names in the downstream-consumer re-export set so the short skills::* path reaches them. - entry_type_is_link test: pin every non-link EntryType variant (GNULongLink in particular) so a tar-crate reclassification cannot silently widen the judgment. skills::install 22 passed, plugins::install 24 passed, fmt/clippy clean. Signed-off-by: asto <asto18089@126.com>
Review of #81 found the contract wording overclaiming on several axes; this aligns each claim with what the code and the ecosystem actually do: - Module doc: the in-tree plugin installer consumes the judgments verbatim; importers with deliberately different verdict semantics (zip's normalize-and-accept traversal) register divergence with tests rather than restate an "aligned" copy. Drop the private-module path pointer external readers cannot resolve. - InstalledFromMarker: the marker history is v1 spec/url/checksum, v2 adding schema_version/source_checksum/content_digest/installed_name/ registry_version; url/installed_name/registry_version are intentionally not modeled by the struct. - source_checksum(): the v1 fallback leg is returned verbatim and can be empty; empty reads as "changed" (fail-safe) — now stated. - is_safe_path / validate_skill_name_segment: replace "single source of truth" / "cannot drift" (no mechanism enforces embedder adoption) with reviewable-home wording; stricter embedder allowlists are policy, not drift. - entry_type_is_link: separate the predicate from caller policy (subtree filtering), and state that zip keeps the unix-mode face pinned by tests. - InstallError: it is now part of the embedder contract (downcast pattern); drop the internal-only audience statement. - skills/mod.rs: list the new contract names in the downstream-consumer re-export set so the short skills::* path reaches them. - entry_type_is_link test: pin every non-link EntryType variant (GNULongLink in particular) so a tar-crate reclassification cannot silently widen the judgment. skills::install 22 passed, plugins::install 24 passed, fmt/clippy clean. Signed-off-by: asto <asto18089@126.com>
Summary
Pinvou's plugin/skill importer currently keeps a re-implementation of the skill-install safety checks whose header says it is kept ``aligned with the engine's install.rs by comment'' (
checked_zip_entry_pathand friends in `pinvou3-app/src-tauri/src/features/marketplace/plugin_import.rs`). Nothing enforces that alignment — and the label is partly inaccurate: the zip sanitizer's verdicts were never point-identical with the engine's (it normalizes and accepts where the engine's `is_safe_path` rejects), and the app's name validators are deliberately stricter allowlists. Pinvou's follow-up (draft PR Pinvou/pinvou-agent#653) adopts the point-equivalent checks (`add_entry_size`, `INSTALLED_FROM_MARKER`) and registers the deliberately divergent judgments (traversal, symlink face, name allowlist, legacy marker format) with pin tests instead of restating them as "aligned" copies.This PR makes the minimal engine surface public so embedders can reuse the real checks — the same reuse path the in-tree plugin installer (
plugins::install) already took viapub(crate)foris_safe_path,validate_skill_name_segment, and the marker pair. Follow-up commits on this branch route the plugin installer's last inline link predicate through the sharedentry_type_is_linkand calibrate the contract docs. The plugin tarball's size gate keeps its own accumulation because it must raisePluginInstallError::OversizedBundlerather thanInstallError::OversizedTarball— a differently-typed error, not a divergent judgment.Exported surface (
codewhale_tui::skills::install)is_safe_path(&Path) -> bool— entry-path safety predicate: rejects path traversal (..), absolute paths, and Windows prefix/root components. Waspub(crate).entry_type_is_link(tar::EntryType) -> bool— symlink/hard-link entry rejection judgment, extracted unchanged from being inline in bothscan_tarballandextract_into(new; both passes now call it, and the plugin installer's pass two as well).add_entry_size(total, added, max_size) -> Result<u64>— running uncompressed-size cap (saturating,>-strict), extracted unchanged from being inline in both passes: scan measures header-declared sizes, extract re-measures bytes actually read so a forged header cannot skip the bound (new).validate_skill_name_segment(&str) -> Result<&str>— single path-safe segment validation for user- or archive-supplied names. Waspub(crate).INSTALLED_FROM_MARKER— alreadypub; doc now states the embedder contract.InstalledFromMarker— the.installed-fromJSON schema struct, nowpubwithpubfields (andsource_checksum()), so marker readers deserialize through the shared schema instead of redefining it.The module itself stays at its existing visibility; only these items are public. The contract names are additionally reachable through the short
skills::*re-export list.Verification
cargo fmt --all— clean.CARGO_TARGET_DIR=/tmp/cw-up/target RUST_MIN_STACK=32212254 cargo test -p codewhale-tui --lib skills::install:: -- --test-threads=4—test result: ok. 22 passed; 0 failed(includes 2 new tests:entry_type_is_link_accepts_only_link_entries— now pinning every non-linktar::EntryTypevariant — andadd_entry_size_rejects_entries_past_the_cap; traversal/absolute-path and name-segment vectors already existed and still pass).CARGO_TARGET_DIR=/tmp/cw-up/target RUST_MIN_STACK=32212254 cargo test -p codewhale-tui --lib plugins::install:: -- --test-threads=4—test result: ok. 24 passed; 0 failed(in-tree consumer of the widened surface).CARGO_TARGET_DIR=/tmp/cw-up/target cargo clippy -p codewhale-tui --lib— clean.No-Issue: visibility-only export of existing install-safety checks for embedder reuse (app-side chop tracked as the parent repo PR Pinvou/pinvou-agent 653).