feat: prepare fable-session 0.3.0b1 public beta#1
Conversation
Budget pass-through: optional registry key max_budget_usd, validated as a finite positive plain-decimal TOML number before command construction; when set, exactly one --max-budget-usd argument is emitted, printed in the redacted summary, and recorded as structural manifest metadata (truthful null when absent). 0.2 registries without the key stay valid. Command-length/privacy repair: prompt payloads no longer ride the tmux command line. The runner writes run-scoped 0600 payload files (brief.payload/task.payload) from its single scanned read via the held reservation FD (new state.ReservedRun.write_private_file); the capture wrapper verifies each payload's sha256 and delivers the brief via exactly-one placeholder substitution and the task via Claude's stdin (-p with no inline prompt). Fixes the verified 7,745-byte-task + 8,072-byte-brief `tmux: command too long` launch failure (regression test bounds the tmux argv). Dry runs leave no payload material; hashes, read-once bytes, redaction, and fail-closed state semantics preserved. Privacy-safe usage reporting: bounded modelUsage aggregates (input/output/ cache-read/cache-creation tokens, web-search requests, total_cost_usd) across models in audit --format json, audit text, and terminal watch events when structurally available. Missing usage is an explicit null; malformed usage is null plus integrity reason malformed_model_usage and never falsely proves purity. No prompt/result/tool text in outputs, events, or state. Release mechanics: version 0.3.0b1 (prerelease tag v0.3.0-beta.1); the three deprecated 0.1 console aliases are removed after their one migration release (old-manifest reading still works); SPDX MIT metadata with license-files, README long description, honest classifiers, and setuptools>=77; README install section (pipx/uv tool/isolated pip, prerequisites, Linux + Claude Code 2.1.208 as the tested combination), updated privacy contract, release notes, and contribution guidance; PUBLIC_RELEASE_CHECKLIST records the reviewed clean root commit, resolves the export blocker, keeps the full-history gate, and corrects the private-to-public order (vulnerability reporting is enabled and verified immediately AFTER going public); new deterministic full-history readiness scan reusing the tracked-tree policy; CI pinned to verified action SHAs and extended (full history, wheelhouse-seeded offline smoke, build, wheel install smoke, whitespace check); new tag-triggered attested prerelease workflow (SHA256SUMS, GitHub artifact attestation, prerelease-only, PyPI out of scope); offline smoke gains FABLE_SESSION_WHEELHOUSE and a PEP-639-capable backend floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pinned tooling, truthful payload state Five approved follow-ups on the reviewed public-beta candidate: - ci.yml: top-level least-privilege `permissions: contents: read` (no job widens it), enforced by a new YAML-parsing test. - release.yml: fail-closed tag/package-version consistency gate (tests/release_tag_check.py) runs before every gate/build/publish step; only v0.3.0-beta.1 may release 0.3.0b1, any unmapped version shape refuses outright. Step ordering is asserted on the parsed step list, not raw text (an intro comment mentioning SHA256SUMS previously shadowed the real gate position). - README: pre-PyPI install wording — every install command is tag-pinned GitHub source, unqualified index installs are banned by test, and the warning (with a contiguous "tag exists" availability claim) precedes every command. - CI/release Python tooling pinned to tested versions (PyYAML 6.0.2, build 1.5.1, setuptools 83.0.0, wheel 0.47.0; --no-isolation build). The pin test inspects only executable workflow lines, so comments documenting the pyproject `setuptools>=77` floor are not findings. - runner: truthful backward-compatible `payload_files_created` manifest field — False for every dry-run/pending/payload-failure record, True only after BOTH run-scoped payload files exist; old manifests without the key stay fully readable. Credential scanner stays fail-closed and unrelaxed; the two tracked- tree false positives were fixed at the source (a `token:`-shaped CI comment reworded; a `token` variable in the pin helper renamed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Confidence Score: 4/5
Summary
Well-executed beta-hardening PR. Verified independently in a sandbox: all 506 unit tests pass on the PR head, compileall is clean, the tracked-tree and full-history readiness scans pass (3 commits / 75 blobs), and the tag gate accepts only v0.3.0-beta.1. All five workflow action SHA pins were confirmed to match their claimed v4/v5/v3/v2 tags via the GitHub API. The payload redesign (0600 O_EXCL/O_NOFOLLOW files written through a held directory FD, sha256 verification in the capture wrapper, task via stdin, fail-closed exit codes) is sound and honestly documented, budget validation is strict and pre-argv, and usage reporting preserves evidence-integrity precedence. Remaining findings are minor: an avoidable verify-then-reread gap for the brief payload, a subtle invented-zero cost in usage aggregation, and hardcoded version strings in CI. No blockers to merge.
Important Files Changed
| File | Overview |
|---|---|
| .github/workflows/ci.yml | Least-privilege permissions (contents: read), actions pinned to verified tag SHAs, full-depth checkout for history scan, new history/whitespace/offline-smoke/build gates, wheel-based CLI smoke asserting removed aliases. Hardcodes 0.3.0b1 in a grep; on: push + pull_request double-runs PR branches. |
| .github/workflows/release.yml | New tag-triggered prerelease workflow: fail-closed tag/version gate first, re-runs all release gates, pinned build toolchain, SHA256SUMS, GitHub artifact attestation, always-prerelease gh-release. Pins verified; permissions scoped to what release needs. No PyPI publication, matching the stated scope. |
| PUBLIC_RELEASE_CHECKLIST.md | Checklist advanced truthfully: clean-root export/review recorded as done, visibility flip and post-flip vulnerability-reporting enable kept as unchecked blockers, tag/attestation sequence documented. |
| README.md | Adds install docs with a clear pre-PyPI warning against unqualified index installs, documents payload-file delivery and the remaining brief-in-argv same-UID visibility, max_budget_usd semantics, usage reporting rules, alias removal, and release notes. Accurate against the code. |
| SECURITY.md | Supported-versions line updated to the 0.3.x line. Fine. |
| pyproject.toml | 0.3.0b1, PEP 639 SPDX license + license-files, explicit empty dependencies, honest Linux-only classifier, 0.1 console aliases removed, setuptools>=77 floor. Consistent with CI's pinned toolchain. |
| src/fable_session/init.py | Version bump to 0.3.0b1 with tag-mapping comment. |
| src/fable_session/capture.py | Adds hash-verified payload delivery: O_NOFOLLOW regular-file opens, sha256 checks before any side effect, brief substituted for a strictly-counted placeholder, task attached as stdin, new EXIT_PAYLOAD, FD hygiene on all paths. One nit: the brief is hashed then re-read (see finding). |
| src/fable_session/config.py | Optional max_budget_usd with strict validation (finite, positive, non-bool, plain-decimal round-trip) enforced before any command construction; 0.2 registries without the key stay valid. Solid. |
| src/fable_session/jsonl.py | Bounded modelUsage aggregation with strict type checks; malformed usage is a sticky integrity failure that forfeits PURE; missing usage is explicit null. One nit: total_cost_usd can report an invented 0.0 (see finding). |
| src/fable_session/model_audit.py | Legacy alias removed; text output renders usage or an explicit unavailability line. Clean. |
| src/fable_session/runner.py | Command is payload-free by construction; budget flag emitted exactly once from the canonical string; payload files written from the once-read text before tmux with fail-closed error recording; payload_files_created flips to true only after both writes succeed. Ordering and failure paths are truthful. |
| src/fable_session/state.py | New write_private_file: allowlisted single-component name, O_EXCL|O_NOFOLLOW 0600 create via the held run-dir FD, full write + fsync, fail-closed StateError. Correct. |
| src/fable_session/watchdog.py | Legacy alias removed; terminal events carry the bounded usage aggregate only when structurally available. Consistent with jsonl semantics. |
| tests/full_history_readiness_check.py | New full-history scan reusing the tracked-tree policy verbatim; dedupes blobs, skips binary. Passed here on the PR head. |
| tests/offline_install_smoke.py | Wheelhouse-seeded setuptools>=77 path with no silent online fallback; asserts SPDX metadata, version, alias removal. Hardcodes 0.3.0b1 (same maintenance nit as CI). |
| tests/public_readiness_check.py | Policy updated for 0.3.0b1: SPDX license form, empty-dependencies pin, alias-removal enforcement, checklist state transitions, and a narrow credential-regex exemption for workflow permission grants (write|read|none only, word-bounded — acceptable). |
| tests/release_tag_check.py | Fail-closed beta-only tag mapping; refuses every unmapped version shape. Verified: accepts v0.3.0-beta.1, rejects v0.3.0. |
| tests/test_capture.py | New coverage for payload parsing, placeholder discipline, hash mismatch, symlink/non-regular refusal, stdin delivery, and the runner/capture placeholder-constant pin. |
| tests/test_config.py | Budget validation coverage: bools, negatives, zero, inf/nan, exponent forms, canonical formatting. |
| tests/test_full_history_readiness.py | Unit contract for the history scanner including dirty-old-blob detection in a synthetic repo. |
| tests/test_jsonl_audit.py | Usage aggregation coverage: bounded totals, malformed-usage integrity precedence, multiple-results ambiguity. |
| tests/test_migration_fable_session.py | Updated for alias removal and 0.3.0b1 while pinning that 0.1 manifests and 0.2 registries stay readable. |
| tests/test_mit_release.py | Packaging truth checks updated: SPDX form, readme, classifiers, single console script. |
| tests/test_model_audit_cli.py | Audit CLI usage-output coverage and alias-removal assertions. |
| tests/test_public_readiness.py | Extended contract tests for the updated policy including the permission-grant regex exemption staying narrow. |
| tests/test_release_tag_gate.py | Thorough negative-space coverage of the tag mapping (post/dev/rc/whitespace/non-canonical forms all refused). |
| tests/test_runner.py | Large new coverage: payload-free argv, budget flag emission, payload write failure recording, payload_files_created truthfulness across dry-run/pending/failed/launched records. |
| tests/test_skeleton.py | Version pin bump. |
| tests/test_state.py | write_private_file coverage: name allowlist, exclusive create, symlink refusal, 0600 mode. |
| tests/test_watchdog.py | Terminal-event usage summary coverage under the availability rule. |
| options["--brief-payload"], options["--brief-sha256"], "brief" | ||
| ) | ||
| try: | ||
| brief_bytes = b"" |
There was a problem hiding this comment.
The brief payload is read twice: _open_verified_payload hashes the file, then main re-reads the FD for the bytes actually delivered. A same-UID writer modifying the inode between the two passes gets unverified bytes substituted into the Claude argv despite the hash check. This gap is avoidable at zero cost: read the bytes once, hash that in-memory buffer against --brief-sha256, and deliver exactly those bytes (the task-payload stdin FD cannot get the same treatment, but the brief can). The module docstring acknowledges the window generally, yet the single-pass fix would close it entirely for the brief.
| or not math.isfinite(value) or value < 0: | ||
| return None, True | ||
| cost += value | ||
| totals["total_cost_usd"] = cost |
There was a problem hiding this comment.
total_cost_usd is initialized to 0.0 and reported even when no modelUsage entry carries a costUSD field (or modelUsage is an empty dict), so a present-but-costless modelUsage yields an invented $0.00 — in tension with the README/docstring claim that zeroes are never invented. Consider tracking whether any costUSD was seen and emitting null (or omitting the key) when none was, mirroring the treatment of missing modelUsage.
| python3 -m pip install --quiet . | ||
| python3 -m pip install --quiet dist/*.whl | ||
| fable-session --version | ||
| fable-session --version | grep -F "0.3.0b1" |
There was a problem hiding this comment.
The expected version '0.3.0b1' is hardcoded here (and twice in tests/offline_install_smoke.py), so every release bump requires touching CI and the smoke in lockstep or the pipeline goes red on an otherwise-correct bump. Consider deriving the expected version from pyproject.toml (e.g. python3 -c "import tomllib; ...") so the single source of truth is the file release_tag_check.py already gates on. Separately, 'on: push + pull_request' double-runs CI for PR branches pushed to this repo; restricting push to main (or tags) would halve CI noise without losing coverage.
Summary
0.3.0b1and remove the deprecated 0.1 console aliases;max_budget_usdpass-through;Security and compatibility
Verification
python3.12 -m compileall -q src tests--max-budget-usd 0.5; actual cost$0.154APPROVE_TO_PUSHSHIP_BETA_FIRSTRelease sequence
After CI passes and this PR is merged: flip the repository public, immediately enable private vulnerability reporting, apply branch protection, then create tag
v0.3.0-beta.1. The tag workflow must attest artifacts and create a GitHub prerelease. PyPI remains out of scope.