Skip to content

Release cut for the Aug adopt/webui hardening + 2 residual staging gaps #288

Description

Two asks: a release cut, and two residual staging gaps the August hardening left open.

Ask 1 — cut a release

PyPI skillopt latest is 0.2.0, released 2026-07-02, with no release since (checked today; pyproject.toml at main still says 0.2.0). Since that wheel was built, main merged:

So pip install skillopt today installs an adopt() with none of #241's checks and a webui that binds 0.0.0.0 by default — while the fixes have been merged for a month. #117 (closed) already showed the practical cost of this exact lag: a wheel-content bug fixed at HEAD that kept biting every fresh install. A 0.2.1 / 0.3.0 cut would make the installable artifact match the hardened tree.

Ask 2 — two residual gaps at HEAD (main @ 79124b37, skillopt_sleep/staging.py blob 615c5ac8cf4c70ccb93e0283e8f7b1439890b6e2)

(a) the legacy SKILL/CLAUDE pair adopt still has no roots containment

_live_target_within_roots (L368 — its own docstring: "the manifest is not a trust boundary") is called on exactly one path: the per-skill adoption (L1508). The legacy-pair block at the bottom of the file (L~3058-3130) validates pins, symlinks/hardlinks, and realpath identity — but never checks the live target against the roots recorded when the night was staged, and _planned_live_directories (L3069) will create parent directories for an absent target at any absolute *.md path the manifest names. The per-skill path gained exactly this containment in #241; the legacy pair didn't. Suggested: re-derive legacy live targets from current config the way the per-skill path does, and run the same _live_target_within_roots check there.

(b) staging has no provenance marker — a staging tree that arrives with a cloned repo is indistinguishable from engine output

Staging lives at <project>/.skillopt-sleep/staging (L829) — project-relative. Nothing about a staging night records that the local engine produced it: the manifest format, file layout, and sha256 pins are all self-consistent and documented, and adopt verifies self-consistency only. #241's threat model (from the PR body) was locally generated, then tampered — "it needs local write access to the staging manifest". The repo-shipped vector needs no local tampering: a cloned project can carry a ready-made .skillopt-sleep/staging/<night>/ and a README/CONTRIBUTING line like "run skillopt-sleep adopt to apply our pre-staged tuned skills". On the current 0.2.0 wheel that is an arbitrary-destination write of repo-authored bytes (verified below); at HEAD the per-skill path confines it to the skills roots, but the legacy pair (gap a) does not.

Suggested: a provenance marker the repo cannot forge — a state-side registry of staged nights (outside the project, e.g. under the sleep state dir), or an HMAC of the manifest keyed by a sleep-state secret — so adopt rejects staging that did not originate from the local engine.

Reproduction on the current wheel (0.2.0)

A scratch clone containing only .skillopt-sleep/staging/20260920-120000/{manifest.json, proposed_SKILL.md, proposed_CLAUDE.md}, with the manifest's live_skill_path set to ../../../../../staged-outside-repo.md, then the product's own documented apply command in that directory:

$ skillopt-sleep adopt
[sleep] adopted from <clone>\.skillopt-sleep\staging\20260920-120000
   -> ../../../../../staged-outside-repo.md
   -> ./CLAUDE.md

Result: the repo-authored bytes land at the drive root (the overlong ../ chain clamps there on Windows — destination independent of clone depth and username), plus the clone's own CLAUDE.md is overwritten. With an absolute live_skill_path under <home>\.claude\skills\<name>\SKILL.md, the bytes land in the user-global skills root that Claude Code auto-loads in every future session, every project. The wheel's adopt() core (staging.py L137-161 in 0.2.0) is makedirs + shutil.copy2 on the manifest value — no path safety, no pins, no authorship check. At HEAD these specific writes are contained by #241 on the per-skill path (which is why the release cut matters first); the residual is gaps (a) and (b) above.

Happy to help with either a release checklist or a PR for the legacy containment + provenance marker.

Activity

  1. Yif-Yang commented on Sep 30, 2026

    @Yif-Yang
    Contributor

    The work remains split across three requirements: a release containing the already-merged hardening, an authoritative legacy adoption boundary, and evidence that staging originated from the local engine. #289/#290 provide recorded-root containment, but their root authority still needs correction; #289 also has an explicit-target compatibility regression. Neither supplies staging provenance or a release.

    #264's WebUI/authentication work passes the reviewed tests, but its SpreadsheetBench change is now held after a differential downstream-boundary finding. The author has been asked to validate persistent destinations before continuation or separate the independent hardening. Scoped green tests do not resolve that finding.

    Please keep this issue open and coordinate the overlapping root changes as one implementation. Security-sensitive reproductions remain private under SECURITY.md. This status update does not commit to a release date.

  2. Yif-Yang commented on Oct 7, 2026

    @Yif-Yang
    Contributor

    Status update after the latest merges, superseding the earlier statement that #264 was still held:

    This issue remains open: neither merge delivers a new release, an authoritative legacy-adoption boundary, or evidence that a staging tree originated from the local engine. #289/#290 still require a satisfactory authorization design; recorded roots alone do not establish that authority. The latest GitHub release remains v0.2.0; a passing main is not proof that the released package contains these changes.

    #306 separately tracks the newly reported analogous benchmark output-path pattern outside #264's scope. We have not treated that report as independently verified or resolved. Please keep security-sensitive reproductions private under SECURITY.md. No release date is promised by this update.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions