refactor(release): break the nvattest proof-host import cycle
Audit follow-up to 52d03fd07, 63f871c94, and 4d95e0d45. Two findings were real;
three were assessed and rejected with reasons recorded below.
Binding nvattest into the proof-host channel created an import cycle:
release_nvattest_proof imported TARGET_POLICY from release_proof_host, and
release_proof_host then needed nvattest validators back, which was papered over
with two function-local lazy imports. TARGET_POLICY and TARGET_ENV_KEYS now live
in scripts/release_target_policy.py, a neutral module both import at top level.
The lazy imports are gone, every import site was updated directly, and nothing
is re-exported. release_nvattest_proof's import line changed; its internals did
not.
Also removed two dead locals in the candidate driver: proof_paths, which was
populated and silenced with an underscore assignment, and the unused return of
the target-proof lane call, whose receipts are written to the output paths
passed in and validated later by the shared report path.
Rejected findings, for the record:
- Transparency receipt validators are looser than the driver's inventory gate,
but that path is unreachable: transparency obtains its CandidateReport from
run_recover, which validates the evidence inventory exactly in both
directions, and CandidateReport has no other producer. Adding a redundant
guard would contradict the trust-internal-code invariant.
- The target names in docs/CHANNEL_ADAPTERS.md are pre-existing config-schema
examples naming lane keys operators must type literally, not drift-prone
prose.
- The AGENTS.md inventory-exactness statement is accurate, because the driver
enforces it.
Canonical make ci passes at 15894 passed, 17 skipped. The earlier per-slice runs
used make test-only, which runs neither the formatter check nor ruff; both are
clean now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(convey): make the notification icon a Lucide name
The notification icon contract is now a Lucide icon name, not an emoji. Emitters in JS, Python, and sol notify help send names; the renderer resolves names to vouched SVG and never renders the caller's value as markup. A legacy glyph-to-name table keeps already-persisted localStorage['solstone:notification_history'] entries rendering correctly; that is a migration path for external data, not a backwards-compatibility shim.
The honest price is that this route deliberately manufactures window.ConveyIcons.svg('name') literals inside the notifications object whose only consumer is the guard test's allow_list <= used direction. It trades a one-line regex change for a hand-maintained duplicate list that must stay in sync with CONVEY_ICON_NAMES, and it narrows the notification namespace from 36 available names to 13. It is the right call here because the safety property - a caller can never inject markup through icon - is worth it. It is not parity with L3's genuinely-free zero-mechanism route, and a later session must not read it as precedent for always manufacturing literals.
D5 and D6 were fixed, not deferred. ERROR_ICON in SurfaceState was a hand-rolled inline triangle-alert SVG; it now uses window.ConveyIcons.svg('triangle-alert'). _updateCard never refreshed the icon even though show() updates existing.icon for keyed notifications - invisible while both were the same emoji, but a stale icon under resolved SVG. It now refreshes from the resolver.
The browser Notification icon member was dropped because it is specified as a URL; the old emoji was parsed as a relative URL and fetched, which always failed. Title, body, and tag still deliver.
feat(sandbox): machine-facing journal sandbox-profile lifecycle
Add the journal-host command journal sandbox-profile with describe, prepare, apply, status, and disable subcommands, registered with surface="service".\n\nKeep the lifecycle journal-local only: it composes existing services/ and backup/ owner functions without calling portal, broker, relay-retirement, or production-storage paths. Validate the read-only .solstone-sandbox.json marker before any journal side effect, and persist run-owned non-secret intent at health/sandbox-profile/intent.json with an L2 write-owner row.\n\nDefine the contract version 1 envelope with closed vocabularies and ok=0 / degraded=1 / error=2 / cleanup_failed=3 exit mapping. Read handoffs from stdin only, cap them at 64 KiB, validate them strictly, and keep secrets out of argv, stdout, stderr, logs, and exception text.\n\nFollow through on registry generation by bumping EXPECTED_SERVICE_COMMANDS_COUNT from 42 to 43 and regenerating the native journal-host command inventory.\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(openapi): add observer client contract bundle
Add a deterministic committed bundle for separately released native clients: an OpenAPI projection of the 8 observer-client operations, generated fixtures, typed behavioral vectors, consumer audit data, and a digest manifest.
Split the bundle authorities across generation, recording, verification, export, and compatibility modules. The fd-based no-follow traversal, renameat2(RENAME_NOREPLACE) export path, strict SemVer handling, and fail-closed git history probing are deliberate race-safety and release-safety properties.
feat(convey): tear down the Jinja shell; guard against its return
The SPA conversion (L1–L6) flipped all 22 owner apps to the static
shell (convey/static/shell.html). This capstone removes the now-dead
Jinja container and makes shell-serving unconditional:
- Delete the unrendered Jinja shell: templates/{app.html, menu_bar,
date_nav, status_pane, diagnostic_console, chat_bar}.html. Only the
static, construct-free init.html remains under convey/templates/.
- Retire the transitional `spa` app flag end-to-end: drop the App
dataclass field, its parse, all 22 app.json entries, and the
/api/shell payload key. The injected index route now always serves
shell.html and the workspace fragment route serves unconditionally;
workspace_url is non-null for every app.
- Remove the orphaned App.get_workspace_template (get_background_template
is retained and unchanged).
- Add an AST-based CI guard to scripts/check_api_conventions.py: flag any
flask render_template(...) call under solstone/apps + solstone/convey
(excluding tests), resolved by import binding so the local
provider_readiness._render_template helper is not matched. The two PDF
routes (news, reflections) are the only allowlisted call sites. A
construct-free guard asserts convey/templates/ carries no Jinja markers.
Executable negative controls prove both guards fire.
No app's rendered output changes — this is dead-code removal plus an
anti-regression guard. Docs (CONVEY-FRONTEND.md, APPS.md, CONVEY.md)
updated to retire the flag and note the guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(cogitate): one approved journal-command vocabulary
Define {identity, health, talent} once in cogitate_contract.py and render it
into the policy, the prompt lint, the inventory scan, the runtime preamble,
both model-visible tool descriptions, and the sol-tool hint. The preamble no
longer bans bare `journal ...` while policy allows three families; it now
states the positive rule — normal access is `sol` / `sol call`, the three
approved host families run directly as `journal <family> ...` and must not be
prefixed with `sol call`.
Refuse the two forms seen in the stuck production run before execution, each
with the working repair: `sol call journal <family> ...` for any of the three
families, and the bare `journal search` / `journal facet` retries.
feat(packaging): ship the speakers analyze helper as its own platform wheel
Add the solstone-core-speakers-analyze package leaf, runtime staging script, helper platform declarations, workspace/rendering wiring, dependency-policy target coverage, and Makefile build lanes for the standalone helper wheel.
The wheel tags are derived from measured artifact requirements: manylinux_2_27_x86_64, manylinux_2_27_aarch64, and macosx_14_0_arm64. Prep measured GLIBC_2.27 on both the bundled CPU ONNX Runtime library and the zig-built helper binary.
A host GNU cargo build was measured at GLIBC_2.34 during prep; this was observed, not anticipated. That measurement is why the Linux helper lanes are pinned to zig-GNU targets: a host build could stamp manylinux_2_27 on a binary that cannot run on that floor.
The binary resolves ONNX Runtime through an $ORIGIN-relative RUNPATH to the library shipped inside its own wheel, with no system path, Python environment, or runtime environment variable fallback.
ONNX Runtime bytes come from the digest-pinned CPU onnxruntime wheels in uv.lock. The staging path rejects GPU provider members so the local onnxruntime-gpu build cannot be shipped by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(sol): remove Python sol skills implementation
Relocate _discover_project_sources and _ROUTER_SKILL_NAMES into solstone/think/doctor.py rather than sharing them, because doctor is still Python and must not shell out to the native binary. They are knowingly duplicated with core/crates/solstone-core-sol/src/skills.rs, and that duplication ends when doctor itself is ported.
Exactly one assertion is knowingly dropped for a reason other than the build verb removal: the duplicate-router-name ValueError branch from the original skills_cli.discover_project_sources. It is unreachable now that the required payload names are a declared constant of two distinct literals.
The build verb is removed from the CLI surface as a deliberate, operator-approved change. Its sys.path.insert plus from scripts import build_skill_references scaffolding is deleted outright, not ported. make skills invokes scripts/build_skill_references.py directly as of the preceding lode.
Delete scripts/capture_skills_parity_oracle.py in this same commit. The harness drives solstone.think.skills_cli directly: it subprocesses python -m solstone.think.skills_cli and monkeypatches skills_cli.get_project_root and skills_cli.resources.files. Once that module is gone, the script cannot run at all; it would be dead code, not merely stale. Its provenance role is fully preserved: the harness and the 30 vectors it produced are committed together at 5240cfaaf, strictly before this deletion, so git log over core/fixtures/native-sol/skills-parity-v1/ still proves the vectors were captured from the live Python oracle. The frozen vectors themselves remain in the tree and continue to gate the native implementation.
Leave solstone/think/setup.py deliberately untouched. Its skills_user_command() and skills_journal_command() build python -m solstone.think.sol_cli skills install ..., but skills is in JOURNAL_ACCESS_ONLY_COMMANDS, so sol_cli rejects it with exit 2 and journal setup reports both skills_user and skills_journal as failures. That defect predates this lode and is left for a follow-up; tests/test_setup.py and tests/test_setup_jsonl.py pin the current argv shape and pass unmodified.
Co-Authored-By: OpenAI Codex <codex@openai.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(convey): generate native-client OpenAPI 3.1 contract with staleness + breaking guards
Add code-adjacent opt-in fragments for link, observer, and push, assembled by a journal-free generator into docs/openapi/convey-clients.json with 14 operations across 13 paths. The explicit three-module fragment list keeps R0 dashboard and internal routes structurally out of the contract.
Define the shared {error, reason_code, detail} Error schema with reason_code enum generated from solstone/convey/reasons.py, plus per-operation x-reason-codes as the breaking-governed referenced set distinct from the staleness-governed global enum.
Model the Callosum SSE baseline as {tract, event, ts, ...open} and render the non-exhaustive tract/event registry from one CALLOSUM_REGISTRY constant into both the artifact and a generated docs/CONVEY.md block, while fixing the stale SSE route/auth prose.
Cover segments protocol-version oneOf(envelope|bare-array) and document callosumStream operation-level versus in-stream SSE error-frame reason-code sets, including auth_key_invalid as pre-stream-only.
Wire three checks: staleness via build_openapi_contract.py --check, breaking tripwire via check_openapi_contract.py, and conformance coverage in tests/test_openapi_contract.py. check-openapi runs in install-checks/make ci, and each failure path prints its exact regenerate or re-pin command.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(supervisor): harden provider runtime ownership
Write a provider's durable ownership record before publishing its port file, and fail closed when that ownership write fails. The child is cleaned, no port is published, and startup does not mark the provider ready. Previously a port file could point at a live process with no ownership record, leaving later cleanup unable to prove generation ownership.
Route every launch and stop cleanup path through the preserving cleanup wrapper. If termination raises, the managed handle is adopted by the cleanup-failed reconciler instead of being dropped and orphaning the child.
Restore Parakeet install bootstrap reachability after the Callosum start commands were removed. Missing artifacts now trigger bootstrap from reconciliation only when the desired-state table allows acquisition, with install and launch still communicating through durable Lode B fact changes.
Also preserve the Parakeet STT admission latch across ready and probe writes, move local context-window and Parakeet placement writes behind the generation fence, tighten the provider start-command gate for from solstone.think import callosum, rename the sweepable provider proctitle set, and delete stale helpers.
feat(release): validate and prove the speakers analyze helper wheel
Add helper wheel content validation, release artifact enumeration, Linux helper lane validation, macOS build-host collection, native-record handling, install-smoke proof wiring, and unit/integration coverage for the standalone speakers analyze wheel.
--auditwheel skip preserves the helper wheel's data/lib layout so the $ORIGIN RUNPATH resolves the bundled ONNX Runtime library from its installed location. It also removes maturin's own floor validation, making the measured floor-vs-tag check load-bearing rather than belt-and-braces.
Helper platform tags are imported from solstone/think/probe.py and are never redeclared locally in the wheel-content gate or release artifact inventory.
The core wheel guard remains inverted from the helper: it asserts solstone-core stays static, with no PT_INTERP, no DT_NEEDED, and tags still sourced from SOLSTONE_CORE_PLATFORM_TAGS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(release): document the nvattest candidate, recovery, and publication rail
Describe the behavior that shipped in 52d03fd07 and 63f871c94, and apply the
repository formatter to the code those commits touched.
AGENTS.md section 5 now states that every candidate generates a fresh challenge
and requires a per-target receipt pair, that authority is extracted from the
candidate's own root wheels, that recovery re-derives that authority from
retained wheel bytes and reads no release lock, that publication refuses before
any external seam when retained authority does not bind to the current
checkout, and that the retained evidence directories are inventory-exact in both
directions. It also records why lock agreement is enforced at cut time: the
retained declarations are the lock-derived declarations, so a retained candidate
keeps validating across an unrelated dependency bump.
The prose deliberately describes rules rather than enumerating target names or
support distribution names, so it cannot drift as those sets change. The
immutable create-only reuse rule and the boundary to VPE-owned live SPP
acceptance, real remote lanes, and production URL reach are stated explicitly.
scripts/release.sh header and usage describe the receipt pair and the recovery
guarantees. docs/CHANNEL_ADAPTERS.md documents the request/response growth, the
support and authority staging with the same copy-verify and directory-identity
guards as candidate wheels, the two response descriptors, and the naming
distinction between the renamed seam run_target_proofs and the unchanged
install primitive release_install_smoke.run_install_proof.
Formatting: the per-slice focused runs used make test-only, which does not run
the format check that make test performs, so five files reached the earlier
commits unformatted. Canonical make ci now passes at 15894 passed, 17 skipped.
AGENTS.md is edited directly; CLAUDE.md and GEMINI.md remain symlinks to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(doctor): split into universal `sol doctor` + journal-host `journal doctor`
Reframe diagnostics so each surface owns the right question:
- `sol doctor` (surface "universal", valid from both `sol` and `journal`)
runs only universal CLI-usability checks — python sanity, sol_importable,
local_bin_sol_reachable, and the `sol`-wrapper stale-alias check — and is
clean on a box with no local journal.
- new `journal doctor` runs journal-host health: disk, config/journal-dir
writability, service identity + running/crash-loop state, journal_sync,
the `journal`-wrapper stale-alias check, launchd plist, and feature
advisories. Folder/service checks skip cleanly (never false-fail) off a
journal host; severities per design (config-invalid/identity-mismatch/
crash-loop/systemd-failed/sync-conflict = fail, stopped/socket-absent =
warn, features = advisory).
Consolidate the probe infra that `preflight.py` had duplicated into a new
stdlib-only `solstone/think/probe.py` (Check/CheckResult/ProbeOutput/
run_probe/make_result + helpers + the shared check bodies); preflight and
doctor now compose from it. preflight behavior is byte-identical.
Repoint `journal setup` step-1 at a readiness battery via `doctor
--readiness` (universal + disk + journal-dir-writable + feature advisories,
no runtime service/sync checks). STEP_NAMES and the doctor JSONL event/
status/summary contract are unchanged for the macOS installer parser.
Split the per-binary stale-alias check + legacy uv-tool/pipx auto-migration
so each surface migrates only its own wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(preflight): add stdlib-only make-driven install-readiness battery
Add `make preflight`, a stdlib-only subset of `sol doctor`'s pre-install
checks (python_version, uv_installed, venv_consistent, local_bin_sol_reachable,
disk_space, config_dir_readable) that runs before `.venv` exists and even when
`uv` is absent. It is a deliberate, temporary duplication of doctor.py's check
subset — doctor.py imports third-party deps (timefhuman) at module top, so it
cannot run pre-`.venv`; a later lode unifies the two onto shared stdlib code.
- New `solstone/think/preflight.py` (pure stdlib) + `scripts/preflight.py` shim.
- Makefile uv-guard is now MAKECMDGOALS-filtered so `preflight`/`install` no
longer abort at parse time when uv is absent; uv-requiring goals still do.
- `make install` runs `$(MAKE) preflight` as the first `.installed` step,
fail-fast before `uv sync`.
- `tests/test_preflight.py` covers pass, blocker-fail, stdlib-only-import
(under an __import__ guard that also proves doctor still fails), and
install-runs-preflight paths.
`sol doctor` (solstone/think/doctor.py) is left behaviorally identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(release): validate and prove the speakers analyze helper wheel
Add helper wheel content validation, release artifact enumeration, Linux helper lane validation, macOS build-host collection, native-record handling, install-smoke proof wiring, and unit/integration coverage for the standalone speakers analyze wheel.
--auditwheel skip preserves the helper wheel's data/lib layout so the $ORIGIN RUNPATH resolves the bundled ONNX Runtime library from its installed location. It also removes maturin's own floor validation, making the measured floor-vs-tag check load-bearing rather than belt-and-braces.
Helper platform tags are imported from solstone/think/probe.py and are never redeclared locally in the wheel-content gate or release artifact inventory.
The core wheel guard remains inverted from the helper: it asserts solstone-core stays static, with no PT_INTERP, no DT_NEEDED, and tags still sourced from SOLSTONE_CORE_PLATFORM_TAGS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(release): document the nvattest candidate, recovery, and publication rail
Describe the behavior that shipped in 52d03fd07 and 63f871c94, and apply the
repository formatter to the code those commits touched.
AGENTS.md section 5 now states that every candidate generates a fresh challenge
and requires a per-target receipt pair, that authority is extracted from the
candidate's own root wheels, that recovery re-derives that authority from
retained wheel bytes and reads no release lock, that publication refuses before
any external seam when retained authority does not bind to the current
checkout, and that the retained evidence directories are inventory-exact in both
directions. It also records why lock agreement is enforced at cut time: the
retained declarations are the lock-derived declarations, so a retained candidate
keeps validating across an unrelated dependency bump.
The prose deliberately describes rules rather than enumerating target names or
support distribution names, so it cannot drift as those sets change. The
immutable create-only reuse rule and the boundary to VPE-owned live SPP
acceptance, real remote lanes, and production URL reach are stated explicitly.
scripts/release.sh header and usage describe the receipt pair and the recovery
guarantees. docs/CHANNEL_ADAPTERS.md documents the request/response growth, the
support and authority staging with the same copy-verify and directory-identity
guards as candidate wheels, the two response descriptors, and the naming
distinction between the renamed seam run_target_proofs and the unchanged
install primitive release_install_smoke.run_install_proof.
Formatting: the per-slice focused runs used make test-only, which does not run
the format check that make test performs, so five files reached the earlier
commits unformatted. Canonical make ci now passes at 15894 passed, 17 skipped.
AGENTS.md is edited directly; CLAUDE.md and GEMINI.md remain symlinks to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(release): non-destructive, mirror-bound, signed-packet make audit
Replaces the GitHub-DB-refreshing make audit with a stable, non-destructive audit: verifies a minisign-signed freshness receipt against pinned sol trust (key ID + public-key SHA-256), materializes the RustSec advisory DB only from a locally-supplied git bundle at the receipt's signed commit, and runs the pinned cargo-deny advisory check fully offline against that isolated DB, emitting exactly one JSON success witness.
Four explicit operator inputs (AUDIT_ADVISORY_BUNDLE/RECEIPT/PUBKEY/LOCATOR); signature is derived as the adjacent <receipt>.minisig; locator is used only as cargo-deny's db-urls identity and never contacted; no GitHub fallback.
New authority module scripts/advisory_mirror_audit.py reuses the release-advisory isolation, minisign, and redaction primitives via import; the release-candidate prepare_policy_run path, PolicyRun/ledger schemas, and the 24h/14d rules are unchanged (regression-covered).
Recipe keeps the cargo-deny + minisign preflights, redirects preflight stdout to stderr, and suppresses command echo so make audit stdout is exactly the witness JSON and the private locator is never printed.
Adds a deterministic 113-test suite (fake tools/packets, real-git bundle materialization), rewrites the audit recipe contract test, and reconciles the now-false release_advisory_policy docstring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(packaging): derive helper wheel tags instead of restating them
Audit found the helper's platform tags hardcoded in four release paths
rather than imported from solstone/think/probe.py, where criterion 5
requires them to be declared exactly once.
The drift this prevents is concrete: if the measured floor moves and
probe.py is updated, a literal selector keeps matching the OLD filename.
The release path then silently selects nothing, or the wrong artifact,
and proves nothing while reporting success -- the same silent-wrong-answer
shape the wheel's own tag discipline exists to prevent.
Product selectors now derive from SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS:
release_install_smoke.py, release_build_host.py, release_candidate_driver.py,
and the Makefile's macOS helper lane. The Makefile derives via a $(shell)
import, which works precisely because probe.py is stdlib-only and needs no
venv. Pre-existing root/core literals are left alone; they predate this lode.
Left as literals deliberately, because they are not tag declarations:
maturin `--compatibility manylinux_2_27` build arguments and the lane
validator's expected token tuples, which are build inputs rather than
selectors; and the upstream `onnxruntime-1.25.0-...whl` filenames in the
staging script's digest-pinned provenance table, which identify exact
third-party bytes.
The constants test no longer pins tag literals. Comparing a tag against
the string the code just produced passes whether or not the tag is honest.
It now asserts the properties that can actually fail: that the dict cannot
drift from its derivation function, that the Linux helper tags are NOT
equal to the core's (copying them is the specific defect this coverage set
exists to prevent, since the core is static musl with no glibc floor), and
that the parsed floor is at least the measured 2.27. Both mutations were
verified to fail the test before landing.
Fixture writers and their selectors derive together, so neither can drift
from the other.
make ci: 15539 passed, 17 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(release): break the nvattest proof-host import cycle
Audit follow-up to 52d03fd07, 63f871c94, and 4d95e0d45. Two findings were real;
three were assessed and rejected with reasons recorded below.
Binding nvattest into the proof-host channel created an import cycle:
release_nvattest_proof imported TARGET_POLICY from release_proof_host, and
release_proof_host then needed nvattest validators back, which was papered over
with two function-local lazy imports. TARGET_POLICY and TARGET_ENV_KEYS now live
in scripts/release_target_policy.py, a neutral module both import at top level.
The lazy imports are gone, every import site was updated directly, and nothing
is re-exported. release_nvattest_proof's import line changed; its internals did
not.
Also removed two dead locals in the candidate driver: proof_paths, which was
populated and silenced with an underscore assignment, and the unused return of
the target-proof lane call, whose receipts are written to the output paths
passed in and validated later by the shared report path.
Rejected findings, for the record:
- Transparency receipt validators are looser than the driver's inventory gate,
but that path is unreachable: transparency obtains its CandidateReport from
run_recover, which validates the evidence inventory exactly in both
directions, and CandidateReport has no other producer. Adding a redundant
guard would contradict the trust-internal-code invariant.
- The target names in docs/CHANNEL_ADAPTERS.md are pre-existing config-schema
examples naming lane keys operators must type literally, not drift-prone
prose.
- The AGENTS.md inventory-exactness statement is accurate, because the driver
enforces it.
Canonical make ci passes at 15894 passed, 17 skipped. The earlier per-slice runs
used make test-only, which runs neither the formatter check nor ruff; both are
clean now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(release): require challenge-bound nvattest proofs in candidate evidence
Wire the landed nvattest native-proof primitive into the candidate rail. Every
fresh candidate now generates a 64-hex challenge, extracts nvattest authority
from its own root wheels, materializes a lock-verified eight-wheel support set,
and requires one challenge-bound nvattest receipt per native target alongside
the existing install receipt.
What a candidate now requires before promotion:
- a fresh challenge per attempt, so two attempts over identical version, source,
and payload inputs differ only in the challenge and the receipts bound to it
- authority extracted from the candidate's own root wheels, proven byte-identical
across all three targets, digested into the ledger, and passed to every lane
- the eight locked support wheels materialized into retained evidence and
verified against uv.lock before the ledger is written and before any host call
- one valid nvattest receipt per target in a sibling nvattest/ directory, so the
existing install-proof gates stay exact and unweakened
Evidence inventory grows from {ledger.json, proofs} to
{ledger.json, proofs, nvattest, support}, each gate exact in both directions.
The ledger gains one top-level nvattest key carrying the challenge, the
authority payload, its byte digest, and the locked support declarations. The
target list is not restated there; proofs.expected_targets stays the single
source of truth for both receipt kinds.
The proof-host seam is renamed run_target_proofs and returns both receipt paths
from a single host round-trip. release_install_smoke.run_install_proof, the
install prover the adapter harness invokes, is deliberately unchanged.
bundle_digest now binds the nvattest receipt digests. This intentionally changes
a reported candidate identity for future retained candidates; without it the
bundle digest would not cover the new evidence.
Lock verification is deliberately candidate-side only. Recovery anchors support
verification in retained bytes and the retained ledger's declarations, which are
themselves the lock-derived declarations captured at cut time. Anchoring
recovery in the checkout lock would make a retained candidate stop validating
after any support dependency bump, and would block publishing valid retained
bytes because publish_release gates on _verify_recover.
Real remote lanes, production URL reach, journal publication, and live SPP
composite acceptance remain VPE post-ship work; no test contacts a host,
downloads a wheel, or runs a real build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(packaging): derive helper wheel tags instead of restating them
Audit found the helper's platform tags hardcoded in four release paths
rather than imported from solstone/think/probe.py, where criterion 5
requires them to be declared exactly once.
The drift this prevents is concrete: if the measured floor moves and
probe.py is updated, a literal selector keeps matching the OLD filename.
The release path then silently selects nothing, or the wrong artifact,
and proves nothing while reporting success -- the same silent-wrong-answer
shape the wheel's own tag discipline exists to prevent.
Product selectors now derive from SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS:
release_install_smoke.py, release_build_host.py, release_candidate_driver.py,
and the Makefile's macOS helper lane. The Makefile derives via a $(shell)
import, which works precisely because probe.py is stdlib-only and needs no
venv. Pre-existing root/core literals are left alone; they predate this lode.
Left as literals deliberately, because they are not tag declarations:
maturin `--compatibility manylinux_2_27` build arguments and the lane
validator's expected token tuples, which are build inputs rather than
selectors; and the upstream `onnxruntime-1.25.0-...whl` filenames in the
staging script's digest-pinned provenance table, which identify exact
third-party bytes.
The constants test no longer pins tag literals. Comparing a tag against
the string the code just produced passes whether or not the tag is honest.
It now asserts the properties that can actually fail: that the dict cannot
drift from its derivation function, that the Linux helper tags are NOT
equal to the core's (copying them is the specific defect this coverage set
exists to prevent, since the core is static musl with no glibc floor), and
that the parsed floor is at least the measured 2.27. Both mutations were
verified to fail the test before landing.
Fixture writers and their selectors derive together, so neither can drift
from the other.
make ci: 15539 passed, 17 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(release): require challenge-bound nvattest proofs in candidate evidence
Wire the landed nvattest native-proof primitive into the candidate rail. Every
fresh candidate now generates a 64-hex challenge, extracts nvattest authority
from its own root wheels, materializes a lock-verified eight-wheel support set,
and requires one challenge-bound nvattest receipt per native target alongside
the existing install receipt.
What a candidate now requires before promotion:
- a fresh challenge per attempt, so two attempts over identical version, source,
and payload inputs differ only in the challenge and the receipts bound to it
- authority extracted from the candidate's own root wheels, proven byte-identical
across all three targets, digested into the ledger, and passed to every lane
- the eight locked support wheels materialized into retained evidence and
verified against uv.lock before the ledger is written and before any host call
- one valid nvattest receipt per target in a sibling nvattest/ directory, so the
existing install-proof gates stay exact and unweakened
Evidence inventory grows from {ledger.json, proofs} to
{ledger.json, proofs, nvattest, support}, each gate exact in both directions.
The ledger gains one top-level nvattest key carrying the challenge, the
authority payload, its byte digest, and the locked support declarations. The
target list is not restated there; proofs.expected_targets stays the single
source of truth for both receipt kinds.
The proof-host seam is renamed run_target_proofs and returns both receipt paths
from a single host round-trip. release_install_smoke.run_install_proof, the
install prover the adapter harness invokes, is deliberately unchanged.
bundle_digest now binds the nvattest receipt digests. This intentionally changes
a reported candidate identity for future retained candidates; without it the
bundle digest would not cover the new evidence.
Lock verification is deliberately candidate-side only. Recovery anchors support
verification in retained bytes and the retained ledger's declarations, which are
themselves the lock-derived declarations captured at cut time. Anchoring
recovery in the checkout lock would make a retained candidate stop validating
after any support dependency bump, and would block publishing valid retained
bytes because publish_release gates on _verify_recover.
Real remote lanes, production URL reach, journal publication, and live SPP
composite acceptance remain VPE post-ship work; no test contacts a host,
downloads a wheel, or runs a real build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(release): break the nvattest proof-host import cycle
Audit follow-up to 52d03fd07, 63f871c94, and 4d95e0d45. Two findings were real;
three were assessed and rejected with reasons recorded below.
Binding nvattest into the proof-host channel created an import cycle:
release_nvattest_proof imported TARGET_POLICY from release_proof_host, and
release_proof_host then needed nvattest validators back, which was papered over
with two function-local lazy imports. TARGET_POLICY and TARGET_ENV_KEYS now live
in scripts/release_target_policy.py, a neutral module both import at top level.
The lazy imports are gone, every import site was updated directly, and nothing
is re-exported. release_nvattest_proof's import line changed; its internals did
not.
Also removed two dead locals in the candidate driver: proof_paths, which was
populated and silenced with an underscore assignment, and the unused return of
the target-proof lane call, whose receipts are written to the output paths
passed in and validated later by the shared report path.
Rejected findings, for the record:
- Transparency receipt validators are looser than the driver's inventory gate,
but that path is unreachable: transparency obtains its CandidateReport from
run_recover, which validates the evidence inventory exactly in both
directions, and CandidateReport has no other producer. Adding a redundant
guard would contradict the trust-internal-code invariant.
- The target names in docs/CHANNEL_ADAPTERS.md are pre-existing config-schema
examples naming lane keys operators must type literally, not drift-prone
prose.
- The AGENTS.md inventory-exactness statement is accurate, because the driver
enforces it.
Canonical make ci passes at 15894 passed, 17 skipped. The earlier per-slice runs
used make test-only, which runs neither the formatter check nor ruff; both are
clean now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(release): require challenge-bound nvattest proofs in candidate evidence
Wire the landed nvattest native-proof primitive into the candidate rail. Every
fresh candidate now generates a 64-hex challenge, extracts nvattest authority
from its own root wheels, materializes a lock-verified eight-wheel support set,
and requires one challenge-bound nvattest receipt per native target alongside
the existing install receipt.
What a candidate now requires before promotion:
- a fresh challenge per attempt, so two attempts over identical version, source,
and payload inputs differ only in the challenge and the receipts bound to it
- authority extracted from the candidate's own root wheels, proven byte-identical
across all three targets, digested into the ledger, and passed to every lane
- the eight locked support wheels materialized into retained evidence and
verified against uv.lock before the ledger is written and before any host call
- one valid nvattest receipt per target in a sibling nvattest/ directory, so the
existing install-proof gates stay exact and unweakened
Evidence inventory grows from {ledger.json, proofs} to
{ledger.json, proofs, nvattest, support}, each gate exact in both directions.
The ledger gains one top-level nvattest key carrying the challenge, the
authority payload, its byte digest, and the locked support declarations. The
target list is not restated there; proofs.expected_targets stays the single
source of truth for both receipt kinds.
The proof-host seam is renamed run_target_proofs and returns both receipt paths
from a single host round-trip. release_install_smoke.run_install_proof, the
install prover the adapter harness invokes, is deliberately unchanged.
bundle_digest now binds the nvattest receipt digests. This intentionally changes
a reported candidate identity for future retained candidates; without it the
bundle digest would not cover the new evidence.
Lock verification is deliberately candidate-side only. Recovery anchors support
verification in retained bytes and the retained ledger's declarations, which are
themselves the lock-derived declarations captured at cut time. Anchoring
recovery in the checkout lock would make a retained candidate stop validating
after any support dependency bump, and would block publishing valid retained
bytes because publish_release gates on _verify_recover.
Real remote lanes, production URL reach, journal publication, and live SPP
composite acceptance remain VPE post-ship work; no test contacts a host,
downloads a wheel, or runs a real build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(release): break the nvattest proof-host import cycle
Audit follow-up to 52d03fd07, 63f871c94, and 4d95e0d45. Two findings were real;
three were assessed and rejected with reasons recorded below.
Binding nvattest into the proof-host channel created an import cycle:
release_nvattest_proof imported TARGET_POLICY from release_proof_host, and
release_proof_host then needed nvattest validators back, which was papered over
with two function-local lazy imports. TARGET_POLICY and TARGET_ENV_KEYS now live
in scripts/release_target_policy.py, a neutral module both import at top level.
The lazy imports are gone, every import site was updated directly, and nothing
is re-exported. release_nvattest_proof's import line changed; its internals did
not.
Also removed two dead locals in the candidate driver: proof_paths, which was
populated and silenced with an underscore assignment, and the unused return of
the target-proof lane call, whose receipts are written to the output paths
passed in and validated later by the shared report path.
Rejected findings, for the record:
- Transparency receipt validators are looser than the driver's inventory gate,
but that path is unreachable: transparency obtains its CandidateReport from
run_recover, which validates the evidence inventory exactly in both
directions, and CandidateReport has no other producer. Adding a redundant
guard would contradict the trust-internal-code invariant.
- The target names in docs/CHANNEL_ADAPTERS.md are pre-existing config-schema
examples naming lane keys operators must type literally, not drift-prone
prose.
- The AGENTS.md inventory-exactness statement is accurate, because the driver
enforces it.
Canonical make ci passes at 15894 passed, 17 skipped. The earlier per-slice runs
used make test-only, which runs neither the formatter check nor ruff; both are
clean now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(release): document the nvattest candidate, recovery, and publication rail
Describe the behavior that shipped in 52d03fd07 and 63f871c94, and apply the
repository formatter to the code those commits touched.
AGENTS.md section 5 now states that every candidate generates a fresh challenge
and requires a per-target receipt pair, that authority is extracted from the
candidate's own root wheels, that recovery re-derives that authority from
retained wheel bytes and reads no release lock, that publication refuses before
any external seam when retained authority does not bind to the current
checkout, and that the retained evidence directories are inventory-exact in both
directions. It also records why lock agreement is enforced at cut time: the
retained declarations are the lock-derived declarations, so a retained candidate
keeps validating across an unrelated dependency bump.
The prose deliberately describes rules rather than enumerating target names or
support distribution names, so it cannot drift as those sets change. The
immutable create-only reuse rule and the boundary to VPE-owned live SPP
acceptance, real remote lanes, and production URL reach are stated explicitly.
scripts/release.sh header and usage describe the receipt pair and the recovery
guarantees. docs/CHANNEL_ADAPTERS.md documents the request/response growth, the
support and authority staging with the same copy-verify and directory-identity
guards as candidate wheels, the two response descriptors, and the naming
distinction between the renamed seam run_target_proofs and the unchanged
install primitive release_install_smoke.run_install_proof.
Formatting: the per-slice focused runs used make test-only, which does not run
the format check that make test performs, so five files reached the earlier
commits unformatted. Canonical make ci now passes at 15894 passed, 17 skipped.
AGENTS.md is edited directly; CLAUDE.md and GEMINI.md remain symlinks to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(release): break the nvattest proof-host import cycle
Audit follow-up to 52d03fd07, 63f871c94, and 4d95e0d45. Two findings were real;
three were assessed and rejected with reasons recorded below.
Binding nvattest into the proof-host channel created an import cycle:
release_nvattest_proof imported TARGET_POLICY from release_proof_host, and
release_proof_host then needed nvattest validators back, which was papered over
with two function-local lazy imports. TARGET_POLICY and TARGET_ENV_KEYS now live
in scripts/release_target_policy.py, a neutral module both import at top level.
The lazy imports are gone, every import site was updated directly, and nothing
is re-exported. release_nvattest_proof's import line changed; its internals did
not.
Also removed two dead locals in the candidate driver: proof_paths, which was
populated and silenced with an underscore assignment, and the unused return of
the target-proof lane call, whose receipts are written to the output paths
passed in and validated later by the shared report path.
Rejected findings, for the record:
- Transparency receipt validators are looser than the driver's inventory gate,
but that path is unreachable: transparency obtains its CandidateReport from
run_recover, which validates the evidence inventory exactly in both
directions, and CandidateReport has no other producer. Adding a redundant
guard would contradict the trust-internal-code invariant.
- The target names in docs/CHANNEL_ADAPTERS.md are pre-existing config-schema
examples naming lane keys operators must type literally, not drift-prone
prose.
- The AGENTS.md inventory-exactness statement is accurate, because the driver
enforces it.
Canonical make ci passes at 15894 passed, 17 skipped. The earlier per-slice runs
used make test-only, which runs neither the formatter check nor ruff; both are
clean now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(packaging): ship the speakers analyze helper as its own platform wheel
Add the solstone-core-speakers-analyze package leaf, runtime staging script, helper platform declarations, workspace/rendering wiring, dependency-policy target coverage, and Makefile build lanes for the standalone helper wheel.
The wheel tags are derived from measured artifact requirements: manylinux_2_27_x86_64, manylinux_2_27_aarch64, and macosx_14_0_arm64. Prep measured GLIBC_2.27 on both the bundled CPU ONNX Runtime library and the zig-built helper binary.
A host GNU cargo build was measured at GLIBC_2.34 during prep; this was observed, not anticipated. That measurement is why the Linux helper lanes are pinned to zig-GNU targets: a host build could stamp manylinux_2_27 on a binary that cannot run on that floor.
The binary resolves ONNX Runtime through an $ORIGIN-relative RUNPATH to the library shipped inside its own wheel, with no system path, Python environment, or runtime environment variable fallback.
ONNX Runtime bytes come from the digest-pinned CPU onnxruntime wheels in uv.lock. The staging path rejects GPU provider members so the local onnxruntime-gpu build cannot be shipped by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(packaging): ship the speakers analyze helper as its own platform wheel
Add the solstone-core-speakers-analyze package leaf, runtime staging script, helper platform declarations, workspace/rendering wiring, dependency-policy target coverage, and Makefile build lanes for the standalone helper wheel.
The wheel tags are derived from measured artifact requirements: manylinux_2_27_x86_64, manylinux_2_27_aarch64, and macosx_14_0_arm64. Prep measured GLIBC_2.27 on both the bundled CPU ONNX Runtime library and the zig-built helper binary.
A host GNU cargo build was measured at GLIBC_2.34 during prep; this was observed, not anticipated. That measurement is why the Linux helper lanes are pinned to zig-GNU targets: a host build could stamp manylinux_2_27 on a binary that cannot run on that floor.
The binary resolves ONNX Runtime through an $ORIGIN-relative RUNPATH to the library shipped inside its own wheel, with no system path, Python environment, or runtime environment variable fallback.
ONNX Runtime bytes come from the digest-pinned CPU onnxruntime wheels in uv.lock. The staging path rejects GPU provider members so the local onnxruntime-gpu build cannot be shipped by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(release): bind retained nvattest evidence in recovery, transparency, publication
Complete the nvattest rail binding started in 52d03fd07. Recovery now re-derives
the authority anchor from retained bytes, transparency carries both receipt
classes as public evidence, and publication refuses before any external seam
when the retained candidate does not bind to the current checkout.
What recovery now re-derives rather than reads:
- authority extracted from each target's retained root wheel, proven
byte-identical across targets, compared against the ledger payload, its byte
digest, and every receipt's installed_authority
- retained support-wheel bytes re-hashed against the ledger declarations and
against every receipt's support_distributions
- the challenge, bound from ledger to every receipt
- each receipt's target plus version, source commit, candidate digest, ledger
sha256, and core lock sha256
_validate_retained_nvattest_binding is reached from the shared report path, so
candidate and recovery run one implementation rather than two. It takes no
checkout root, so it cannot read the release lock even by accident: recovery is
byte-only, network-free, and writes nothing on success or failure.
A self-consistent forgery that rewrites the ledger nvattest block and every
receipt to agree with each other still fails, because authority is anchored in
the retained candidate wheel bytes rather than in the ledger. That case is
covered by an explicit test asserting the specific authority failure, alongside
missing, extra, duplicate, swapped, stale, replayed, mutated, wrong-challenge,
and noncanonical receipt cases, retained support-wheel byte mutation, and ledger
declaration mutation.
What publication now refuses: any retained candidate whose nvattest authority
does not bind to the current checkout's canonical authority. The check sits
between recovery verification and the production branch, so production and test
mode both short-circuit with zero index, upload, tag, and witness calls. Tests
assert those call counts are zero rather than merely that an error was raised.
Transparency publishes retained nvattest receipts as public objects named
{target}.nvattest.json, mapped from their on-disk sibling directory in exactly
one place. The install-receipt glob is unchanged and still scopes to install
receipts only.
Real remote lanes, production URL reach, journal publication, and live SPP
composite acceptance remain VPE post-ship work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>