diff --git a/README.md b/README.md index e0cc21c..46d6bc7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ -# HRA +# HRA v0 -[![HRA](https://hra.sh/opengraph-image)](https://hra.sh) +[![HRA](https://hra-weld.vercel.app/opengraph-image)](https://hra-weld.vercel.app) -**A metaharness for Codex.** HRA turns the Codex accounts you already use into +> [!IMPORTANT] +> This repository preserves HRA v0 and its final v0.1.14 prerelease. The +> current HRA is at [hra.sh](https://hra.sh) with source at +> [github.com/hraness/hra](https://github.com/hraness/hra). + +**The original metaharness for Codex.** HRA v0 turns the Codex accounts you already use into one durable system for planning work, delegating it, running it in parallel, and bringing it back for review. -[Download for macOS](https://hra.sh/download) · [Website](https://hra.sh) · -[Compare HRA](https://hra.sh/alternatives) · [Open HRA](https://hra.sh/app) +[Download for macOS](https://hra-weld.vercel.app/download) · [Release history](https://hra-weld.vercel.app/releases) · [Website](https://hra-weld.vercel.app) · +[Historical comparisons](https://hra-weld.vercel.app/alternatives) · [Current HRA](https://hra.sh) -> HRA 0.1.14 build 15 is a source release candidate for Apple Silicon Macs. -> Direct downloads remain disabled until its exact source commit, annotated -> tag, runtime tree, manifest, checksum, and artifact hashes are published. The -> candidate uses an ad-hoc code seal; it is not Developer ID signed or notarized. +> HRA v0.1.14 build 15 is the final archived prerelease for Apple Silicon Macs. +> Its exact source commit, annotated tag, runtime tree, manifest, checksum, and +> artifact hashes are published. The app uses an ad-hoc code seal; it is not +> Developer ID signed or notarized. ## Why HRA exists @@ -49,7 +54,7 @@ worktree manager may be a better fit when model choice or workspace isolation is the main problem, and a remote client may be better when the main job is checking an agent from your phone. -The [comparison pages](https://hra.sh/alternatives) explain those tradeoffs +The [comparison pages](https://hra-weld.vercel.app/alternatives) explain those tradeoffs using current first-party sources, including Codex app, OpenCode Desktop, Paseo, Conductor, Superset, OpenChamber, and Happy Coder. @@ -77,12 +82,18 @@ and data boundary. ## Install the prerelease The native app targets Apple Silicon and macOS 13 or newer. The -[download page](https://hra.sh/download) exposes no draft asset while the -checked release contract is a candidate. After publication, download the DMG -and checksum from that page, verify the SHA-256, and follow the -unknown-developer instructions. You can build the candidate source locally. +[download page](https://hra-weld.vercel.app/download) exposes the immutable +v0.1.14 build 15 prerelease from the HRA v0 archive. Download the DMG and +checksum there, verify the SHA-256, and follow the unknown-developer +instructions. The tagged candidate source and complete corresponding-source +archives remain attached to that release. + +The [release history](https://hra-weld.vercel.app/releases) records every tag +from v0.1.7 through v0.1.14, all seven immutable GitHub prereleases, and every +asset's exact byte count, SHA-256 digest, and archive download link. v0.1.11 is +preserved as a tag-only candidate. -## Develop HRA +## Develop HRA v0 Repository development uses Bun 1.3.14 and Node.js 24. Native work additionally requires Zig 0.16.0, Xcode Command Line Tools, and an Apple Silicon Mac. @@ -124,7 +135,9 @@ corresponding-source artifacts are documented in the ## Project and license -HRA is under active development. It is an independent project and is not +HRA v0 is archived. Security reports and narrowly scoped archival corrections +remain welcome, but feature development continues in the separate +[current HRA repository](https://github.com/hraness/hra). HRA v0 is an independent project and is not affiliated with, endorsed by, or sponsored by OpenAI. “OpenAI” and “Codex” are used only to identify the product HRA interoperates with. diff --git a/SECURITY.md b/SECURITY.md index 069dc4a..f074297 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,9 @@ -# Security policy +# HRA v0 security policy ## Report a vulnerability Report suspected vulnerabilities through [GitHub private vulnerability -reporting](https://github.com/hraness/hra/security/advisories/new). Do not put +reporting](https://github.com/hraness/hra-v0/security/advisories/new). Do not put an exploit, secret, personal data, or other sensitive detail in a public issue or discussion. @@ -18,9 +18,11 @@ vulnerability details in that issue. ## Supported versions -Security fixes target the current `main` branch. When a published release is -affected, its advisory states which release versions receive a fix. Older -commits and unreleased development snapshots are not supported separately. +HRA v0 is archived at v0.1.14. Maintainers assess reports against the archived +`main` branch and state any available mitigation in the advisory. No new +feature release or automatic update channel is promised. Reports about the +current HRA belong in the separate +[current repository](https://github.com/hraness/hra). ## Disclosure diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 330ea88..716c07c 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,6 +1,6 @@ -# Third-party notices +# HRA v0 third-party notices -HRA includes and builds on third-party software and assets. Their license terms +The archived HRA v0 source and final prerelease include and build on third-party software and assets. Their license terms apply to those components. This summary is informational and does not replace the license and notice files distributed with each component. @@ -56,7 +56,7 @@ and resolved commits are recorded in `package.json` and `bun.lock`. ## PostHog browser SDK -The public `hra.sh` site bundles `posthog-js` 1.412.1 under its declared Apache +The public `hra-weld.vercel.app` site bundles `posthog-js` 1.412.1 under its declared Apache License 2.0 and MIT terms. Copyright 2020 Posthog / Hiberly, Inc., and copyright 2015 Mixpanel, Inc. The upstream package retains its complete `LICENSE`; the exact version is recorded in `package.json` and `bun.lock`. diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 69034aa..3be68ab 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -1,7 +1,7 @@ # Contents - `desktop/` – the macOS Native SDK shell, Codex account runtime, and local session dashboard. -- `web/` – the HRA Next.js task control plane, authoritative Convex backend, and `hra.sh` Vercel root. +- `web/` – the archived HRA v0 Next.js task control plane, authoritative Convex backend, and `hra-weld.vercel.app` fallback root. - `cli/` – the non-interactive `taskctl` client used by humans and agents. # Guidelines diff --git a/apps/cli/package.json b/apps/cli/package.json index 9d0d3d1..e6facd8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,12 +6,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "apps/cli" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "bin": { diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 0a0b33a..f3a1f7f 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -1,6 +1,12 @@ -# HRA for macOS +# HRA v0 for macOS -HRA is a local-first macOS interface for long-running, parallel Codex work. Panes are repository-bound chats that can run independently. Settings manages local Codex subscriptions. HRA keeps the pane grid unavailable until at least one subscription is signed in. +This directory preserves the archived HRA v0 macOS application and its final +v0.1.14 prerelease. The current HRA is at [hra.sh](https://hra.sh) in the +[current repository](https://github.com/hraness/hra). HRA v0 is a local-first +interface for long-running, parallel Codex work. Panes are repository-bound +chats that can run independently. Settings manages local Codex subscriptions. +HRA keeps the pane grid unavailable until at least one subscription is signed +in. HRA does not require a separate HRA account for local use. It routes each prompt to a bounded model, reasoning, and service-tier profile, then exposes that read-only decision with the pane's current activity and latest assistant response. The gateway admits work only to an eligible signed-in subscription. A provider usage limit stops the affected work; HRA does not move that work to another subscription or use multiple subscriptions to circumvent provider limits. @@ -147,7 +153,7 @@ Turn off every scheduled chat before creating or restoring a portable backup. A HRA keeps the bundle identifier `kitchen.hraness` and the state root `~/Library/Application Support/OPRTE` as compatibility custody. The supported handoff changes the visible application authority from `/Applications/OPRTE.app` to `/Applications/HRA.app`; it does not rename or copy live state into a new product root. -Run the handoff only after Suite Accounts v0.3.0 is the deployed account authority, the Accounts registry recognizes `hraness:hra:production:v1`, and `release-download.json` contains published v0.1.14 build 15 evidence. The command verifies that published source, the candidate bundle, full state and Keychain continuity, both installed bundle archives, updater quiescence, and ordered AppKit shutdown before committing HRA as the sole visible application: +Run the handoff only after Suite Accounts v0.3.0 is the deployed account authority, the Accounts registry recognizes `hraness:hra:production:v1`, and `release-download.json` contains published v0.1.14 build 15 evidence. The command verifies that published source, the selected installation bundle, full state and Keychain continuity, both installed bundle archives, updater quiescence, and ordered AppKit shutdown before committing HRA as the sole visible application: ```sh bun run installation:handoff \ @@ -158,13 +164,13 @@ bun run installation:handoff \ An existing `/Applications/HRA.app` is accepted only when it is the exact v0.1.7 build 8, immutable v0.1.8 build 9, immutable v0.1.9 build 10, immutable v0.1.10 build 11, immutable v0.1.12 build 13, or current published v0.1.13 build 14 release. The handoff archives that prior app and can restore it through the bounded rollback path. -The exact OPRTE v0.1.4 build 5 predecessor uses the historical self-managed OPRTE Preview certificate chain, which current macOS can report as untrusted even when its signature bytes are unchanged. The handoff accepts that trust result only for the frozen predecessor tree, designated requirement, CodeDirectory, detached CMS signature, certificate fingerprints and chain, signed CMS time within the exact encoded leaf-certificate validity interval, root metadata, and extended-attribute inventory. Preview signing requested no secure timestamp. The pinned executable hash freezes the exact CMS bytes, and the verifier pins the signed time instead of making receipt-bound rollback depend on the current wall clock. The v0.1.14 candidate, every supported prior HRA, and every other bundle remain subject to strict system code-signature verification. The receipt preserves the selected policy so archive, restart, rollback, and staging deletion checks cannot infer the exception from a bundle name or error message. +The exact OPRTE v0.1.4 build 5 predecessor uses the historical self-managed OPRTE Preview certificate chain, which current macOS can report as untrusted even when its signature bytes are unchanged. The handoff accepts that trust result only for the frozen predecessor tree, designated requirement, CodeDirectory, detached CMS signature, certificate fingerprints and chain, signed CMS time within the exact encoded leaf-certificate validity interval, root metadata, and extended-attribute inventory. Preview signing requested no secure timestamp. The pinned executable hash freezes the exact CMS bytes, and the verifier pins the signed time instead of making receipt-bound rollback depend on the current wall clock. The published v0.1.14 bundle, every supported prior HRA, and every other bundle remain subject to strict system code-signature verification. The receipt preserves the selected policy so archive, restart, rollback, and staging deletion checks cannot infer the exception from a bundle name or error message. The retired annotated `v0.1.11` tag object `e4c171e33e414d74a36791fc8577cbfbcef8e52e` points directly to candidate commit `5a2a9842cacc75fee42ab8e23ca8c215a643e21e`. GitHub has no release or assets for that tag. HRA v0.1.11 build 12 is tagged-only historical evidence, never installed-app or rollback authority, and the tag must not be moved or reused. HRA v0.1.12 build 13 remains published prior installed-app and rollback authority. Its direct annotated tag object `626be494d24733d12e53d09932cb5cc6218bc2fe` points to candidate commit `9ab991d08d1507fd73c9e7ef5fb4a37baee9c014`, and immutable GitHub prerelease `374867227` carries its exact seven assets. Publication commit `bfb60415c3eea7bc1021db8e8cc92f3e95800a46` records the release evidence. Its source `installation:handoff` must not initiate cutover because it self-detects the descriptor intentionally retained by the handoff process. -The current published predecessor is HRA v0.1.13 build 14. Its direct annotated tag object `44f00fd5c5e00bc8dcded0c9b176a8e37ada90f3` points to candidate commit `9ba06a441c9b12b448cfe34784432592dbeccb19`, and immutable GitHub prerelease `374920071` carries its exact seven assets. Publication commit `7825cb231890aa971f965412c31dfa2cb7796561` records the release evidence. It remains valid prior installed-app and rollback authority. Its source `installation:handoff` must not initiate cutover because it copies the candidate to a path ending in `.bundle`, which its strict packaged-app verifier rejects before installation. HRA v0.1.14 is the first corrected operator. +The immediate published predecessor is HRA v0.1.13 build 14. Its direct annotated tag object `44f00fd5c5e00bc8dcded0c9b176a8e37ada90f3` points to candidate commit `9ba06a441c9b12b448cfe34784432592dbeccb19`, and immutable GitHub prerelease `374920071` carries its exact seven assets. Publication commit `7825cb231890aa971f965412c31dfa2cb7796561` records the release evidence. It remains valid prior installed-app and rollback authority. Its source `installation:handoff` must not initiate cutover because it copies the candidate to a path ending in `.bundle`, which its strict packaged-app verifier rejects before installation. HRA v0.1.14 is the first corrected operator. The backup directory must not exist. The operation creates it with user-only permissions and writes durable phase receipts. It rejects symbolic-link, case, Unicode-normalization, inode, process-birth, AppKit launch-identity, open-file, updater, receipt, custody, bundle, and Git-provenance ambiguity. A pre-commit interruption restores both original applications. An interruption after the committed receipt preserves HRA authority. Resume only its bounded, idempotent staging cleanup with the committed receipt: @@ -234,72 +240,73 @@ zig-out/release/macos/arm64/ The Bun archive is a deterministic complete-source bundle containing its pinned native build inputs, nested Git sources, Node headers, and locked `lol-html` Cargo closure. Patched WebKit and JavaScriptCore remain in their own archive because it is close to GitHub's 2 GiB asset limit. The Git and Dugite Native archives close the bundled Git source boundary. Full packaging requires network access and a clean source tree. CI uses `package:macos:adhoc` to verify the same compiler, runtime, and license pins, app, DMG, and checksum without downloading the large source archives. -The root `release-download.json` file is the strict download and publication contract. HRA 0.1.14 build 15 remains `candidate` while its source and artifact evidence are unknown. In that state every commit, tag-object, runtime-tree, byte-count, and SHA-256 field is `null`, and the website exposes no direct asset URL. The candidate contract does not change v0.1.13's status as the current published predecessor. - -Publication uses two commits so no Git commit must contain its own object ID. The clean candidate commit C retains the null candidate contract; packaging, the annotated `v0.1.14` tag, manifest, app, DMG, and checksum all name C. After those immutable values exist, publication commit P changes only `release-download.json` to the complete `published` evidence for C. P must have C as its only direct parent. The verifier rejects another changed path, another parent, a skipped or follow-up commit, a candidate-contract drift, a tag not peeled to C, or an artifact/app that does not embed C and its runtime-tree digest. The installation handoff runs only from clean P. - -The immutable published HRA v0.1.8 build 9, v0.1.9 build 10, v0.1.10 build 11, v0.1.12 build 13, and v0.1.13 build 14 prereleases remain historical evidence. Do not replace their tags, releases, or assets. HRA v0.1.13 is the latest published predecessor and remains accepted as prior installed authority. The retired tagged-only v0.1.11 build 12 candidate also remains immutable, but it is not a published release and must never be accepted as prior installed authority. Do not reuse any of those versions or builds for the new candidate. - -`bun run check:release-source` is the artifact-independent gate used by CI and desktop builds. Candidate C validates the null contract. Published P additionally requires the full clean C-to-P Git transition and direct annotated tag. Vercel's shallow checkout uses a separate provider binding documented in the web runbook; it cannot substitute for the full Required CI gate. - -Release provenance comes from the checkout, never a caller-supplied commit. Packaging and candidate verification require the canonical repository top-level with a real `.git` directory, a clean tree, no submodules, alternates, grafts, replacement refs, shallow history, or included local Git configuration. They reject inherited `GIT_*` variables, run `/usr/bin/git` with explicit Git and work-tree paths, and disable global and system configuration. Use a primary standalone checkout rather than a linked worktree for release work. - -Verify the checked contract or a complete local candidate with: +The root `release-download.json` is the maintained archive contract for the +published HRA v0.1.14 build 15 prerelease. It names +`https://github.com/hraness/hra-v0` and records the fixed source commit, tag +object, runtime-tree digest, artifact byte counts, and SHA-256 digests used by +the download page. + +The root `release-history.json` is the checked compatibility ledger for tags +v0.1.7 through v0.1.14. It records each annotated tag object and peeled commit, +the seven GitHub release IDs, all 49 asset IDs, byte counts, and SHA-256 +digests, and the v0.1.11 tag-only state. The public `/releases` page and remote +gate consume this one ledger. + +The immutable publication history remains separate from that maintained +repository coordinate: + +- Candidate C is `7b39c459827b2acf45aa2d911c94fdb5d4f37860`. +- Publication P is `6221f79b745f154882080936b961ff431569f33e`. +- Annotated tag object `37ed37afb39cacfd6a51044cf7f3c1b873571aa3` + points directly to C. + +C contains the historical candidate contract. P is C's sole child and changes +only `release-download.json` from that candidate state to the complete +published evidence. Both commits record the repository's name at publication, +`https://github.com/hraness/hra`. The GitHub repository rename moves the same +tag, immutable release, and seven assets to `hraness/hra-v0`; it does not +recreate or republish them. + +The archive surface performs one reviewed navigation migration in +`release-download.json`: only the repository coordinate changes from +`hraness/hra` to `hraness/hra-v0`. Every release, source, and artifact value, +and every other contract byte, remains equal to P. The source gate proves the +exact C-to-P transition and tag, requires the checked archive surface to +descend from P, and requires exactly one single-parent contract change with +that exact historical-to-archive byte replacement. Another path at P, another +parent, another contract edit, a rewrite and restore, tag drift, or artifact +drift fails closed. + +Run the archive checks from a clean standalone checkout: ```sh bun run --cwd apps/desktop check:release-contract -bun run --cwd apps/desktop verify:release-candidate -``` - -The candidate command verifies clean C, an optional collision-free annotated tag, packaged DMG, checksum, manifest commit, and runtime tree. It emits the exact evidence for P. `verify:published-release` then verifies the strict C-to-P transition and the same local assets. Historical tags and releases are immutable inputs; creating a new candidate never rewrites v0.1.7 through v0.1.13. - -Publish v0.1.14 from the root of the clean standalone C checkout only after the -full package and candidate verifier pass. Create and push a new direct -annotated tag for C, then create the non-draft prerelease with the exact seven -package outputs. Do not use `--clobber`, a glob, or an existing release: - -```sh -git tag -a v0.1.14 -m "HRA v0.1.14" HEAD -git push origin refs/tags/v0.1.14 -gh release create v0.1.14 \ - apps/desktop/zig-out/release/macos/arm64/HRA-0.1.14-15-macos-arm64.dmg \ - apps/desktop/zig-out/release/macos/arm64/HRA-0.1.14-15-macos-arm64.dmg.sha256 \ - apps/desktop/zig-out/release/macos/arm64/HRA-0.1.14-15-release-manifest.json \ - apps/desktop/zig-out/release/macos/arm64/bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz \ - apps/desktop/zig-out/release/macos/arm64/bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz \ - apps/desktop/zig-out/release/macos/arm64/git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz \ - apps/desktop/zig-out/release/macos/arm64/dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz \ - --repo hraness/hra \ - --verify-tag \ - --prerelease \ - --latest=false \ - --title "HRA v0.1.14" \ - --notes-from-tag -``` - -GitHub creates the release as a draft while it uploads and publishes it only -after every upload succeeds. Repository release immutability must then report -the release immutable. Fill the working-tree `release-download.json` with the -candidate verifier's exact commit, direct tag-object, runtime-tree, byte-count, -and SHA-256 evidence, without changing another file, and run: - -```sh +bun run check:release-source bun run verify:remote-release ``` -The remote gate requires one immutable, non-draft v0.1.14 prerelease with the -exact seven assets in `uploaded` state. It checks every name, byte count, -canonical download URL, and GitHub SHA-256 digest against the publication -contract and the downloaded release manifest. It also downloads and hashes the -checksum and manifest, binds the checksum to the DMG, and binds all four -corresponding-source archives to the manifest. It does not redownload the -multi-gigabyte immutable assets. A mutable, incomplete, additional, or -different remote release fails before P is committed. - -Commit P with only `release-download.json` changed. From clean P run -`check:release-source`, `verify:published-release`, and -`verify:remote-release`; Required CI repeats the source and remote gates. The -web runbook controls the later Vercel allowlist and exact-P redeployment. +Release provenance comes from the checkout, never a caller-supplied commit. +The gate requires the canonical repository top-level with a real `.git` +directory, a clean tree, no submodules, alternates, grafts, replacement refs, +shallow history, or included local Git configuration. It rejects inherited +`GIT_*` variables, runs `/usr/bin/git` with explicit Git and work-tree paths, +and disables global and system configuration. + +The remote gate reads the renamed `hraness/hra-v0` repository. It checks the +exact eight-tag set and annotated tag objects, their peeled commits, the exact +seven-release and 49-asset set, upload state, canonical URLs, byte counts, and +GitHub SHA-256 digests. It requires v0.1.11 to remain tag-only. For v0.1.14 it +also verifies the checksum-to-DMG binding, release manifest, and all four +corresponding-source records. It does not redownload the multi-gigabyte +immutable assets. A moved tag, new or missing release, mutable release, or +asset difference fails closed. + +The immutable published HRA v0.1.7 build 8, v0.1.8 build 9, v0.1.9 build 10, +v0.1.10 build 11, v0.1.12 build 13, and v0.1.13 build 14 prereleases remain +historical evidence. +The retired tagged-only v0.1.11 build 12 candidate is not a published release +and must never be accepted as prior installed authority. Do not replace or +reuse any historical tag, release, version, build, or asset. The ad-hoc package proves bundle integrity but does not identify a registered Apple developer. macOS may require **Privacy & Security → Open Anyway** after download. It is not notarized, and automatic updates remain disabled. Developer ID signing, notarization, and publication require separately provisioned release credentials. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8342879..767e597 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -7,12 +7,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "apps/desktop" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "keywords": [ "agent-orchestration", diff --git a/apps/desktop/runtime/AGENTS.md b/apps/desktop/runtime/AGENTS.md index fa482cd..45f1392 100644 --- a/apps/desktop/runtime/AGENTS.md +++ b/apps/desktop/runtime/AGENTS.md @@ -16,7 +16,7 @@ - `test/image-normalizer.macos.test.ts` and `test/codex-signature-normalization.macos.test.ts` – macOS-only image-normalizer regressions and cross-host Codex signing-page determinism evidence. - `control-plane-maintenance.ts` – app-stopped health checks plus encrypted backup, inspection, verification, and restore. - `installation-handoff.ts`, `installation-path-authority.ts`, and `installation-process-authority.ts` – the fail-closed OPRTE-to-HRA application handoff, resumable committed cleanup, exact filesystem authority, ordered native-root shutdown, and unchanged-state rollback. -- `release-download-contract.ts`, `release-provenance.ts`, and `release-download.json` at the repository root – strict candidate/publication evidence and hermetic canonical Git provenance for downloadable releases. +- `release-download-contract.ts`, `release-history-contract.ts`, `release-provenance.ts`, and the root release manifests – immutable candidate/publication evidence, the exact v0.1.7–v0.1.14 remote compatibility ledger, the reviewed archive-repository coordinate migration, and hermetic canonical Git provenance for downloadable releases. - `reactive-baseline.ts` – the owned gateway, projection, SQLite, Direct bridge, React, containment, and cleanup baseline. - `THIRD_PARTY_NOTICES.md` and adjacent license texts – exact notices and provenance for pinned Bun, Codex, ripgrep, Git, Git LFS, Git Credential Manager, JavaScript, and asset runtimes. diff --git a/apps/desktop/runtime/release-download-contract.ts b/apps/desktop/runtime/release-download-contract.ts index 3abdb34..e4cb746 100644 --- a/apps/desktop/runtime/release-download-contract.ts +++ b/apps/desktop/runtime/release-download-contract.ts @@ -7,13 +7,22 @@ import { z } from "@hra-internal/schema"; import { correspondingSourceSpecs } from "./corresponding-sources"; import { macosPackage } from "./macos-package-config"; +import { + verifyRemoteReleaseHistory, + type RemoteReleaseHistoryEvidence, +} from "./release-history-contract"; import { HRA_CANONICAL_REPOSITORY, - inspectCanonicalReleasePublication, + HRA_HISTORICAL_PUBLICATION_REPOSITORY, + HRA_V0_CURRENT_REPOSITORY, + inspectArchiveReleaseSurface, + inspectCanonicalArchiveRelease, + inspectReleasePublicationAtCommit, inspectReleasePublicationTransition, inspectReleaseSourceRepository, inspectReleaseTag, - type CanonicalReleasePublicationEvidence, + type ArchiveReleaseSurfaceEvidence, + type CanonicalArchiveReleaseEvidence, type ReleasePublicationEvidence, type ReleaseRepositoryEvidence, type ReleaseTagEvidence, @@ -73,14 +82,13 @@ const publishedReleaseSchema = z.object({ tagObject: objectIdSchema, }).strict(), }).strict(); -const releaseDownloadContractSchema = z.object({ - release: z.discriminatedUnion("availability", [ - candidateReleaseSchema, - publishedReleaseSchema, - ]), - repository: z.literal(HRA_CANONICAL_REPOSITORY), - schemaVersion: z.literal(1), -}).strict().superRefine((contract, context) => { +function refineReleaseDownloadContract( + contract: Readonly<{ + release: z.infer + | z.infer; + }>, + context: z.RefinementCtx, +): void { const { release } = contract; const dmg = `HRA-${release.version}-${release.build}-macos-arm64.dmg`; if ( @@ -95,9 +103,30 @@ const releaseDownloadContractSchema = z.object({ message: "Release tag and artifact names must derive from version and build.", }); } -}); +} + +const releaseDownloadContractSchema = z.object({ + release: publishedReleaseSchema, + repository: z.literal(HRA_CANONICAL_REPOSITORY), + schemaVersion: z.literal(1), +}).strict().superRefine(refineReleaseDownloadContract); + +const historicalCandidateReleaseDownloadContractSchema = z.object({ + release: candidateReleaseSchema, + repository: z.literal(HRA_HISTORICAL_PUBLICATION_REPOSITORY), + schemaVersion: z.literal(1), +}).strict().superRefine(refineReleaseDownloadContract); + +const historicalPublishedReleaseDownloadContractSchema = z.object({ + release: publishedReleaseSchema, + repository: z.literal(HRA_HISTORICAL_PUBLICATION_REPOSITORY), + schemaVersion: z.literal(1), +}).strict().superRefine(refineReleaseDownloadContract); export type ReleaseDownloadContract = z.infer; +export type HistoricalCandidateReleaseDownloadContract = z.infer< + typeof historicalCandidateReleaseDownloadContractSchema +>; export type PublishedReleaseDownloadContract = Readonly<{ release: z.infer; repository: typeof HRA_CANONICAL_REPOSITORY; @@ -130,54 +159,55 @@ export interface PublishedReleaseSourceEvidence { readonly contract: PublishedReleaseDownloadContract; readonly publication: ReleasePublicationEvidence; readonly repository: ReleaseRepositoryEvidence; + readonly surface: ArchiveReleaseSurfaceEvidence; readonly tag: ReleaseTagEvidence; } -export type ReleaseSourceGateEvidence = - | Readonly<{ - availability: "candidate"; - contract: ReleaseDownloadContract; - status: "valid_candidate_contract"; - }> - | Readonly; +export type ReleaseSourceGateEvidence = Readonly< + PublishedReleaseSourceEvidence & { + availability: "published"; + status: "verified_published_source"; + } +>; export const releasePublicationCommitAllowlistEnvironmentVariable = "HRA_RELEASE_PUBLICATION_COMMIT_ALLOWLIST" as const; +export const releaseSurfaceCommitAllowlistEnvironmentVariable = + "HRA_V0_SURFACE_COMMIT_ALLOWLIST" as const; +export const HRA_V0_RELEASE_PUBLICATION_COMMIT = + "6221f79b745f154882080936b961ff431569f33e" as const; -export type VercelReleaseSourceGateEvidence = - | Extract - | Readonly<{ - availability: "published"; - contract: PublishedReleaseDownloadContract; - publicationCommit: string; - status: "verified_vercel_publication_binding"; - }>; +export type VercelReleaseSourceGateEvidence = Readonly<{ + availability: "published"; + contract: PublishedReleaseDownloadContract; + publicationCommit: string; + repositoryMigrationCommit: string; + status: "verified_vercel_archive_surface_binding"; + surfaceCommit: string; +}>; export type ReleaseHttpFetcher = ( url: string, init: RequestInit, ) => Promise; -export type RemoteReleaseGateEvidence = - | Readonly<{ - availability: "candidate"; - contract: ReleaseDownloadContract; - status: "candidate_has_no_remote_release"; - }> - | Readonly<{ - assets: LocalReleaseCandidateEvidence["artifacts"]; - availability: "published"; - contract: PublishedReleaseDownloadContract; - immutable: true; - releaseId: number; - status: "verified_immutable_remote_release"; - }>; +export type RemoteReleaseStateEvidence = Readonly<{ + assets: LocalReleaseCandidateEvidence["artifacts"]; + availability: "published"; + contract: PublishedReleaseDownloadContract; + immutable: true; + releaseId: number; + status: "verified_immutable_remote_release"; +}>; +export type RemoteReleaseGateEvidence = Readonly< + RemoteReleaseStateEvidence & { + history: RemoteReleaseHistoryEvidence; + } +>; type ReleaseSourceStateOptions = Readonly<{ environment?: Readonly>; + publicationCommit?: string; repositoryRoot?: string; }>; @@ -207,6 +237,12 @@ export function parseReleaseDownloadContract(value: unknown): ReleaseDownloadCon return releaseDownloadContractSchema.parse(value); } +export function parseHistoricalReleaseCandidateContract( + value: unknown, +): HistoricalCandidateReleaseDownloadContract { + return historicalCandidateReleaseDownloadContractSchema.parse(value); +} + export async function readReleaseDownloadContract(): Promise { return parseReleaseDownloadContract( JSON.parse(await readFile(releaseDownloadPath, "utf8")) as unknown, @@ -235,44 +271,38 @@ export async function verifyReleaseDownloadContract(): Promise { return await verifyReleaseSourceState(await verifyReleaseDownloadContract()); } /** - * Read back the immutable GitHub prerelease before a publication commit is - * authorized for Vercel. GitHub's immutable asset digest binds the large DMG; - * the small checksum and manifest are additionally downloaded and parsed. - * Candidate contracts perform no network requests. + * Read back the immutable GitHub prerelease from the maintained archive. + * GitHub's immutable asset digest binds the large DMG; the small checksum and + * manifest are additionally downloaded and parsed. */ export async function verifyRemoteReleaseGate( fetcher: ReleaseHttpFetcher = defaultReleaseFetcher, ): Promise { - return await verifyRemoteReleaseState( - await verifyReleaseDownloadContract(), - fetcher, - ); + const contract = await verifyReleaseDownloadContract(); + const [release, history] = await Promise.all([ + verifyRemoteReleaseState(contract, fetcher), + verifyRemoteReleaseHistory(fetcher), + ]); + return Object.freeze({ ...release, history }); } export async function verifyRemoteReleaseState( contract: ReleaseDownloadContract, fetcher: ReleaseHttpFetcher, -): Promise { - if (contract.release.availability === "candidate") { - return Object.freeze({ - availability: "candidate", - contract, - status: "candidate_has_no_remote_release", - }); - } +): Promise { const publishedContract = asPublishedContract(contract); const metadataUrl = - `https://api.github.com/repos/hraness/hra/releases/tags/${publishedContract.release.tag}`; + `https://api.github.com/repos/hraness/hra-v0/releases/tags/${publishedContract.release.tag}`; const metadataResponse = await fetcher(metadataUrl, { headers: githubJsonHeaders(), redirect: "error", @@ -386,10 +416,10 @@ export async function verifyRemoteReleaseState( } /** - * Vercel Git builds are intentionally shallow, so the provider consumes an - * exact publication-commit allowlist only after CI has verified the full - * C-to-P transition. The Vercel-owned Git identity binds that immutable commit - * to the canonical repository without trusting a shallow local .git directory. + * Vercel Git builds are intentionally shallow. The provider binds immutable P + * and one allowlisted archive surface, then fetches canonical history to prove + * C-to-P, the tag, and the one-field repository migration without trusting the + * ambient shallow .git directory. */ export async function verifyVercelReleaseSourceGate( environment: Readonly> = process.env, @@ -406,15 +436,17 @@ export async function verifyVercelReleaseSourceState( inspectCanonical: (options: Readonly<{ candidateCommit: string; publicationCommit: string; + surfaceCommit: string; tag: string; - }>) => Promise = - inspectCanonicalReleasePublication, + }>) => Promise = + inspectCanonicalArchiveRelease, ): Promise { + const publishedContract = asPublishedContract(contract); if ( environment.VERCEL !== "1" || environment.VERCEL_GIT_PROVIDER !== "github" || environment.VERCEL_GIT_REPO_OWNER !== "hraness" - || environment.VERCEL_GIT_REPO_SLUG !== "hra" + || environment.VERCEL_GIT_REPO_SLUG !== "hra-v0" ) { throw new Error( "HRA provider source requires the canonical Vercel Git repository identity.", @@ -435,30 +467,27 @@ export async function verifyVercelReleaseSourceState( ) { throw new Error("HRA Production source must be deployed from main."); } - const actualCommit = requireObjectId( + const surfaceCommit = requireObjectId( environment.VERCEL_GIT_COMMIT_SHA, "Vercel source commit", ); - if (contract.release.availability === "candidate") { - return Object.freeze({ - availability: "candidate", - contract, - status: "valid_candidate_contract", - }); - } - const allowedCommit = requireObjectId( + const publicationCommit = requireObjectId( environment[releasePublicationCommitAllowlistEnvironmentVariable], "Trusted Vercel publication commit allowlist", ); - if (actualCommit !== allowedCommit) { + const allowedSurfaceCommits = parseCommitAllowlist( + environment[releaseSurfaceCommitAllowlistEnvironmentVariable], + "Trusted HRA v0 surface commit allowlist", + ); + if (!allowedSurfaceCommits.has(surfaceCommit)) { throw new Error( - "The Vercel Git commit is not the trusted release publication commit.", + "The Vercel Git commit is not an allowlisted HRA v0 archive surface.", ); } - const publishedContract = asPublishedContract(contract); const canonical = await inspectCanonical({ candidateCommit: publishedContract.release.source.commit, - publicationCommit: actualCommit, + publicationCommit, + surfaceCommit, tag: publishedContract.release.tag, }); verifyPublicationContractTransition( @@ -473,11 +502,26 @@ export async function verifyVercelReleaseSourceState( "The canonical annotated release tag differs from the published contract.", ); } + if ( + canonical.surface.publicationCommit !== publicationCommit + || canonical.surface.surfaceCommit !== surfaceCommit + || canonical.surface.status !== "verified_descendant_archive_surface" + ) { + throw new Error( + "The canonical HRA v0 archive surface differs from provider source.", + ); + } + const repositoryMigrationCommit = requireObjectId( + canonical.surface.repositoryMigrationCommit, + "Canonical archive repository migration commit", + ); return Object.freeze({ availability: "published", contract: publishedContract, - publicationCommit: actualCommit, - status: "verified_vercel_publication_binding", + publicationCommit, + repositoryMigrationCommit, + status: "verified_vercel_archive_surface_binding", + surfaceCommit, }); } @@ -490,18 +534,12 @@ export async function verifyReleaseSourceState( contract: ReleaseDownloadContract, options: ReleaseSourceStateOptions = {}, ): Promise { - if (contract.release.availability === "candidate") { - return Object.freeze({ - availability: "candidate", - contract, - status: "valid_candidate_contract", - }); - } const publishedContract = asPublishedContract(contract); const repository = await inspectReleaseSourceRepository(options); - const published = await verifyPublishedReleaseSourceEvidence( + const published = await verifyArchivedReleaseSourceEvidence( publishedContract, repository, + options.publicationCommit ?? HRA_V0_RELEASE_PUBLICATION_COMMIT, ); return Object.freeze({ ...published, @@ -513,31 +551,12 @@ export async function verifyReleaseSourceState( export async function verifyLocalReleaseCandidate( releaseDirectoryValue: string, ): Promise { - const releaseDirectory = await requireCanonicalReleaseDirectory( + await requireCanonicalReleaseDirectory( releaseDirectoryValue, ); - const contract = await verifyReleaseDownloadContract(); - if (contract.release.availability !== "candidate") { - throw new Error("Local candidate verification requires a candidate download contract."); - } - const repository = await inspectReleaseSourceRepository(); - const evidence = await inspectReleaseArtifactSet(releaseDirectory, contract); - if (evidence.commit !== repository.commit) { - throw new Error("The release manifest commit differs from the clean source commit."); - } - const tag = await inspectReleaseTag(repository, contract.release.tag); - if (tag !== null && tag.commit !== repository.commit) { - throw new Error("The release tag already points to another commit."); - } - return Object.freeze({ - artifacts: evidence.artifacts, - commit: repository.commit, - releaseDirectory, - repository: HRA_CANONICAL_REPOSITORY, - runtimeTreeSha256: evidence.runtimeTreeSha256, - status: "verified_local_candidate", - tag: tag ?? "absent", - }); + throw new Error( + "The maintained HRA v0 archive has no candidate release state.", + ); } export async function requirePublishedReleaseSource(): Promise< @@ -545,13 +564,42 @@ export async function requirePublishedReleaseSource(): Promise< > { const contract = await requirePublishedContract(); const repository = await inspectReleaseSourceRepository(); - return await verifyPublishedReleaseSourceEvidence(contract, repository); + return await verifyArchivedReleaseSourceEvidence( + contract, + repository, + HRA_V0_RELEASE_PUBLICATION_COMMIT, + ); } -export async function verifyPublishedReleaseSourceEvidence( +export async function verifyArchivedReleaseSourceEvidence( contract: PublishedReleaseDownloadContract, repository: ReleaseRepositoryEvidence, + publicationCommit: string, ): Promise { + const publication = await inspectReleasePublicationAtCommit( + repository, + contract.release.source.commit, + publicationCommit, + ); + verifyPublicationContractTransition(contract, publication); + const [surface, tag] = await Promise.all([ + inspectArchiveReleaseSurface(repository, publicationCommit), + inspectReleaseTag(repository, contract.release.tag), + ]); + if ( + tag === null + || tag.commit !== contract.release.source.commit + || tag.object !== contract.release.source.tagObject + ) { + throw new Error("The local annotated release tag differs from the published contract."); + } + return Object.freeze({ contract, publication, repository, surface, tag }); +} + +export async function verifyPublishedReleaseSourceEvidence( + contract: PublishedReleaseDownloadContract, + repository: ReleaseRepositoryEvidence, +): Promise> { const publication = await inspectReleasePublicationTransition( repository, contract.release.source.commit, @@ -572,23 +620,25 @@ function verifyPublicationContractTransition( contract: PublishedReleaseDownloadContract, publication: ReleasePublicationEvidence, ): void { - const published = parseContractText( + const published = parseHistoricalPublishedContractText( publication.publicationContract, "The publication release contract is not JSON.", ); - if (!isDeepStrictEqual(published, contract)) { + const expectedArchiveContract: PublishedReleaseDownloadContract = { + release: published.release, + repository: HRA_CANONICAL_REPOSITORY, + schemaVersion: published.schemaVersion, + }; + if (!isDeepStrictEqual(expectedArchiveContract, contract)) { throw new Error( - "The canonical publication contract differs from the provider source.", + "The archive contract is not the exact repository-coordinate migration from publication P.", ); } - const candidate = parseContractText( + const candidate = parseHistoricalCandidateContractText( publication.candidateContract, "The tagged candidate release contract is not JSON.", ); - if (candidate.release.availability !== "candidate") { - throw new Error("The tagged source must contain the candidate release contract."); - } - const expectedCandidate: ReleaseDownloadContract = { + const expectedCandidate: HistoricalCandidateReleaseDownloadContract = { release: { architecture: contract.release.architecture, artifacts: { @@ -619,8 +669,8 @@ function verifyPublicationContractTransition( tag: contract.release.tag, version: contract.release.version, }, - repository: contract.repository, - schemaVersion: contract.schemaVersion, + repository: HRA_HISTORICAL_PUBLICATION_REPOSITORY, + schemaVersion: published.schemaVersion, }; if (!isDeepStrictEqual(candidate, expectedCandidate)) { throw new Error( @@ -629,17 +679,30 @@ function verifyPublicationContractTransition( } } -function parseContractText( +function parseHistoricalCandidateContractText( value: string, invalidJsonMessage: string, -): ReleaseDownloadContract { +): HistoricalCandidateReleaseDownloadContract { let parsed: unknown; try { parsed = JSON.parse(value) as unknown; } catch { throw new Error(invalidJsonMessage); } - return parseReleaseDownloadContract(parsed); + return parseHistoricalReleaseCandidateContract(parsed); +} + +function parseHistoricalPublishedContractText( + value: string, + invalidJsonMessage: string, +): z.infer { + let parsed: unknown; + try { + parsed = JSON.parse(value) as unknown; + } catch { + throw new Error(invalidJsonMessage); + } + return historicalPublishedReleaseDownloadContractSchema.parse(parsed); } export async function verifyPublishedReleaseCandidate( @@ -686,19 +749,22 @@ export async function verifyPublishedReleaseArtifacts( } async function requirePublishedContract(): Promise { - return asPublishedContract(await verifyReleaseDownloadContract()); + return await verifyReleaseDownloadContract(); } function asPublishedContract( - contract: ReleaseDownloadContract, + contract: unknown, ): PublishedReleaseDownloadContract { - if (contract.release.availability !== "published") { - throw new Error("The HRA release-download contract is not published."); + const parsed = releaseDownloadContractSchema.safeParse(contract); + if (!parsed.success) { + throw new Error( + "The maintained HRA v0 release-download contract must be published.", + ); } return Object.freeze({ - release: contract.release, - repository: contract.repository, - schemaVersion: contract.schemaVersion, + release: parsed.data.release, + repository: parsed.data.repository, + schemaVersion: parsed.data.schemaVersion, }); } @@ -917,9 +983,9 @@ function inspectRemoteReleaseAssetMetadata( `GitHub release asset ${expected.name} ID`, ); const expectedDownloadUrl = - `${HRA_CANONICAL_REPOSITORY}/releases/download/${tag}/${expected.name}`; + `${HRA_V0_CURRENT_REPOSITORY}/releases/download/${tag}/${expected.name}`; const expectedApiUrl = - `https://api.github.com/repos/hraness/hra/releases/assets/${id}`; + `https://api.github.com/repos/hraness/hra-v0/releases/assets/${id}`; if ( metadata["state"] !== "uploaded" || metadata["size"] !== expected.bytes @@ -1102,6 +1168,28 @@ function requireObjectId(value: unknown, label: string): string { return value; } +function parseCommitAllowlist( + value: unknown, + label: string, +): ReadonlySet { + if (typeof value !== "string" || value.length === 0 || value.length > 1_312) { + throw new Error(`${label} must contain 1 to 32 full SHA-1 object IDs.`); + } + const commits = value.split(","); + if ( + commits.length === 0 + || commits.length > 32 + || commits.some((commit) => !/^[0-9a-f]{40}$/u.test(commit)) + ) { + throw new Error(`${label} must contain 1 to 32 full SHA-1 object IDs.`); + } + const unique = new Set(commits); + if (unique.size !== commits.length) { + throw new Error(`${label} may not contain duplicate commits.`); + } + return unique; +} + function requireDigest(value: unknown, label: string): string { if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) { throw new Error(`${label} must be one SHA-256 digest.`); diff --git a/apps/desktop/runtime/release-history-contract.ts b/apps/desktop/runtime/release-history-contract.ts new file mode 100644 index 0000000..966973e --- /dev/null +++ b/apps/desktop/runtime/release-history-contract.ts @@ -0,0 +1,310 @@ +import { isDeepStrictEqual } from "node:util"; + +import { z } from "@hra-internal/schema"; + +import releaseHistory from "../../../release-history.json"; + +const repository = "https://github.com/hraness/hra-v0" as const; +const apiRepository = "https://api.github.com/repos/hraness/hra-v0" as const; +const objectIdSchema = z.string().regex(/^[0-9a-f]{40}$/u); +const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u); +const assetSchema = z.object({ + bytes: z.number().int().positive().safe(), + id: z.number().int().positive().safe(), + name: z.string().min(1).max(200), + sha256: digestSchema, +}).strict(); +const releaseSchema = z.object({ + assets: z.array(assetSchema).length(7), + id: z.number().int().positive().safe(), + immutable: z.literal(true), + prerelease: z.literal(true), + publishedAt: z.string().regex(/^2026-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/u), +}).strict(); +const tagSchema = z.object({ + build: z.number().int().positive().safe(), + commit: objectIdSchema, + objectKind: z.literal("annotated"), + release: releaseSchema.nullable(), + tag: z.string().regex(/^v0\.1\.(?:7|8|9|10|11|12|13|14)$/u), + tagObject: objectIdSchema, + version: z.string().regex(/^0\.1\.(?:7|8|9|10|11|12|13|14)$/u), +}).strict(); +const releaseHistorySchema = z.object({ + generation: z.literal(0), + publicationCommit: z.literal("6221f79b745f154882080936b961ff431569f33e"), + repository: z.literal(repository), + repositoryId: z.literal(1_334_876_494), + schemaVersion: z.literal(1), + tags: z.array(tagSchema).length(8), +}).strict().superRefine((history, context) => { + const expected = ["0.1.7", "0.1.8", "0.1.9", "0.1.10", "0.1.11", "0.1.12", "0.1.13", "0.1.14"] as const; + for (const [index, version] of expected.entries()) { + const entry = history.tags[index]; + if ( + entry === undefined + || entry.version !== version + || entry.tag !== `v${version}` + || entry.build !== index + 8 + || (entry.release === null) !== (version === "0.1.11") + ) { + context.addIssue({ + code: "custom", + message: "Release history must contain the exact ordered v0.1.7–v0.1.14 sequence.", + }); + break; + } + if (entry.release !== null) { + const names = entry.release.assets.map(({ name }) => name); + const expectedNames = [ + `HRA-${version}-${entry.build}-macos-arm64.dmg`, + `HRA-${version}-${entry.build}-macos-arm64.dmg.sha256`, + `HRA-${version}-${entry.build}-release-manifest.json`, + ]; + if ( + new Set(names).size !== names.length + || new Set(entry.release.assets.map(({ id }) => id)).size !== entry.release.assets.length + || names.join("\0") !== names.toSorted().join("\0") + || !expectedNames.every((name) => names.includes(name)) + ) { + context.addIssue({ + code: "custom", + message: `Release ${entry.tag} must have one sorted, unique seven-asset inventory.`, + }); + } + } + } +}); + +export type ReleaseHistoryContract = z.infer; +export type ReleaseHistoryFetcher = (url: string, init: RequestInit) => Promise; +export type RemoteReleaseHistoryEvidence = Readonly<{ + assetCount: 49; + releaseCount: 7; + repository: typeof repository; + status: "verified_exact_remote_release_history"; + tagCount: 8; + tagOnly: readonly ["v0.1.11"]; +}>; + +export function parseReleaseHistoryContract(value: unknown): ReleaseHistoryContract { + return releaseHistorySchema.parse(value); +} + +export function readReleaseHistoryContract(): ReleaseHistoryContract { + return parseReleaseHistoryContract(releaseHistory); +} + +export async function verifyRemoteReleaseHistory( + fetcher: ReleaseHistoryFetcher = fetch, +): Promise { + return await verifyRemoteReleaseHistoryState(readReleaseHistoryContract(), fetcher); +} + +export async function verifyRemoteReleaseHistoryState( + contract: ReleaseHistoryContract, + fetcher: ReleaseHistoryFetcher, +): Promise { + const [releaseResponse, refsResponse] = await Promise.all([ + fetchJson(fetcher, `${apiRepository}/releases?per_page=100`, "GitHub release history", 8 * 1_024 * 1_024), + fetchJson(fetcher, `${apiRepository}/git/matching-refs/tags/v0.1`, "GitHub release tags", 1_048_576), + ]); + if (releaseResponse.response.headers.get("link")?.includes('rel="next"') === true) { + throw new Error("GitHub release history exceeds one bounded page."); + } + const remoteReleases = requireArray(releaseResponse.value, "GitHub release history", 100); + const expectedPublished = contract.tags.filter((entry) => entry.release !== null); + if (remoteReleases.length !== expectedPublished.length) { + throw new Error("GitHub has a different exact HRA v0 release set."); + } + const releasesByTag = uniqueRecordsByString(remoteReleases, "tag_name", "GitHub release"); + if (!isDeepStrictEqual( + [...releasesByTag.keys()].toSorted(), + expectedPublished.map(({ tag }) => tag).toSorted(), + )) { + throw new Error("GitHub has a different exact HRA v0 release tag set."); + } + for (const entry of expectedPublished) { + const expectedRelease = entry.release; + if (expectedRelease === null) throw new Error("Published release evidence is missing."); + verifyReleaseMetadata(entry, expectedRelease, releasesByTag.get(entry.tag)); + } + + const remoteRefs = requireArray(refsResponse.value, "GitHub release tags", 64); + const refsByName = uniqueRecordsByString(remoteRefs, "ref", "GitHub tag ref"); + const expectedRefs = contract.tags.map(({ tag }) => `refs/tags/${tag}`).toSorted(); + if (!isDeepStrictEqual([...refsByName.keys()].toSorted(), expectedRefs)) { + throw new Error("GitHub has a different exact HRA v0 tag-ref set."); + } + await Promise.all(contract.tags.map(async (entry) => { + const ref = refsByName.get(`refs/tags/${entry.tag}`); + const refObject = requireRecord(ref?.["object"], `GitHub tag ref ${entry.tag} object`); + if ( + refObject["type"] !== "tag" + || refObject["sha"] !== entry.tagObject + || refObject["url"] !== `${apiRepository}/git/tags/${entry.tagObject}` + ) { + throw new Error(`GitHub tag ref ${entry.tag} differs from annotated tag evidence.`); + } + const { value } = await fetchJson( + fetcher, + `${apiRepository}/git/tags/${entry.tagObject}`, + `GitHub tag object ${entry.tag}`, + 262_144, + ); + const tagObject = requireRecord(value, `GitHub tag object ${entry.tag}`); + const target = requireRecord(tagObject["object"], `GitHub tag object ${entry.tag} target`); + if ( + tagObject["sha"] !== entry.tagObject + || tagObject["tag"] !== entry.tag + || target["type"] !== "commit" + || target["sha"] !== entry.commit + || target["url"] !== `${apiRepository}/git/commits/${entry.commit}` + ) { + throw new Error(`GitHub tag object ${entry.tag} differs from peeled commit evidence.`); + } + })); + + return Object.freeze({ + assetCount: 49, + releaseCount: 7, + repository, + status: "verified_exact_remote_release_history", + tagCount: 8, + tagOnly: Object.freeze(["v0.1.11"] as const), + }); +} + +function verifyReleaseMetadata( + entry: ReleaseHistoryContract["tags"][number], + expected: NonNullable, + raw: Record | undefined, +): void { + const release = requireRecord(raw, `GitHub release ${entry.tag}`); + if ( + release["id"] !== expected.id + || release["tag_name"] !== entry.tag + || release["draft"] !== false + || release["prerelease"] !== expected.prerelease + || release["immutable"] !== expected.immutable + || release["published_at"] !== expected.publishedAt + || release["html_url"] !== `${repository}/releases/tag/${entry.tag}` + ) { + throw new Error(`GitHub release ${entry.tag} differs from the checked release ledger.`); + } + const rawAssets = requireArray(release["assets"], `GitHub release ${entry.tag} assets`, 64); + if (rawAssets.length !== expected.assets.length) { + throw new Error(`GitHub release ${entry.tag} has a different exact asset set.`); + } + const assetsByName = uniqueRecordsByString(rawAssets, "name", `GitHub release ${entry.tag} asset`); + if (!isDeepStrictEqual( + [...assetsByName.keys()].toSorted(), + expected.assets.map(({ name }) => name).toSorted(), + )) { + throw new Error(`GitHub release ${entry.tag} has a different exact asset-name set.`); + } + for (const asset of expected.assets) { + const remote = assetsByName.get(asset.name); + if ( + remote?.["id"] !== asset.id + || remote["state"] !== "uploaded" + || remote["size"] !== asset.bytes + || remote["digest"] !== `sha256:${asset.sha256}` + || remote["browser_download_url"] !== `${repository}/releases/download/${entry.tag}/${asset.name}` + || remote["url"] !== `${apiRepository}/releases/assets/${asset.id}` + ) { + throw new Error(`GitHub release asset ${entry.tag}/${asset.name} differs from the checked ledger.`); + } + } +} + +async function fetchJson( + fetcher: ReleaseHistoryFetcher, + url: string, + label: string, + maximumBytes: number, +): Promise> { + const response = await fetcher(url, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "hraness-hra-release-history-verifier", + "X-GitHub-Api-Version": "2026-03-10", + }, + redirect: "error", + signal: AbortSignal.timeout(30_000), + }); + if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}.`); + const bytes = await readBoundedResponse(response, maximumBytes, label); + try { + return { response, value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown }; + } catch { + throw new Error(`${label} is not valid UTF-8 JSON.`); + } +} + +async function readBoundedResponse( + response: Response, + maximumBytes: number, + label: string, +): Promise { + if (response.body === null) throw new Error(`${label} has no response body.`); + const declared = response.headers.get("content-length"); + if (declared !== null) { + const length = Number(declared); + if (!Number.isSafeInteger(length) || length < 0 || length > maximumBytes) { + throw new Error(`${label} has an invalid or oversized Content-Length.`); + } + } + const chunks: Uint8Array[] = []; + let total = 0; + const reader = response.body.getReader(); + try { + while (true) { + const result = await reader.read(); + if (result.done) break; + total += result.value.byteLength; + if (total > maximumBytes) throw new Error(`${label} exceeds its byte limit.`); + chunks.push(result.value); + } + } finally { + reader.releaseLock(); + } + const output = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + return output; +} + +function requireArray(value: unknown, label: string, maximumLength: number): readonly unknown[] { + if (!Array.isArray(value) || value.length > maximumLength) { + throw new Error(`${label} must be one bounded array.`); + } + return value; +} + +function requireRecord(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`${label} must be an object.`); + } + return value as Record; +} + +function uniqueRecordsByString( + values: readonly unknown[], + key: string, + label: string, +): ReadonlyMap> { + const records = new Map>(); + for (const [index, value] of values.entries()) { + const record = requireRecord(value, `${label} ${index}`); + const identity = record[key]; + if (typeof identity !== "string" || identity.length === 0 || records.has(identity)) { + throw new Error(`${label} identities must be unique nonempty strings.`); + } + records.set(identity, record); + } + return records; +} diff --git a/apps/desktop/runtime/release-provenance.ts b/apps/desktop/runtime/release-provenance.ts index 9f81ac0..c689613 100644 --- a/apps/desktop/runtime/release-provenance.ts +++ b/apps/desktop/runtime/release-provenance.ts @@ -7,12 +7,26 @@ import { import { tmpdir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; -export const HRA_CANONICAL_REPOSITORY = +/** The repository that owns the maintained HRA v0 archive surface. */ +export const HRA_V0_CURRENT_REPOSITORY = + "https://github.com/hraness/hra-v0" as const; + +/** + * The repository spelling recorded in the immutable v0.1.14 publication + * contract. It is historical evidence, not the maintained source location. + */ +export const HRA_HISTORICAL_PUBLICATION_REPOSITORY = "https://github.com/hraness/hra" as const; +/** Canonical repository recorded by the maintained archive contract. */ +export const HRA_CANONICAL_REPOSITORY = + HRA_V0_CURRENT_REPOSITORY; + const fullObjectIdPattern = /^[0-9a-f]{40}$/u; +const archiveHistoryCommitLimit = 1_024; +const releaseContractByteLimit = 32_768; const productionRepositoryRoot = resolve(import.meta.dir, "../../.."); -const canonicalGitRepository = `${HRA_CANONICAL_REPOSITORY}.git` as const; +const canonicalGitRepository = `${HRA_V0_CURRENT_REPOSITORY}.git` as const; const hermeticGitEnvironment = Object.freeze({ GIT_CONFIG_COUNT: "0", GIT_CONFIG_GLOBAL: "/dev/null", @@ -28,7 +42,7 @@ const hermeticGitEnvironment = Object.freeze({ export interface ReleaseRepositoryEvidence { readonly commit: string; readonly gitDirectory: string; - readonly repository: typeof HRA_CANONICAL_REPOSITORY; + readonly repository: typeof HRA_V0_CURRENT_REPOSITORY; readonly repositoryRoot: string; readonly status: "clean_canonical_source"; readonly tree: string; @@ -55,6 +69,18 @@ export interface CanonicalReleasePublicationEvidence { readonly tag: ReleaseTagEvidence; } +export interface ArchiveReleaseSurfaceEvidence { + readonly publicationCommit: string; + readonly repositoryMigrationCommit: string; + readonly status: "verified_descendant_archive_surface"; + readonly surfaceCommit: string; +} + +export interface CanonicalArchiveReleaseEvidence + extends CanonicalReleasePublicationEvidence { + readonly surface: ArchiveReleaseSurfaceEvidence; +} + type ReleaseGitRunner = Readonly<{ run: (args: readonly string[]) => Promise; runAllowNoMatch: (args: readonly string[]) => Promise; @@ -161,7 +187,7 @@ export async function inspectReleaseSourceRepository( return Object.freeze({ commit: normalizedCommit, gitDirectory, - repository: HRA_CANONICAL_REPOSITORY, + repository: HRA_V0_CURRENT_REPOSITORY, repositoryRoot, status: "clean_canonical_source", tree: normalizedTree, @@ -253,6 +279,292 @@ export async function inspectReleasePublicationTransition( ); } +/** + * Verify the immutable C-to-P transition at an explicit historical commit. + * The maintained archive checkout may be a later descendant, so P must not be + * inferred from its HEAD. + */ +export async function inspectReleasePublicationAtCommit( + repository: ReleaseRepositoryEvidence, + candidateCommitValue: string, + publicationCommitValue: string, +): Promise { + const candidateCommit = requireObjectId( + candidateCommitValue, + "Published candidate commit", + ); + const publicationCommit = requireObjectId( + publicationCommitValue, + "Published publication commit", + ); + const runner = releaseGitRunner( + repository.repositoryRoot, + repository.gitDirectory, + ); + return await inspectReleasePublicationWithRunner( + runner, + candidateCommit, + publicationCommit, + ); +} + +/** + * Bind a maintained archive surface to the immutable publication while + * allowing exactly one reviewed repository-coordinate migration. Every other + * byte of release-download.json remains fixed at P. + */ +export async function inspectArchiveReleaseSurface( + repository: ReleaseRepositoryEvidence, + publicationCommitValue: string, +): Promise { + const publicationCommit = requireObjectId( + publicationCommitValue, + "Published publication commit", + ); + const runner = releaseGitRunner( + repository.repositoryRoot, + repository.gitDirectory, + ); + return await inspectArchiveReleaseSurfaceWithRunner( + runner, + publicationCommit, + repository.commit, + ); +} + +async function inspectArchiveReleaseSurfaceWithRunner( + runner: ReleaseGitRunner, + publicationCommit: string, + surfaceCommit: string, +): Promise { + if (publicationCommit === surfaceCommit) { + throw new Error( + "The HRA v0 archive surface must descend from the publication commit.", + ); + } + const mergeBase = requireObjectId( + await runner.run(["merge-base", publicationCommit, surfaceCommit]), + "Archive publication merge base", + ); + if (mergeBase !== publicationCommit) { + throw new Error( + "The HRA v0 archive surface must descend from the publication commit.", + ); + } + const [publicationContract, surfaceContract] = await Promise.all([ + runner.run(["show", `${publicationCommit}:release-download.json`]), + runner.run(["show", `${surfaceCommit}:release-download.json`]), + ]); + const historicalCoordinate = + `"repository": "${HRA_HISTORICAL_PUBLICATION_REPOSITORY}"`; + const currentCoordinate = + `"repository": "${HRA_V0_CURRENT_REPOSITORY}"`; + if ( + publicationContract.split(historicalCoordinate).length !== 2 + || publicationContract.includes(currentCoordinate) + ) { + throw new Error( + "The immutable publication must contain exactly one historical repository coordinate.", + ); + } + const expectedSurfaceContract = publicationContract.replace( + historicalCoordinate, + currentCoordinate, + ); + if (surfaceContract !== expectedSurfaceContract) { + throw new Error( + "The HRA v0 archive surface may change only the reviewed release repository coordinate.", + ); + } + for (const [label, value] of [ + ["publication", publicationContract], + ["surface", surfaceContract], + ] as const) { + if (new TextEncoder().encode(value).byteLength > releaseContractByteLimit) { + throw new Error(`The archive ${label} release contract is oversized.`); + } + } + + // Do not add a pathspec here. Git history simplification may hide a rewrite + // followed by a restore on a merged side branch. The bounded, unpruned DAG + // is the evidence: every commit and every direct parent visible from P..S + // must carry exactly historical H or archive A bytes. + const historyOutput = (await runner.run([ + "rev-list", + "--full-history", + "--topo-order", + "--parents", + `--max-count=${String(archiveHistoryCommitLimit + 1)}`, + `${publicationCommit}..${surfaceCommit}`, + ])).trim(); + const historyLines = historyOutput.length === 0 + ? [] + : historyOutput.split("\n"); + if (historyLines.length > archiveHistoryCommitLimit) { + throw new Error( + `The HRA v0 archive history exceeds ${String(archiveHistoryCommitLimit)} commits.`, + ); + } + if (historyLines.length === 0) { + throw new Error( + "The HRA v0 archive history contains no descendant commits.", + ); + } + + const history = historyLines.map((line, index) => { + const objectIds = line.trim().split(/\s+/u); + const commit = requireObjectId( + objectIds[0] ?? "", + `Archive history commit ${String(index)}`, + ); + const parents = objectIds.slice(1).map((value, parentIndex) => + requireObjectId( + value, + `Archive history commit ${String(index)} parent ${String(parentIndex)}`, + ) + ); + if (parents.length === 0) { + throw new Error("Every HRA v0 archive commit must have a direct parent."); + } + return Object.freeze({ commit, parents }); + }); + if (new Set(history.map(({ commit }) => commit)).size !== history.length) { + throw new Error("The HRA v0 archive history contains a duplicate commit."); + } + + type ArchiveContractState = "archive" | "historical"; + const contracts = new Map([ + [publicationCommit, publicationContract], + [surfaceCommit, surfaceContract], + ]); + const relevantCommits = new Set([publicationCommit]); + for (const { commit, parents } of history) { + relevantCommits.add(commit); + for (const parent of parents) relevantCommits.add(parent); + } + for (const commit of relevantCommits) { + if (contracts.has(commit)) continue; + contracts.set( + commit, + await readBoundedReleaseContractAtCommit(runner, commit), + ); + } + const states = new Map(); + for (const [commit, contract] of contracts) { + const state = contract === publicationContract + ? "historical" + : contract === expectedSurfaceContract + ? "archive" + : null; + if (state === null) { + throw new Error( + `Every HRA v0 archive commit and parent must preserve exact historical H or archive A release contract bytes; ${commit} does not.`, + ); + } + states.set(commit, state); + } + + const frontierCommits: string[] = []; + for (const { commit, parents } of history) { + const state = states.get(commit); + if (state === undefined) { + throw new Error("Archive release contract state is incomplete."); + } + const parentStates = parents.map((parent) => { + const parentState = states.get(parent); + if (parentState === undefined) { + throw new Error("Archive parent release contract state is incomplete."); + } + return parentState; + }); + if ( + state === "historical" + && parentStates.some((parentState) => parentState === "archive") + ) { + throw new Error( + "The HRA v0 archive history may not contain an archive A to historical H edge.", + ); + } + if ( + state === "archive" + && !parentStates.some((parentState) => parentState === "archive") + ) { + if (parents.length !== 1 || parentStates[0] !== "historical") { + throw new Error( + "A merge commit may not invent archive A without an archive A parent.", + ); + } + frontierCommits.push(commit); + } + } + if (frontierCommits.length !== 1) { + throw new Error( + "The HRA v0 archive history must contain exactly one single-parent historical H to archive A frontier.", + ); + } + const repositoryMigrationCommit = requireObjectId( + frontierCommits[0] ?? "", + "Archive repository migration commit", + ); + const migrationParent = history.find( + ({ commit }) => commit === repositoryMigrationCommit, + )?.parents[0]; + if (migrationParent === undefined) { + throw new Error("The archive repository migration parent is missing."); + } + const migrationChangedPath = await runner.run([ + "diff-tree", + "--no-commit-id", + "--name-status", + "-r", + "-z", + "--no-renames", + migrationParent, + repositoryMigrationCommit, + "--", + "release-download.json", + ]); + if ( + contracts.get(migrationParent) !== publicationContract + || contracts.get(repositoryMigrationCommit) !== expectedSurfaceContract + || migrationChangedPath !== "M\0release-download.json\0" + ) { + throw new Error( + "The reviewed release repository migration must be the exact historical-to-archive coordinate change.", + ); + } + return Object.freeze({ + publicationCommit, + repositoryMigrationCommit, + status: "verified_descendant_archive_surface", + surfaceCommit, + }); +} + +async function readBoundedReleaseContractAtCommit( + runner: ReleaseGitRunner, + commit: string, +): Promise { + const object = `${commit}:release-download.json`; + const sizeText = (await runner.run(["cat-file", "-s", object])).trim(); + if (!/^(?:0|[1-9][0-9]*)$/u.test(sizeText)) { + throw new Error("Git reported an invalid archive release contract size."); + } + const size = Number(sizeText); + if ( + !Number.isSafeInteger(size) + || size <= 0 + || size > releaseContractByteLimit + ) { + throw new Error("An archive release contract is empty or oversized."); + } + const contract = await runner.run(["cat-file", "-p", object]); + if (new TextEncoder().encode(contract).byteLength !== size) { + throw new Error("Git emitted inconsistent archive release contract bytes."); + } + return contract; +} + async function inspectReleasePublicationWithRunner( runner: ReleaseGitRunner, candidateCommit: string, @@ -424,6 +736,99 @@ export async function inspectCanonicalReleasePublication(options: Readonly<{ } } +/** + * Fetch and verify both immutable release history and an allowlisted archive + * surface from the maintained HRA v0 repository. P retains the historical + * repository spelling; the archive surface contains its one reviewed + * coordinate migration. + */ +export async function inspectCanonicalArchiveRelease(options: Readonly<{ + candidateCommit: string; + publicationCommit: string; + surfaceCommit: string; + tag: string; +}>): Promise { + const candidateCommit = requireObjectId( + options.candidateCommit, + "Published candidate commit", + ); + const publicationCommit = requireObjectId( + options.publicationCommit, + "Published publication commit", + ); + const surfaceCommit = requireObjectId( + options.surfaceCommit, + "Archive surface commit", + ); + if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/u.test(options.tag)) { + throw new Error("Release tag is invalid."); + } + const temporaryRoot = await realpath( + await mkdtemp(join(tmpdir(), "hra-v0-release-fetch-")), + ); + const gitDirectory = join(temporaryRoot, "publication.git"); + try { + await runHermeticGit( + ["init", "--bare", "--initial-branch=main", gitDirectory], + temporaryRoot, + ); + const runner = releaseGitObjectRunner(gitDirectory); + await runner.run(["remote", "add", "canonical", canonicalGitRepository]); + await runner.run([ + "fetch", + "--quiet", + "--force", + "--no-tags", + "--depth=64", + "--filter=blob:limit=32768", + "canonical", + surfaceCommit, + ]); + await runner.run([ + "fetch", + "--quiet", + "--force", + "--no-tags", + "--depth=2", + "--filter=blob:limit=32768", + "canonical", + publicationCommit, + ]); + const tagRef = `refs/tags/${options.tag}`; + await runner.run([ + "fetch", + "--quiet", + "--force", + "--no-tags", + "--depth=2", + "--filter=blob:limit=32768", + "canonical", + `${tagRef}:${tagRef}`, + ]); + const [publication, tag, surface] = await Promise.all([ + inspectReleasePublicationWithRunner( + runner, + candidateCommit, + publicationCommit, + ), + inspectReleaseTagWithRunner(runner, options.tag), + inspectArchiveReleaseSurfaceWithRunner( + runner, + publicationCommit, + surfaceCommit, + ), + ]); + if (tag === null) { + throw new Error( + "The canonical HRA v0 repository has no annotated release tag.", + ); + } + return Object.freeze({ publication, surface, tag }); + } finally { + await rm(temporaryRoot, { force: true, recursive: true }); + } +} + export function rejectAmbientGitSteering( environment: Readonly>, ): void { diff --git a/apps/desktop/runtime/test/native-bridge-manifest-policy.test.ts b/apps/desktop/runtime/test/native-bridge-manifest-policy.test.ts index 01f2cc8..bdfa7a5 100644 --- a/apps/desktop/runtime/test/native-bridge-manifest-policy.test.ts +++ b/apps/desktop/runtime/test/native-bridge-manifest-policy.test.ts @@ -96,7 +96,7 @@ describe("Native transport lifecycle bridge policy", () => { ); expect(updater).toContain("HRAShowManualUpdateFallbackOnMainThread();"); expect(updater).toContain( - 'NSURL URLWithString:@"https://hra.sh/download"', + 'NSURL URLWithString:@"https://hra-weld.vercel.app/download"', ); const automaticStart = updater.slice( diff --git a/apps/desktop/runtime/test/release-download-contract.test.ts b/apps/desktop/runtime/test/release-download-contract.test.ts index 7dd473e..9985912 100644 --- a/apps/desktop/runtime/test/release-download-contract.test.ts +++ b/apps/desktop/runtime/test/release-download-contract.test.ts @@ -6,19 +6,22 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { + parseHistoricalReleaseCandidateContract, parseReleaseDownloadContract, readReleaseDownloadContract, requirePublishedReleaseSource, verifyReleaseDownloadContract, verifyLocalReleaseCandidate, verifyPublishedReleaseArtifacts, - verifyPublishedReleaseCandidate, + verifyArchivedReleaseSourceEvidence, verifyPublishedReleaseSourceEvidence, verifyReleaseSourceGate, verifyReleaseSourceState, verifyRemoteReleaseState, releasePublicationCommitAllowlistEnvironmentVariable, + releaseSurfaceCommitAllowlistEnvironmentVariable, verifyVercelReleaseSourceState, + type HistoricalCandidateReleaseDownloadContract, type PublishedReleaseDownloadContract, type ReleaseDownloadContract, type ReleaseHttpFetcher, @@ -38,7 +41,10 @@ const setupEnvironment = Object.freeze({ LC_ALL: "C", PATH: "/usr/bin:/bin", }); -const candidateContractFixture = parseReleaseDownloadContract({ +const currentRepository = "https://github.com/hraness/hra-v0" as const; +const historicalPublicationRepository = + "https://github.com/hraness/hra" as const; +const candidateContractFixture = parseHistoricalReleaseCandidateContract({ release: { architecture: "Apple Silicon", artifacts: { @@ -69,7 +75,7 @@ const candidateContractFixture = parseReleaseDownloadContract({ tag: "v0.1.14", version: "0.1.14", }, - repository: "https://github.com/hraness/hra", + repository: historicalPublicationRepository, schemaVersion: 1, }); @@ -82,43 +88,42 @@ afterEach(async () => { }); describe("release and download convergence", () => { - test("verifies the v0.1.14 build 15 repository contract in either protocol state", async () => { + test("verifies the published-only v0.1.14 build 15 archive contract", async () => { const contract = await readReleaseDownloadContract(); expectReleaseIdentity(contract); expect(await verifyReleaseDownloadContract()).toEqual(contract); const source = await verifyReleaseSourceGate(); - if (contract.release.availability === "candidate") { - expect(contract).toEqual(candidateContractFixture); - expect(source).toMatchObject({ - availability: "candidate", - contract, - status: "valid_candidate_contract", - }); - await expectRejection(requirePublishedReleaseSource(), "not published"); - await expectRejection( - verifyPublishedReleaseCandidate("/Applications/HRA.app"), - "not published", - ); - } else { - expect(source).toMatchObject({ - availability: "published", - contract, - status: "verified_published_source", - }); - expect(await requirePublishedReleaseSource()).toMatchObject({ contract }); - } + expect(source).toMatchObject({ + availability: "published", + contract, + status: "verified_published_source", + }); + expect(await requirePublishedReleaseSource()).toMatchObject({ contract }); }); - test("keeps candidate-only fixtures independent from repository publication state", async () => { - const contract = candidateContractFixture; + test("keeps candidate parsing historical and rejects it at every maintained gate", async () => { + const historicalCandidate = candidateContractFixture; + const maintainedCandidate = { + ...historicalCandidate, + repository: currentRepository, + }; + expect(() => parseReleaseDownloadContract(maintainedCandidate)).toThrow(); + expect( + parseHistoricalReleaseCandidateContract(historicalCandidate), + ).toEqual(historicalCandidate); + expect(() => + parseHistoricalReleaseCandidateContract(maintainedCandidate) + ).toThrow(); + + const unsafeCandidate = maintainedCandidate as unknown as ReleaseDownloadContract; let remoteRequests = 0; - expect(await verifyRemoteReleaseState(contract, () => { - remoteRequests += 1; - return Promise.reject(new Error("candidate must not use the network")); - })).toMatchObject({ - availability: "candidate", - status: "candidate_has_no_remote_release", - }); + await expectRejection( + verifyRemoteReleaseState(unsafeCandidate, () => { + remoteRequests += 1; + return Promise.reject(new Error("candidate must not use the network")); + }), + "must be published", + ); expect(remoteRequests).toBe(0); const candidateVercelEnvironment = { VERCEL: "1", @@ -127,40 +132,19 @@ describe("release and download convergence", () => { VERCEL_GIT_COMMIT_SHA: "a".repeat(40), VERCEL_GIT_PROVIDER: "github", VERCEL_GIT_REPO_OWNER: "hraness", - VERCEL_GIT_REPO_SLUG: "hra", + VERCEL_GIT_REPO_SLUG: "hra-v0", VERCEL_TARGET_ENV: "preview", } as const; - expect(await verifyVercelReleaseSourceState( - contract, - candidateVercelEnvironment, - )).toMatchObject({ - availability: "candidate", - contract, - status: "valid_candidate_contract", - }); - for (const environment of [ - { ...candidateVercelEnvironment, VERCEL: "0" }, - { ...candidateVercelEnvironment, VERCEL_GIT_REPO_OWNER: "attacker" }, - { - ...candidateVercelEnvironment, - VERCEL_ENV: "production", - VERCEL_GIT_COMMIT_REF: "feature", - VERCEL_TARGET_ENV: "production", - }, - ] as const) { - await expectRejection( - verifyVercelReleaseSourceState(contract, environment), - environment.VERCEL_GIT_COMMIT_REF === "feature" - ? "deployed from main" - : "canonical Vercel Git repository identity", - ); - } await expectRejection( - verifyVercelReleaseSourceState(contract, { - ...candidateVercelEnvironment, - VERCEL_GIT_COMMIT_SHA: "short", - }), - "one full SHA-1 object ID", + verifyVercelReleaseSourceState( + unsafeCandidate, + candidateVercelEnvironment, + ), + "must be published", + ); + await expectRejection( + verifyReleaseSourceState(unsafeCandidate), + "must be published", ); }); @@ -168,6 +152,7 @@ describe("release and download convergence", () => { const candidate = candidateContractFixture; const published = { ...candidate, + repository: currentRepository, release: { ...candidate.release, artifacts: { @@ -185,6 +170,11 @@ describe("release and download convergence", () => { }; expect(parseReleaseDownloadContract(published).release.availability).toBe("published"); + expect(() => parseReleaseDownloadContract({ + ...candidate, + repository: currentRepository, + })).toThrow(); + expect(() => parseReleaseDownloadContract({ ...published, release: { @@ -255,18 +245,23 @@ describe("release and download convergence", () => { }); test("rejects alternate repositories, extra fields, and mismatched tags", () => { - const candidate = candidateContractFixture; - expect(() => parseReleaseDownloadContract({ - ...candidate, - repository: "https://github.com/attacker/hra", - })).toThrow(); + const contract = createRemoteReleaseFixture(candidateContractFixture).contract; + for (const repository of [ + historicalPublicationRepository, + "https://github.com/attacker/hra", + ] as const) { + expect(() => parseReleaseDownloadContract({ + ...contract, + repository, + })).toThrow(); + } expect(() => parseReleaseDownloadContract({ - ...candidate, + ...contract, callerCommit: "0".repeat(40), })).toThrow(); expect(() => parseReleaseDownloadContract({ - ...candidate, - release: { ...candidate.release, tag: "v0.1.7" }, + ...contract, + release: { ...contract.release, tag: "v0.1.7" }, })).toThrow(); }); @@ -348,6 +343,7 @@ describe("release and download convergence", () => { test("keeps the full release suite valid across a synthetic contract-only publication P", async () => { const candidate = candidateContractFixture; + const historicalCandidate = candidate; const repositoryRoot = await realpath( await mkdtemp(join(tmpdir(), "hra-publication-protocol-")), ); @@ -365,7 +361,7 @@ describe("release and download convergence", () => { ]); await writeFile( join(repositoryRoot, "release-download.json"), - `${JSON.stringify(candidate, null, 2)}\n`, + `${JSON.stringify(historicalCandidate, null, 2)}\n`, ); await writeFile(join(repositoryRoot, "source.txt"), "candidate\n"); await runSetupGit(repositoryRoot, ["add", "release-download.json", "source.txt"]); @@ -388,6 +384,7 @@ describe("release and download convergence", () => { ).trim(); const published = parseReleaseDownloadContract({ ...candidate, + repository: currentRepository, release: { ...candidate.release, artifacts: { @@ -415,9 +412,6 @@ describe("release and download convergence", () => { }, }, }); - if (published.release.availability !== "published") { - throw new Error("Expected a published fixture."); - } const publishedContract: PublishedReleaseDownloadContract = { release: published.release, repository: published.repository, @@ -426,14 +420,29 @@ describe("release and download convergence", () => { expectReleaseIdentity(publishedContract); await writeFile( join(repositoryRoot, "release-download.json"), - `${JSON.stringify(publishedContract, null, 2)}\n`, + `${JSON.stringify(historicalContract(publishedContract), null, 2)}\n`, ); await runSetupGit(repositoryRoot, ["add", "release-download.json"]); await runSetupGit(repositoryRoot, ["commit", "-m", "publication P"]); + const publicationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + await Promise.all([ + writeFile(join(repositoryRoot, "archive.md"), "HRA v0 archive\n"), + writeFile( + join(repositoryRoot, "release-download.json"), + `${JSON.stringify(publishedContract, null, 2)}\n`, + ), + ]); + await runSetupGit(repositoryRoot, ["add", "archive.md", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "archive surface Q"]); + const surfaceCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); const verified = await verifyReleaseSourceState( publishedContract, - { environment: {}, repositoryRoot }, + { environment: {}, publicationCommit, repositoryRoot }, ); if (verified.availability !== "published") { throw new Error("Expected published source evidence."); @@ -441,21 +450,27 @@ describe("release and download convergence", () => { expect(verified.publication).toMatchObject({ candidateCommit, changedPath: "release-download.json", - publicationCommit: verified.repository.commit, + publicationCommit, status: "exact_candidate_publication_transition", }); + expect(verified.surface).toEqual({ + publicationCommit, + repositoryMigrationCommit: surfaceCommit, + status: "verified_descendant_archive_surface", + surfaceCommit, + }); expect(verified.tag.commit).toBe(candidateCommit); expect(verified.repository.commit).not.toBe(candidateCommit); expect(verified.status).toBe("verified_published_source"); const objectStore = await inspectReleasePublicationObjectStore({ candidateCommit, gitDirectory: join(repositoryRoot, ".git"), - publicationCommit: verified.repository.commit, + publicationCommit, tag: publishedContract.release.tag, }); expect(objectStore.publication).toMatchObject({ candidateCommit, - publicationCommit: verified.repository.commit, + publicationCommit, status: "exact_candidate_publication_transition", }); expect(objectStore.tag).toEqual(verified.tag); @@ -463,21 +478,51 @@ describe("release and download convergence", () => { VERCEL: "1", VERCEL_ENV: "production", VERCEL_GIT_COMMIT_REF: "main", - VERCEL_GIT_COMMIT_SHA: verified.repository.commit, + VERCEL_GIT_COMMIT_SHA: surfaceCommit, VERCEL_GIT_PROVIDER: "github", VERCEL_GIT_REPO_OWNER: "hraness", - VERCEL_GIT_REPO_SLUG: "hra", + VERCEL_GIT_REPO_SLUG: "hra-v0", VERCEL_TARGET_ENV: "production", [releasePublicationCommitAllowlistEnvironmentVariable]: - verified.repository.commit, + publicationCommit, + [releaseSurfaceCommitAllowlistEnvironmentVariable]: + `${"f".repeat(40)},${surfaceCommit}`, }, () => Promise.resolve({ publication: verified.publication, + surface: verified.surface, tag: verified.tag, }))).toMatchObject({ availability: "published", - publicationCommit: verified.repository.commit, - status: "verified_vercel_publication_binding", + publicationCommit, + repositoryMigrationCommit: surfaceCommit, + status: "verified_vercel_archive_surface_binding", + surfaceCommit, + }); + + await runSetupGit(repositoryRoot, [ + "switch", + "-c", + "invalid-current-coordinate-publication", + candidateCommit, + ]); + await writeFile( + join(repositoryRoot, "release-download.json"), + `${JSON.stringify(publishedContract, null, 2)}\n`, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "invalid publication coordinate"]); + const invalidPublicationRepository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, }); + await expectRejection( + verifyPublishedReleaseSourceEvidence( + publishedContract, + invalidPublicationRepository, + ), + historicalPublicationRepository, + ); + await runSetupGit(repositoryRoot, ["switch", "main"]); await writeFile(join(repositoryRoot, "source.txt"), "follow-up\n"); await runSetupGit(repositoryRoot, ["add", "source.txt"]); @@ -486,14 +531,60 @@ describe("release and download convergence", () => { environment: {}, repositoryRoot, }); + expect(await verifyArchivedReleaseSourceEvidence( + publishedContract, + followUpRepository, + publicationCommit, + )).toMatchObject({ + surface: { publicationCommit, surfaceCommit: followUpRepository.commit }, + }); await expectRejection( verifyPublishedReleaseSourceEvidence(publishedContract, followUpRepository), "only direct parent", ); + + await writeFile( + join(repositoryRoot, "release-download.json"), + `${JSON.stringify(candidate, null, 2)}\n`, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "rewrite release contract"]); + const rewrittenRepository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + await expectRejection( + verifyArchivedReleaseSourceEvidence( + publishedContract, + rewrittenRepository, + publicationCommit, + ), + "may change only the reviewed release repository coordinate", + ); + + await writeFile( + join(repositoryRoot, "release-download.json"), + `${JSON.stringify(publishedContract, null, 2)}\n`, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "hide contract rewrite"]); + const restoredRepository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + await expectRejection( + verifyArchivedReleaseSourceEvidence( + publishedContract, + restoredRepository, + publicationCommit, + ), + "must preserve exact historical H or archive A release contract bytes", + ); }); test("rejects a schema-valid publication with forged tag evidence", async () => { const candidate = candidateContractFixture; + const historicalCandidate = candidate; const repositoryRoot = await realpath( await mkdtemp(join(tmpdir(), "hra-bogus-publication-")), ); @@ -511,7 +602,7 @@ describe("release and download convergence", () => { ]); await writeFile( join(repositoryRoot, "release-download.json"), - `${JSON.stringify(candidate, null, 2)}\n`, + `${JSON.stringify(historicalCandidate, null, 2)}\n`, ); await writeFile(join(repositoryRoot, "source.txt"), "candidate\n"); await runSetupGit(repositoryRoot, ["add", "release-download.json", "source.txt"]); @@ -537,6 +628,7 @@ describe("release and download convergence", () => { : "f".repeat(40); const bogusPublished = parseReleaseDownloadContract({ ...candidate, + repository: currentRepository, release: { ...candidate.release, artifacts: { @@ -566,27 +658,40 @@ describe("release and download convergence", () => { }); await writeFile( join(repositoryRoot, "release-download.json"), - `${JSON.stringify(bogusPublished, null, 2)}\n`, + `${JSON.stringify(historicalContract(bogusPublished), null, 2)}\n`, ); await runSetupGit(repositoryRoot, ["add", "release-download.json"]); await runSetupGit(repositoryRoot, ["commit", "-m", "bogus publication P"]); const bogusPublicationCommit = ( await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) ).trim(); + await Promise.all([ + writeFile(join(repositoryRoot, "archive.md"), "HRA v0 archive\n"), + writeFile( + join(repositoryRoot, "release-download.json"), + `${JSON.stringify(bogusPublished, null, 2)}\n`, + ), + ]); + await runSetupGit(repositoryRoot, ["add", "archive.md", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "archive surface Q"]); + const surfaceCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); await expectRejection( verifyVercelReleaseSourceState(bogusPublished, { VERCEL: "1", VERCEL_ENV: "production", VERCEL_GIT_COMMIT_REF: "main", - VERCEL_GIT_COMMIT_SHA: bogusPublicationCommit, + VERCEL_GIT_COMMIT_SHA: surfaceCommit, VERCEL_GIT_PROVIDER: "github", VERCEL_GIT_REPO_OWNER: "hraness", - VERCEL_GIT_REPO_SLUG: "hra", + VERCEL_GIT_REPO_SLUG: "hra-v0", VERCEL_TARGET_ENV: "production", [releasePublicationCommitAllowlistEnvironmentVariable]: "a".repeat(40), + [releaseSurfaceCommitAllowlistEnvironmentVariable]: "b".repeat(40), }), - "not the trusted release publication commit", + "not an allowlisted HRA v0 archive surface", ); const bogusObjectStore = await inspectReleasePublicationObjectStore({ @@ -600,27 +705,51 @@ describe("release and download convergence", () => { VERCEL: "1", VERCEL_ENV: "production", VERCEL_GIT_COMMIT_REF: "main", - VERCEL_GIT_COMMIT_SHA: bogusPublicationCommit, + VERCEL_GIT_COMMIT_SHA: surfaceCommit, VERCEL_GIT_PROVIDER: "github", VERCEL_GIT_REPO_OWNER: "hraness", - VERCEL_GIT_REPO_SLUG: "hra", + VERCEL_GIT_REPO_SLUG: "hra-v0", VERCEL_TARGET_ENV: "production", [releasePublicationCommitAllowlistEnvironmentVariable]: bogusPublicationCommit, - }, () => Promise.resolve(bogusObjectStore)), + [releaseSurfaceCommitAllowlistEnvironmentVariable]: surfaceCommit, + }, () => Promise.resolve({ + ...bogusObjectStore, + surface: { + publicationCommit: bogusPublicationCommit, + repositoryMigrationCommit: surfaceCommit, + status: "verified_descendant_archive_surface" as const, + surfaceCommit, + }, + })), "canonical annotated release tag differs", ); await expectRejection( verifyReleaseSourceState( bogusPublished, - { environment: {}, repositoryRoot }, + { + environment: {}, + publicationCommit: bogusPublicationCommit, + repositoryRoot, + }, ), "annotated release tag differs", ); }); }); +function historicalContract( + contract: ReleaseDownloadContract, +): Omit & Readonly<{ + repository: typeof historicalPublicationRepository; +}> { + return { + ...contract, + repository: historicalPublicationRepository, + }; +} + function expectReleaseIdentity(contract: ReleaseDownloadContract): void { expect(contract).toMatchObject({ release: { @@ -635,13 +764,9 @@ function expectReleaseIdentity(contract: ReleaseDownloadContract): void { tag: "v0.1.14", version: "0.1.14", }, - repository: "https://github.com/hraness/hra", + repository: currentRepository, schemaVersion: 1, }); - if (contract.release.availability === "candidate") { - expect(contract).toEqual(candidateContractFixture); - return; - } for (const artifact of Object.values(contract.release.artifacts)) { expect(artifact.bytes).toBeGreaterThan(0); expect(artifact.sha256).toMatch(/^[0-9a-f]{64}$/u); @@ -652,12 +777,9 @@ function expectReleaseIdentity(contract: ReleaseDownloadContract): void { } function createRemoteReleaseFixture( - candidate: ReleaseDownloadContract, + candidate: HistoricalCandidateReleaseDownloadContract, options: Readonly<{ manifestCommit?: string }> = {}, ) { - if (candidate.release.availability !== "candidate") { - throw new Error("Remote fixture requires a candidate contract."); - } const encoder = new TextEncoder(); const commit = "c".repeat(40); const runtimeTreeSha256 = "d".repeat(64); @@ -703,6 +825,7 @@ function createRemoteReleaseFixture( }, null, 2)}\n`); const contract = parseReleaseDownloadContract({ ...candidate, + repository: currentRepository, release: { ...candidate.release, artifacts: { @@ -730,11 +853,8 @@ function createRemoteReleaseFixture( }, }, }); - if (contract.release.availability !== "published") { - throw new Error("Expected a published remote fixture."); - } const metadataUrl = - `https://api.github.com/repos/hraness/hra/releases/tags/${contract.release.tag}`; + `https://api.github.com/repos/hraness/hra-v0/releases/tags/${contract.release.tag}`; const metadataAssets = [ contract.release.artifacts.checksum, contract.release.artifacts.dmg, @@ -746,13 +866,13 @@ function createRemoteReleaseFixture( })), ].map((artifact, index) => ({ browser_download_url: - `https://github.com/hraness/hra/releases/download/${contract.release.tag}/${artifact.name}`, + `https://github.com/hraness/hra-v0/releases/download/${contract.release.tag}/${artifact.name}`, digest: `sha256:${artifact.sha256}`, id: 100 + index, name: artifact.name, size: artifact.bytes, state: "uploaded", - url: `https://api.github.com/repos/hraness/hra/releases/assets/${100 + index}`, + url: `https://api.github.com/repos/hraness/hra-v0/releases/assets/${100 + index}`, })); const metadata = { assets: metadataAssets, diff --git a/apps/desktop/runtime/test/release-history-contract.test.ts b/apps/desktop/runtime/test/release-history-contract.test.ts new file mode 100644 index 0000000..cf8d3b7 --- /dev/null +++ b/apps/desktop/runtime/test/release-history-contract.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, test } from "bun:test"; + +import { + parseReleaseHistoryContract, + readReleaseHistoryContract, + verifyRemoteReleaseHistoryState, + type ReleaseHistoryFetcher, +} from "../release-history-contract"; + +const repository = "https://github.com/hraness/hra-v0" as const; +const apiRepository = "https://api.github.com/repos/hraness/hra-v0" as const; + +describe("HRA v0 remote release history", () => { + test("verifies all eight annotated tags and all 49 immutable release assets", async () => { + const fixture = createRemoteHistoryFixture(); + expect(await verifyRemoteReleaseHistoryState(fixture.contract, fixture.fetcher)).toEqual({ + assetCount: 49, + releaseCount: 7, + repository, + status: "verified_exact_remote_release_history", + tagCount: 8, + tagOnly: ["v0.1.11"], + }); + expect(fixture.requests).toHaveLength(10); + expect(fixture.requests).toContain(`${apiRepository}/releases?per_page=100`); + expect(fixture.requests).toContain(`${apiRepository}/git/matching-refs/tags/v0.1`); + for (const entry of fixture.contract.tags) { + expect(fixture.requests).toContain(`${apiRepository}/git/tags/${entry.tagObject}`); + } + expect(fixture.requests.some((url) => url.includes("/releases/download/"))).toBeFalse(); + }); + + test("rejects an additional v0.1.11 release, asset drift, and a moved tag", async () => { + const extraRelease = createRemoteHistoryFixture(); + extraRelease.releases.push({ + assets: [], + draft: false, + html_url: `${repository}/releases/tag/v0.1.11`, + id: 1, + immutable: true, + prerelease: true, + published_at: "2026-08-20T00:00:00Z", + tag_name: "v0.1.11", + }); + await expectRejects( + verifyRemoteReleaseHistoryState(extraRelease.contract, extraRelease.fetcher), + "different exact HRA v0 release set", + ); + + const digestDrift = createRemoteHistoryFixture(); + const assets = requireRecord(digestDrift.releases[0], "release")["assets"]; + if (!Array.isArray(assets) || assets[0] === undefined) { + throw new Error("Expected fixture asset."); + } + requireRecord(assets[0], "asset")["digest"] = `sha256:${"0".repeat(64)}`; + await expectRejects( + verifyRemoteReleaseHistoryState(digestDrift.contract, digestDrift.fetcher), + "differs from the checked ledger", + ); + + const movedTag = createRemoteHistoryFixture(); + const finalEntry = movedTag.contract.tags.at(-1); + if (finalEntry === undefined) throw new Error("Expected final history entry."); + const target = movedTag.tagObjects.get(finalEntry.tagObject); + if (target === undefined) throw new Error("Expected final tag object."); + requireRecord(target["object"], "tag target")["sha"] = "0".repeat(40); + await expectRejects( + verifyRemoteReleaseHistoryState(movedTag.contract, movedTag.fetcher), + "differs from peeled commit evidence", + ); + }); + + test("parses only the exact ordered tag-only and release sequence", () => { + const contract = readReleaseHistoryContract(); + expect(contract.tags.map(({ tag }) => tag)).toEqual([ + "v0.1.7", + "v0.1.8", + "v0.1.9", + "v0.1.10", + "v0.1.11", + "v0.1.12", + "v0.1.13", + "v0.1.14", + ]); + expect(contract.tags.map(({ commit, release, tagObject }) => ({ + commit, + releaseId: release?.id ?? null, + tagObject, + }))).toEqual([ + { commit: "4fa78a8c6141446be343be13df056381c3b5a224", releaseId: 371487477, tagObject: "b9789e1104b6943a36edcc7d61b28635141e3be0" }, + { commit: "a3a142452921b9b9299d880f251d66dbe51c823b", releaseId: 371977411, tagObject: "0b3d3aa7e88e5537cc4b6f85b2dbf3969dd12c60" }, + { commit: "531eb23e165852e2921282862c019770bb3eb914", releaseId: 372074589, tagObject: "1b62548881f190272d0095f3900d683f43c34a69" }, + { commit: "2457962b31b873b9b0521ca5606b9ad3746404de", releaseId: 372110643, tagObject: "9abeb033d6d965bf214b062c3c6266bc600ec76b" }, + { commit: "5a2a9842cacc75fee42ab8e23ca8c215a643e21e", releaseId: null, tagObject: "e4c171e33e414d74a36791fc8577cbfbcef8e52e" }, + { commit: "9ab991d08d1507fd73c9e7ef5fb4a37baee9c014", releaseId: 374867227, tagObject: "626be494d24733d12e53d09932cb5cc6218bc2fe" }, + { commit: "9ba06a441c9b12b448cfe34784432592dbeccb19", releaseId: 374920071, tagObject: "44f00fd5c5e00bc8dcded0c9b176a8e37ada90f3" }, + { commit: "7b39c459827b2acf45aa2d911c94fdb5d4f37860", releaseId: 374980441, tagObject: "37ed37afb39cacfd6a51044cf7f3c1b873571aa3" }, + ]); + expect(contract.tags[4]?.release).toBeNull(); + const reordered = structuredClone(contract); + [reordered.tags[0], reordered.tags[1]] = [reordered.tags[1]!, reordered.tags[0]!]; + expect(() => parseReleaseHistoryContract(reordered)).toThrow( + "exact ordered v0.1.7–v0.1.14 sequence", + ); + }); +}); + +function createRemoteHistoryFixture() { + const contract = readReleaseHistoryContract(); + const releases: Record[] = contract.tags.flatMap((entry) => { + if (entry.release === null) return []; + return [{ + assets: entry.release.assets.map((asset) => ({ + browser_download_url: `${repository}/releases/download/${entry.tag}/${asset.name}`, + digest: `sha256:${asset.sha256}`, + id: asset.id, + name: asset.name, + size: asset.bytes, + state: "uploaded", + url: `${apiRepository}/releases/assets/${asset.id}`, + })), + draft: false, + html_url: `${repository}/releases/tag/${entry.tag}`, + id: entry.release.id, + immutable: entry.release.immutable, + prerelease: entry.release.prerelease, + published_at: entry.release.publishedAt, + tag_name: entry.tag, + }]; + }); + const refs = contract.tags.map((entry) => ({ + object: { + sha: entry.tagObject, + type: "tag", + url: `${apiRepository}/git/tags/${entry.tagObject}`, + }, + ref: `refs/tags/${entry.tag}`, + })); + const tagObjects = new Map(contract.tags.map((entry) => [entry.tagObject, { + object: { + sha: entry.commit, + type: "commit", + url: `${apiRepository}/git/commits/${entry.commit}`, + }, + sha: entry.tagObject, + tag: entry.tag, + }])); + const requests: string[] = []; + const fetcher: ReleaseHistoryFetcher = (url, init) => { + requests.push(url); + if (new Headers(init.headers).has("authorization")) { + return Promise.reject(new Error("History verification must remain credential-free.")); + } + if (url === `${apiRepository}/releases?per_page=100`) return Promise.resolve(jsonResponse(releases)); + if (url === `${apiRepository}/git/matching-refs/tags/v0.1`) return Promise.resolve(jsonResponse(refs)); + const prefix = `${apiRepository}/git/tags/`; + if (url.startsWith(prefix)) { + const value = tagObjects.get(url.slice(prefix.length)); + if (value !== undefined) return Promise.resolve(jsonResponse(value)); + } + return Promise.reject(new Error(`Unexpected history request: ${url}`)); + }; + return { contract, fetcher, releases, requests, tagObjects }; +} + +function jsonResponse(value: unknown): Response { + const body = JSON.stringify(value); + return new Response(body, { + headers: { "content-length": String(new TextEncoder().encode(body).byteLength) }, + status: 200, + }); +} + +function requireRecord(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`Expected ${label}.`); + } + return value as Record; +} + +async function expectRejects(promise: Promise, message: string): Promise { + try { + await promise; + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain(message); + return; + } + throw new Error(`Expected rejection containing ${message}.`); +} diff --git a/apps/desktop/runtime/test/release-provenance.test.ts b/apps/desktop/runtime/test/release-provenance.test.ts index 7f948cc..2fcdf9c 100644 --- a/apps/desktop/runtime/test/release-provenance.test.ts +++ b/apps/desktop/runtime/test/release-provenance.test.ts @@ -11,7 +11,8 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { - HRA_CANONICAL_REPOSITORY, + HRA_V0_CURRENT_REPOSITORY, + inspectArchiveReleaseSurface, inspectReleasePublicationTransition, inspectReleaseSourceRepository, inspectReleaseTag, @@ -50,7 +51,7 @@ describe("hermetic release provenance", () => { expect(evidence).toMatchObject({ gitDirectory: join(repositoryRoot, ".git"), - repository: HRA_CANONICAL_REPOSITORY, + repository: HRA_V0_CURRENT_REPOSITORY, repositoryRoot, status: "clean_canonical_source", }); @@ -347,6 +348,195 @@ describe("hermetic release provenance", () => { "only direct parent", ); }); + + test("accepts one single-parent migration followed by a normal H/A to A merge", async () => { + const repositoryRoot = await createRepository(); + await writeFile( + join(repositoryRoot, "release-download.json"), + '{\n "repository": "https://github.com/hraness/hra"\n}\n', + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "publication P"]); + const publicationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + + await runSetupGit(repositoryRoot, ["switch", "-c", "archive-pr"]); + await writeFile( + join(repositoryRoot, "release-download.json"), + '{\n "repository": "https://github.com/hraness/hra-v0"\n}\n', + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "migrate archive coordinate"]); + const repositoryMigrationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + await writeFile(join(repositoryRoot, "side.txt"), "archive branch\n"); + await runSetupGit(repositoryRoot, ["add", "side.txt"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "archive branch work"]); + + await runSetupGit(repositoryRoot, ["switch", "main"]); + await writeFile(join(repositoryRoot, "main.txt"), "main branch\n"); + await runSetupGit(repositoryRoot, ["add", "main.txt"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "main archive work"]); + await runSetupGit(repositoryRoot, [ + "merge", + "--no-ff", + "archive-pr", + "-m", + "merge archive PR", + ]); + const repository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + + expect( + await inspectArchiveReleaseSurface(repository, publicationCommit), + ).toEqual({ + publicationCommit, + repositoryMigrationCommit, + status: "verified_descendant_archive_surface", + surfaceCommit: repository.commit, + }); + }); + + test("rejects a repository-coordinate migration made by a merge commit", async () => { + const repositoryRoot = await createRepository(); + await writeFile( + join(repositoryRoot, "release-download.json"), + '{\n "repository": "https://github.com/hraness/hra"\n}\n', + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "publication P"]); + const publicationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + + await runSetupGit(repositoryRoot, ["switch", "-c", "migration-side"]); + await writeFile(join(repositoryRoot, "side.txt"), "side\n"); + await runSetupGit(repositoryRoot, ["add", "side.txt"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "side archive work"]); + await runSetupGit(repositoryRoot, ["switch", "main"]); + await writeFile(join(repositoryRoot, "main.txt"), "main\n"); + await runSetupGit(repositoryRoot, ["add", "main.txt"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "main archive work"]); + await runSetupGit(repositoryRoot, [ + "merge", + "--no-ff", + "--no-commit", + "migration-side", + ]); + await writeFile( + join(repositoryRoot, "release-download.json"), + '{\n "repository": "https://github.com/hraness/hra-v0"\n}\n', + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "merge migration"]); + const repository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + + await expectRejection( + inspectArchiveReleaseSurface(repository, publicationCommit), + "may not invent archive A without an archive A parent", + ); + }); + + test("rejects a rewrite and restore hidden on a merged side branch", async () => { + const repositoryRoot = await createRepository(); + const historicalContract = + '{\n "repository": "https://github.com/hraness/hra"\n}\n'; + const archiveContract = + '{\n "repository": "https://github.com/hraness/hra-v0"\n}\n'; + await writeFile( + join(repositoryRoot, "release-download.json"), + historicalContract, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "publication P"]); + const publicationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + + await runSetupGit(repositoryRoot, ["switch", "-c", "rewrite-side"]); + await writeFile( + join(repositoryRoot, "release-download.json"), + '{\n "repository": "https://github.com/attacker/hra"\n}\n', + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "rewrite contract"]); + await writeFile( + join(repositoryRoot, "release-download.json"), + historicalContract, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "restore contract"]); + + await runSetupGit(repositoryRoot, ["switch", "main"]); + await writeFile( + join(repositoryRoot, "release-download.json"), + archiveContract, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "migrate archive coordinate"]); + await runSetupGit(repositoryRoot, [ + "merge", + "--no-ff", + "rewrite-side", + "-m", + "merge restored side branch", + ]); + const repository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + + await expectRejection( + inspectArchiveReleaseSurface(repository, publicationCommit), + "must preserve exact historical H or archive A release contract bytes", + ); + }); + + test("rejects every archive A to historical H edge", async () => { + const repositoryRoot = await createRepository(); + const historicalContract = + '{\n "repository": "https://github.com/hraness/hra"\n}\n'; + const archiveContract = + '{\n "repository": "https://github.com/hraness/hra-v0"\n}\n'; + await writeFile( + join(repositoryRoot, "release-download.json"), + historicalContract, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", "publication P"]); + const publicationCommit = ( + await runSetupGit(repositoryRoot, ["rev-parse", "HEAD"]) + ).trim(); + + for (const [contract, message] of [ + [archiveContract, "migrate archive coordinate"], + [historicalContract, "downgrade archive coordinate"], + [archiveContract, "restore archive coordinate"], + ] as const) { + await writeFile( + join(repositoryRoot, "release-download.json"), + contract, + ); + await runSetupGit(repositoryRoot, ["add", "release-download.json"]); + await runSetupGit(repositoryRoot, ["commit", "-m", message]); + } + const repository = await inspectReleaseSourceRepository({ + environment: {}, + repositoryRoot, + }); + + await expectRejection( + inspectArchiveReleaseSurface(repository, publicationCommit), + "may not contain an archive A to historical H edge", + ); + }); }); async function createRepository(): Promise { diff --git a/apps/desktop/src/macos_updater.m b/apps/desktop/src/macos_updater.m index f121d57..5d8293e 100644 --- a/apps/desktop/src/macos_updater.m +++ b/apps/desktop/src/macos_updater.m @@ -1035,14 +1035,14 @@ static void HRAShowManualUpdateFallbackOnMainThread(void) { alert.messageText = @"Automatic update checking is unavailable"; alert.informativeText = [NSString stringWithFormat: @"%@ will keep running. Retry later, or download the latest release " - @"from https://hra.sh/download.", + @"from https://hra-weld.vercel.app/download.", installed]; [alert addButtonWithTitle:@"Open Download Page"]; [alert addButtonWithTitle:@"Not Now"]; if ([alert runModal] != NSAlertFirstButtonReturn) { return; } - NSURL *downloadURL = [NSURL URLWithString:@"https://hra.sh/download"]; + NSURL *downloadURL = [NSURL URLWithString:@"https://hra-weld.vercel.app/download"]; if (downloadURL != nil) { [NSWorkspace.sharedWorkspace openURL:downloadURL]; } diff --git a/apps/desktop/src/runtime_host.zig b/apps/desktop/src/runtime_host.zig index a2f95b8..8f58e30 100644 --- a/apps/desktop/src/runtime_host.zig +++ b/apps/desktop/src/runtime_host.zig @@ -10989,13 +10989,13 @@ test "production cloud coordinates are pinned and ambient overrides are ignored" &paths, .production, .{ - .api_origin = "https://hra.sh", + .api_origin = "https://hra-weld.vercel.app", .workos_client_id = "client_release", }, ); defer enabled.deinit(); try std.testing.expectEqualStrings( - "https://hra.sh", + "https://hra-weld.vercel.app", enabled.get("HRA_CLOUD_API_URL").?, ); try std.testing.expectEqualStrings( diff --git a/apps/web/.env.example b/apps/web/.env.example index f21de28..5d74d12 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -5,7 +5,7 @@ NEXT_PUBLIC_CONVEX_SITE_URL= # Hraness suite account origin. It must match the checked OIDC consumer # HRA contract exactly; arbitrary origins fail closed. -NEXT_PUBLIC_SITE_URL=https://hra.sh +NEXT_PUBLIC_SITE_URL=https://hra-weld.vercel.app # Public token for shared PostHog project 543691. Scope this record to Vercel # Production only, and enable cookieless mode in the PostHog project. Absence diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 680e254..255ad7c 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -1,6 +1,6 @@ # Contents -- `app/` – the public `hra.sh` landing and download surfaces, authenticated `/app` control plane, Suite prose bridge, and Convex authority adapter for the shared task surface. +- `app/` – the public HRA v0 archive and download surfaces at `hra-weld.vercel.app`, authenticated legacy `/app` control plane, Suite prose bridge, and Convex authority adapter for the shared task surface. - `suite-account-*.ts` – HRA-owned receipt, profile, browser-session, OIDC, and canonical `account.hraness.com` contracts. - `direct/` – the isolated deterministic browser composition, scenarios, and evidence verifier. - `convex/` – schema, authentication, HTTP routes, and transactional domain functions. @@ -32,9 +32,9 @@ - Resolve shared icon entry points to the HRA-local SVG adapter for the Next.js graph, and reject Hugeicons module identifiers or emitted markers during every production build. - Keep the authenticated control plane inside the persistent shared app shell. Workspace and Tasks/Access route state belongs in the query-addressed rail, and only the changing main stage animates. - Keep `/` public and indexable without WorkOS or Convex client providers. Keep `/app` authenticated when WorkOS is configured, and send sign-in, sign-up, callback, organization-switch, and internal control-plane navigation back to that route. -- Keep hosted-environment credentials and provider-write configuration out of source control. `hra.sh` is the canonical public origin, and the normal build must not mutate Convex or a hosting provider. +- Keep hosted-environment credentials and provider-write configuration out of source control. `hra-weld.vercel.app` is the canonical HRA v0 archive origin, and the normal build must not mutate Convex or a hosting provider. - Send only anonymous, cookieless `$pageview` events from exact canonical public routes. Keep the public PostHog token Production-only, and leave Preview and every authenticated, account, task, and control-plane route without analytics ingestion capability. -- Wrap the HRA Next.js configuration with `@hraness/vercel-delivery` using the registered `hra` project name. Preserve HRA security and private-cache headers ahead of the package-owned delivery and Preview response policy. +- Wrap the HRA Next.js configuration with `@hraness/vercel-delivery` using the registered `hra-v0` project name. Preserve HRA security and private-cache headers ahead of the package-owned delivery and Preview response policy. - Permit remote Convex deployment only through the checked Vercel Production wrapper bound to `benevolent-akita-439`. Preview is an app-only client of its exact public endpoints and must reject every production credential, origin claim, or write capability. - Keep `/download` as an exact public prerelease/source-build page. Retain authentication on near misses, and keep signing, notarization, artifact hosting, and release mutation authority outside the web application. - Keep Hraness suite identity additive to the WorkOS human. The OIDC cookie secret belongs only in the web host, the HRA receipt HMAC keyring belongs only in Convex, and missing authority must leave linking unavailable. diff --git a/apps/web/README.md b/apps/web/README.md index b84e80a..4f1361d 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,6 +1,9 @@ -# HRA web control plane +# HRA v0 web control plane -The web workspace owns HRA's local Convex task backend and human control plane. `taskctl` consumes its versioned HTTP API from the sibling CLI workspace. The canonical public origin is `https://hra.sh`. +This archived workspace owns HRA v0's Convex task backend and human control +plane. `taskctl` consumes its versioned HTTP API from the sibling CLI +workspace. The canonical HRA v0 origin is `https://hra-weld.vercel.app`. The +current HRA lives at `https://hra.sh` in `https://github.com/hraness/hra`. The task workspace also owns desktop-runner presence and dispatch. A task created from the web surface is committed atomically with its queued run only when a non-expired HRA gateway advertises the selected repository and free capacity. The browser subscribes to server-authenticated readiness and a bounded public display stream: reasoning summaries, assistant messages, content-free tool activity, and lifecycle state. It never connects directly to a desktop app and never receives raw reasoning, tool details, local paths, command output, or Codex credentials. @@ -51,7 +54,7 @@ Configure `WORKOS_WEBHOOK_SECRET` in the Convex deployment and send WorkOS webho Two leased, paginated Convex jobs run every 15 minutes. One rechecks projected membership IDs, including provider-side deletion; the other enumerates active, inactive, and pending provider memberships for every projected WorkOS organization so a missed create webhook is recoverable. Bounded runs schedule immediate cursor-based continuations instead of waiting for the next interval. Provider calls remain in actions, and projection writes remain in transactions. -The workspace serves the canonical `hra.sh` origin. Local `bun run build` +The workspace serves the canonical `hra-weld.vercel.app` origin. Local `bun run build` compiles the application without provider mutation. Vercel uses the separate checked build entry in `vercel.json`: only its exact Production target may deploy Convex functions to `benevolent-akita-439`. A generated Preview skips @@ -59,8 +62,8 @@ Convex deployment and builds an anonymous app-only client of the exact `benevolent-akita-439.convex.cloud` and `benevolent-akita-439.convex.site` public endpoints. Custom staging and every unrecognized provider target fail closed. The Next.js configuration binds every verified Vercel deployment to -the registered `hra` project and exact Git object through -`X-Hraness-Delivery-Proof`. It preserves HRA's security and private-cache +the registered `hra-v0` project and exact Git object through +`X-Hraness-Delivery-Proof`. It preserves HRA v0's security and private-cache headers, and adds `X-Robots-Tag: noindex, nofollow, noarchive` to generated Preview responses. Session sync remains fail-closed while `HRA_SESSION_SYNC_ENABLED` is absent or `false`; the unchanged @@ -69,291 +72,91 @@ fail closed. Enable it only after the exact WorkOS application, Convex environment, desktop public coordinates, and production HTTP route readbacks pass for the same source revision. -## Production provider cutover - -Before any provider deployment, enable Vercel's **Automatically expose System -Environment Variables** setting on the exact HRA project and read it back as -enabled for Production and Preview. The source gate requires Vercel's own -`VERCEL`, target, Git provider, repository owner/name, branch, and full commit -SHA evidence. It intentionally refuses when that project setting is disabled -or any system identity is absent; caller-created substitutes are not an -accepted deployment path. - -Scope the public `NEXT_PUBLIC_CONVEX_URL` and -`NEXT_PUBLIC_CONVEX_SITE_URL` records to Vercel Production and Preview, with -the exact `benevolent-akita-439` URLs. Scope the Convex production deploy-key record, -`SUITE_IDENTITY_RECEIPT_KEY_VERSION`, `SUITE_OIDC_COOKIE_SECRET`, and -`NEXT_PUBLIC_SITE_URL` to Production only. Scope `NEXT_PUBLIC_POSTHOG_KEY` to -Production only as well; it is a public project token, but it grants analytics -ingestion capability. Use the token for shared PostHog project `543691`, and -enable cookieless mode in that project. When present, the browser sends one anonymous, -cookieless `$pageview` for exact canonical public routes to the fixed -`https://us.i.posthog.com` host. It sends no analytics from Preview, `/app`, -authentication, callbacks, APIs, account or task surfaces, or route near -misses. A missing token leaves analytics inert; a configured malformed token -refuses the Production build before Next.js can embed it. The deploy key must name -`benevolent-akita-439`, the selector must be `v1`, and the site URL must be -`https://hra.sh`. Create a fresh HRA-held deploy key for that deployment and a -fresh independent HRA cookie secret. Do not copy or share the predecessor -deploy key or cookie secret. Cookies cannot cross from `oprte.com` to `hra.sh`, -and deploy keys are exact project/environment custody. Do not copy the -predecessor `https://oprte.com` site value. - -Create the deploy key only with the source-owned Management API helper. The -helper requires the immutable Convex project ID `2680173`, Production -deployment ID `4677913`, deployment name `benevolent-akita-439`, and pre-rename -`cclrte:oprte` identity. It requests exactly `deployment:deploy`, verifies the -new key metadata, and writes the returned secret only through an already-open -caller-owned mode-0600 file descriptor. Its report contains IDs, names, -permissions, file mode, and byte count, never the personal access token, deploy -key, or a stable secret digest. - -First restrict every retained copy of the Convex personal-token config, -including dated backups, to its owner. Then create a fresh empty destination -in a fresh owner-only directory and pass only canonical paths: - -```sh -find ~/.convex -maxdepth 1 -type f -name 'config.json*' -exec chmod 0600 {} \; -CONVEX_AUTH_FILE="$(realpath ~/.convex/config.json)" -HRA_PROVIDER_DIR="$(mktemp -d /private/tmp/hra-provider-cutover.XXXXXX)" -chmod 0700 "$HRA_PROVIDER_DIR" -HRA_DEPLOY_KEY_FILE="$(mktemp "$HRA_PROVIDER_DIR/deploy-key.XXXXXX")" -chmod 0600 "$HRA_DEPLOY_KEY_FILE" -bun run provider:create-convex-deploy-key -- \ - --auth-file "$CONVEX_AUTH_FILE" \ - --secret-file "$HRA_DEPLOY_KEY_FILE" -``` - -Do not use `convex deployment token create` for this key. Convex 1.44 does not -send an `allowedActions` constraint from that command, and its default path -prints the generated secret. If the checked helper reports any failure, or -exits abnormally after sending the create request, do not rerun it until you -inspect the fixed -`vercel-hra-production-2026-08-17` name in the dashboard, revoke any incomplete -result, empty the destination, and begin again. This recovery rule includes -post-create readback, file write, `fsync`, path-identity, and abnormal-exit -failures because the provider key may exist even when local custody did not -commit. Install the successful file as the HRA Vercel Production deploy-key -record without copying it through argv, shell history, or terminal output. -Remove the local file only after Vercel scope and build readback succeed. - -Do not configure Convex-only custody in Vercel, even as an empty record. The -Production wrapper refuses the receipt keyring, session flags, hosted-mutation -keys, taskctl peppers or fixture settings, WorkOS webhook secret and owner -role, and local WorkOS provider overrides. Configure those values only on the -exact Convex deployment. - -For the one-time initial cutover, leave -`HRA_RELEASE_PUBLICATION_COMMIT_ALLOWLIST` unset while the release contract is -`candidate`. For a subsequent release, preserve the exact currently published -commit in that record while the new candidate is built. A candidate provider -build may proceed with that unchanged record, and the wrapper removes it before -launching the final Next build. When the contract changes to `published`, an -automatic build for the new publication commit refuses on the old allowlist. -After the Required CI job succeeds on exact P, replace this non-secret trusted -manual allowlist with P for Vercel Production and Preview, then redeploy exact -P. The provider wrapper proves only that Vercel's canonical `hraness/hra` Git -SHA equals the allowlist; the operator's Required-CI readback is what authorizes -changing it. A mismatch, missing value, non-Git build, wrong repository, or -Production branch other than `main` makes a published build refuse. - -Transfer no custom-staging record. Preview refuses the deploy key, deployment -token, Convex selector, canonical production site claim, Suite cookie or key -selector, WorkOS credentials, and every other checked production capability, -including an empty provider record. Its two public Convex URLs grant no -backend publication authority. - -The create-only deploy-key and shared-authority procedures above are initialization -and recovery guidance. They are not v0.1.14 publication steps while the verified -v0.1.13 authorities remain live. Do not create or rotate the HRA deploy key, -cookie or identity-link secret, Accounts entry, HRA keyring, or receipt-key -selector for this recovery release. - -Use this order for the v0.1.14 recovery publication: - -1. Read back that the predecessor Vercel project remains publication-frozen, - has no deployment in flight, and retains its unchanged READY rollback - anchor. Preserve its bounded rollback key without rotating or recreating it. -2. Read back the existing HRA Vercel project and Git link, exact build commands, - environment-name inventory, and environment scoping. Preserve every existing - deploy, cookie, Suite identity, WorkOS, and Convex selector authority without - exposing or rewriting its value. Preview must remain free of production-only - authority. -3. Read back the existing Accounts HRA entry, HRA-only identity-link keyring, - and receipt-key selector with the checked audits. Treat any drift as a stop; - do not repair it by generating or appending a replacement during release. -4. From the exact candidate commit C, complete the full package, then follow - the desktop release runbook to create and push the - direct annotated `v0.1.14` tag and - publish the exact seven-asset immutable GitHub prerelease. Fill the - working-tree publication contract from C's emitted evidence and require - `bun run verify:remote-release` to read back exact remote names, byte counts, - SHA-256 digests, checksum, manifest, DMG binding, and corresponding-source - binding before committing P. Create P as the exact contract-only child, - push it, and require the green Required CI source-and-remote readback for - that exact P. Preserve the v0.1.13 publication allowlist throughout C. A Git - build of candidate C may proceed against the unchanged backend authority; - it must not create or rotate provider authority. The automatic build of - published P must then refuse against the retained v0.1.13 allowlist. -5. Only after Required succeeds for exact P, replace - `HRA_RELEASE_PUBLICATION_COMMIT_ALLOWLIST` with P in Production and Preview. - This allowlist update is the only environment write for v0.1.14. Redeploy - exact P, then read back its canonical Git SHA, complete build commands, - unchanged environment inventory and scoping, READY state, production routes, - release delivery, discovery, and OIDC endpoints before enabling or exercising - the installation handoff. -6. Preserve all predecessor and HRA backend authorities through installed-app - acceptance and the bounded rollback window. Their later disposition is a - separate handoff decision, not part of v0.1.14 publication. - -The existing annotated `v0.1.11` tag object -`e4c171e33e414d74a36791fc8577cbfbcef8e52e` points directly to -`5a2a9842cacc75fee42ab8e23ca8c215a643e21e`, but has no GitHub release or -assets. Treat it as retired tag-only evidence, never as publication or -installation authority. - -The immutable v0.1.13 build 14 prerelease remains the current published -predecessor and valid prior installed-app authority while v0.1.14 is a -candidate. Its direct annotated tag object -`44f00fd5c5e00bc8dcded0c9b176a8e37ada90f3` points to -`9ba06a441c9b12b448cfe34784432592dbeccb19`, and publication commit -`7825cb231890aa971f965412c31dfa2cb7796561` records the release evidence. Its -source handoff operator fails before installation because its hidden candidate -stage does not retain the required `.app` suffix. Do not retire, replace, -relabel, or use that source operator for cutover. - -### Convex project identity retirement - -Rename the Convex project only after candidate C, publication P, the HRA-only -Suite keyring readbacks, production route checks, and installed-app acceptance -have all passed. Keeping the project named OPRTE during those gates avoids -labeling the still-running predecessor functions and environment as HRA before -the actual authority cutover. The fresh HRA deploy key and retained predecessor -key both use the `prod:benevolent-akita-439|...` deployment identity, so the -later project-slug change does not alter either key's target or provide a reason -to shorten the rollback window. - -Bind every mutation to immutable IDs in its before-and-after evidence, and use -an ID-addressed endpoint wherever the Management API provides one. Deployment -and deploy-key retirement endpoints are name-addressed, so resolve each name to -the exact ID below immediately before mutation and require that exact ID to be -absent afterward: - -- Project `2680173` belongs to team `513923` / `cclrte`, initially has name and - slug `oprte`, and names `benevolent-akita-439` as its default Production - deployment. -- Production deployment `4677913` remains project `2680173`, cloud type - `prod`, default `true`, reference `production`, region `aws-us-east-1`, and - URL `https://benevolent-akita-439.convex.cloud`. -- Environment-variable **names only** and every deploy-key ID, name, - permission set, expiry, and last-used timestamp remain byte-for-byte equal. - Never print or export environment values or deploy-key values for this - comparison. - -Use `GET /v1/projects/2680173`, -`GET /v1/deployments/benevolent-akita-439`, and -`GET /v1/deployments/benevolent-akita-439/list_deploy_keys` for those filtered -Management API records. Capture the environment-name set without values: - -These HTTP literals are evidence contracts, not `curl` instructions. Perform -the readbacks and later mutations in the signed-in Convex dashboard while -checking the exact method, path, request body, and non-secret response fields, -or through a separately reviewed client that opens the mode-0600 personal-token -file itself and redacts foreign provider errors. Never put the bearer token in -argv, an environment variable, shell history, or terminal output. Stop if -neither safe path is available. - -```sh -bun x convex env list --names-only --deployment benevolent-akita-439 -``` - -Clear every slug-bound authority before renaming. Team Settings must still -show one active Admin member, no Authorized Applications for that member, no -other active member, and no custom roles. The authenticated Management API -readback -`GET /v1/projects/2680173/list_preview_deploy_keys?includeManaged=true` must -return an empty `items` array. These checks rule out custom-role selectors, -project OAuth tokens, and both manual and integration-managed preview keys that -would otherwise follow or break on an `oprte` slug change. Search operator -bookmarks and external automation for the old `cclrte:oprte` selector and -replace each confirmed consumer with `cclrte:hra`; HRA source itself selects -the deployment by name and requires no source change. - -Retire the stale staging deployment separately after the same acceptance gate. -Its current immutable identity is deployment `4828041`, name `cool-bear-228`, -project `2680173`, cloud type `prod`, non-default, reference `staging`. Its two -known broad keys, `vercel-oprte-staging-2026-08-10-c` and -`vercel-staging-2026-08-08`, have never been used. Before deletion, read back -that no provider, domain, integration, automation, data, file, or required -backup still depends on it. Preserve an explicit backup or defer retirement if -that evidence is not closed. Otherwise revoke both keys and delete only -deployment `4828041`. Require `GET /v1/deployments/cool-bear-228` to return not -found and `GET /v1/projects/2680173/list_deployments` to contain neither ID -`4828041` nor name `cool-bear-228`. Do not treat deletion of the shared -Production deployment as part of this cleanup. - -After staging retirement and the slug-bound-authority checks, send only -`PATCH /v1/projects/2680173` with the Management API body -`{"name":"HRA","slug":"hra"}`. -Read back the exact HRA name/slug, unchanged team and Production project/deploy -identities, unchanged Production URL, unchanged environment-name set, and -unchanged deploy-key metadata. The direct deployment dashboard URL and both -public Convex endpoints must remain reachable. This is an in-place label and -slug update, not a deployment transfer, data copy, environment rewrite, or key -rotation. - -During the bounded rollback window, send only -`PATCH /v1/projects/2680173` with `{"name":"oprte","slug":"oprte"}` if the -rename readback or a known external selector fails. Repeat the same -immutable-ID, Production URL, names-only environment, and key-metadata -comparison. After HRA acceptance and rollback disposition are final, revoke -the predecessor Production keys, detach -`oprte.com` and `hraness.kitchen` while retaining the domains in the account, -then delete the predecessor Vercel project. Deleting that project earlier would -destroy its READY deployment and environment rollback anchor. - -The deployed source owns a read-only internal audit. Its no-candidate form -returns only selector state, exact-key counts, and a closed status: - -```sh -bun x convex run suiteIdentityAudit:audit '{}' \ - --deployment benevolent-akita-439 -``` - -After both HRA and Accounts have been configured from the same mode-0600 -candidate file, verify HRA with the source-owned wrapper: - -```sh -bun run verify:receipt-provider -- \ - --secret-file /private/absolute/canonical/path/to/receipt-secret -``` - -The wrapper validates the file descriptor and permissions, sends only a fresh -random challenge to the internal audit, and compares its domain-separated HMAC -response locally with a timing-safe check. It accepts only -`keyCount=1`, `hraProductionV1Count=1`, `otherKeyCount=0`, `selectorV1=true`, -`status=ready`, and `candidateSecretMatch=true`. Its output contains only those -counts, status, and match bit. Run the Accounts counterpart against the same -candidate before deleting it; shape-only readback does not prove shared-secret -equality. -The file argument must use its exact native `realpath` spelling. In particular, -macOS temporary paths often print `/var/...` while the canonical path is -`/private/var/...`; pass the canonical result rather than the alias. - -Never install the HRA-only keyring until the candidate deployment in step 3 -has completed and its HRA parser is live. The predecessor backend may reject -the new shape, which would leave neither checked source nor a safe rollback -path in authority. - -The shared deployment may retain -`OPRTE_HOSTED_MUTATION_FINGERPRINT_KEY_CURRENT`, its exact `_VERSION`, and -`OPRTE_SESSION_SYNC_ENABLED` as compatibility inputs. HRA adopts the matching -HRA names only when absent or byte-for-byte equal, so do not rotate or delete -those values during this authority cutover. A production suite-link E2E also -requires separately provisioned WorkOS provider authority. If it is absent, -verify the parser, deployment readbacks, and unavailable response, and record -the WorkOS-backed E2E as an external gate rather than creating credentials or -claiming it passed. +## Archived production continuity + +HRA v0 stays available at `https://hra-weld.vercel.app` while current HRA uses +`https://hra.sh`. Keep the old deployment, data, credentials, storage +identifiers, release assets, and runtime protocol identities in place. Do not +copy any of them into current HRA. + +The immutable v0.1.14 build 15 publication has three fixed Git identities: + +- Candidate C is `7b39c459827b2acf45aa2d911c94fdb5d4f37860`. +- Publication P is `6221f79b745f154882080936b961ff431569f33e`. +- Annotated tag object `37ed37afb39cacfd6a51044cf7f3c1b873571aa3` + points directly to C. + +The seven-asset GitHub prerelease and P's release evidence are immutable. The +maintained `release-download.json` changes only its repository coordinate from +the historical `hraness/hra` name to `hraness/hra-v0`, where the same release +and assets live after the repository rename. Keep +`HRA_RELEASE_PUBLICATION_COMMIT_ALLOWLIST` fixed at P in Vercel Production and +Preview. Set `HRA_V0_SURFACE_COMMIT_ALLOWLIST` to the reviewed archive surface +commit Q after it reaches `main`. The build proves Q descends from P, replays C +to P and the annotated tag, and permits exactly one single-parent +historical-to-archive coordinate migration while requiring every other +contract byte to remain equal to P. A missing anchor, wrong repository, +unallowlisted surface, additional contract edit, or Production branch other +than `main` makes the build refuse. + +Rename the existing Vercel project in place from `hra` to `hra-v0`. Preserve +the existing project identity, READY deployment, and `hra-weld.vercel.app` +fallback. Connect it to `hraness/hra-v0`. Keep automatic Vercel system +variables enabled because the source gate requires Vercel's own target, Git +provider, repository, branch, and full commit SHA evidence. + +The public `/releases` page is generated from the checked root +`release-history.json` ledger. The credential-free remote gate verifies its +eight annotated tags, seven immutable releases, 49 assets, and v0.1.11 +tag-only state against GitHub after the repository rename. The public +`/.well-known/hra.json` marker binds archive generation 0 to the checked +numeric GitHub repository ID and final v0.1.14 publication identity for domain +cutover and rollback checks. + +Keep the public `NEXT_PUBLIC_CONVEX_URL` and +`NEXT_PUBLIC_CONVEX_SITE_URL` records scoped to Production and Preview with the +exact `benevolent-akita-439` URLs. Keep the Convex deploy key, +`SUITE_IDENTITY_RECEIPT_KEY_VERSION`, `SUITE_OIDC_COOKIE_SECRET`, +`NEXT_PUBLIC_SITE_URL=https://hra-weld.vercel.app`, and optional PostHog token +scoped to Production only. Preview must remain free of production capability. +Do not place Convex-only keyrings, session flags, hosted-mutation keys, taskctl +peppers, WorkOS webhook secrets, or local fixture settings in Vercel. + +The existing Convex project has immutable project ID `2680173`, team ID +`513923`, team slug `cclrte`, and current name and slug `HRA` / `hra`. Rename +that project in place to `HRA v0` / `hra-v0` before current HRA claims the clean +name. Preserve Production deployment ID `4677913`, name +`benevolent-akita-439`, reference `production`, region `aws-us-east-1`, and the +public `.convex.cloud` and `.convex.site` URLs. + +Bind the rename to the immutable project ID. The Management API operation is +`PATCH /v1/projects/2680173` with body +`{"name":"HRA v0","slug":"hra-v0"}`. Perform it in the signed-in Convex +dashboard or through a reviewed client that reads its personal token from a +mode-0600 file and redacts foreign errors. Never put that token in argv, an +environment variable, shell history, or terminal output. + +Before and after the rename, read only these non-secret records: + +- `GET /v1/projects/2680173`. +- `GET /v1/deployments/benevolent-akita-439`. +- `GET /v1/deployments/benevolent-akita-439/list_deploy_keys`. +- Environment variable names from + `bun x convex env list --names-only --deployment benevolent-akita-439`. + +Require unchanged team, project ID, Production deployment identity and URL, +environment-name set, and deploy-key IDs, names, permissions, expiry, and +last-used timestamps. Update confirmed operator selectors from `cclrte:hra` to +`cclrte:hra-v0`; source selects the deployment by name and needs no selector +change. Do not rotate keys, rewrite environment values, move data, or delete a +deployment as part of the rename. + +Verify the fallback root, `/download`, authentication callback, release asset +links, and security headers before removing `hra.sh` from the v0 Vercel +project. Update the external OIDC redirect allowlist to the fallback callback +before exercising authentication. Once current HRA has claimed the GitHub, +Vercel, Convex, and domain names, rollback must preserve the `hra-v0` names. +It must never evict the current product. ## Public product pages @@ -378,9 +181,9 @@ Linking requires both a live Hraness session and a currently authorized WorkOS h Product-local identity linking fails closed until the HRA runtimes are configured: -- The web host needs `NEXT_PUBLIC_SITE_URL=https://hra.sh`, a distinct `SUITE_OIDC_COOKIE_SECRET`, and `SUITE_IDENTITY_RECEIPT_KEY_VERSION`. Missing or mismatched values make the suite route return `503 SUITE_OIDC_UNAVAILABLE`. +- The web host needs `NEXT_PUBLIC_SITE_URL=https://hra-weld.vercel.app`, a distinct `SUITE_OIDC_COOKIE_SECRET`, and `SUITE_IDENTITY_RECEIPT_KEY_VERSION`. Missing or mismatched values make the suite route return `503 SUITE_OIDC_UNAVAILABLE`. - Deploy exact Suite Accounts v0.3.0 overlap support before changing either receipt keyring. Generate a fresh independent secret as the canonical unpadded base64url encoding of at least 32 random bytes. The HRA Convex `SUITE_IDENTITY_LINK_KEYS` keyring contains only the `hra:production:v1` entry. Accounts appends that same HRA entry alongside its unrelated existing entry. Never copy the unrelated Accounts key into HRA. Both environments use `SUITE_IDENTITY_RECEIPT_KEY_VERSION=v1`. -- This cutover admits exactly one HRA production/v1 key. Extra HRA versions, development entries, non-HRA entries, a selector other than `v1`, and malformed configuration make proof and receipt actions return `unavailable`. Rotate only through a later reviewed contract that restores bounded multi-key verification. No predecessor OPRTE/Kitchen receipt key is expected to exist, and the predecessor receipt authority being unavailable or invalid is not a rollout health gate. +- The archived deployment admits exactly one HRA production/v1 key. Extra HRA versions, development entries, non-HRA entries, a selector other than `v1`, and malformed configuration make proof and receipt actions return `unavailable`. Rotate only through a later reviewed contract that restores bounded multi-key verification. No predecessor OPRTE/Kitchen receipt key is expected to exist, and the predecessor receipt authority being unavailable or invalid is not a rollout health gate. - The ordinary hosted WorkOS and HRA Convex configuration described above must also be live, because suite linking reuses the existing WorkOS human and active organization-membership authorization rather than creating another product principal. WorkOS-to-Hraness identity linking remains unavailable until the web session diff --git a/apps/web/app/.well-known/hra.json/route.ts b/apps/web/app/.well-known/hra.json/route.ts new file mode 100644 index 0000000..a115f7f --- /dev/null +++ b/apps/web/app/.well-known/hra.json/route.ts @@ -0,0 +1,17 @@ +import { + HRA_DEPLOYMENT_IDENTITY, +} from "../../deployment-identity"; + +const body = `${JSON.stringify(HRA_DEPLOYMENT_IDENTITY, undefined, 2)}\n`; +const headers = { + "Cache-Control": "public, max-age=0, must-revalidate", + "Content-Type": "application/json; charset=utf-8", +} as const; + +export function GET(): Response { + return new Response(body, { headers, status: 200 }); +} + +export function HEAD(): Response { + return new Response(null, { headers, status: 200 }); +} diff --git a/apps/web/app/AGENTS.md b/apps/web/app/AGENTS.md index 20f8992..5911031 100644 --- a/apps/web/app/AGENTS.md +++ b/apps/web/app/AGENTS.md @@ -7,12 +7,14 @@ - `workos-configuration.ts` – one whitespace-safe WorkOS configuration predicate shared by proxy, auth routes, layout providers, and the `/app` configuration state. - `global-error.tsx`, `error.tsx`, `loading.tsx`, and `not-found.tsx` – shared, themed route-state boundaries, including the root-layout replacement. `loading.tsx` stays heading-free so streamed public HTML cannot grow a second H1. `not-found.tsx` owns a distinct noindex title and must not inherit the homepage canonical. - `standalone-theme-header.tsx` – the single final header appearance action shared by configuration, loading, missing-route, signed-out, and recoverable standalone states. -- `page.tsx` – canonical public landing page. It owns the homepage title, description, canonical `/`, indexable robots, matching social titles, product capabilities, boundaries, public-source entry points, and an honest analytics disclosure. +- `page.tsx` – canonical public HRA v0 archive landing page. It owns the homepage title, description, current-HRA handoff, canonical `/`, indexable robots, matching social titles, archived product boundaries, public-source entry points, and an honest analytics disclosure. - `accept-negotiation.ts` and `public-markdown.ts` – acceptmarkdown.com HTML/Markdown ranking, `llms.txt` body, and public-page Markdown representations used by the proxy. - `llms.txt/route.ts` – the root agent-instruction file with when-to-use guidance and links to the existing public pages. - `alternatives/` – public, dated, first-party-sourced comparisons that explain HRA's fit without inventing competitor limitations. - `app/page.tsx` – authenticated WorkOS/Convex control-plane entry and exact local missing-configuration states. - `download/` – public macOS prerelease status and source-build guidance. +- `releases/` and `release-history.ts` – the checked v0.1.7–v0.1.14 compatibility ledger, immutable release and asset links, and exact Git identities generated from the root release-history manifest. +- `.well-known/hra.json/` and `deployment-identity.ts` – the stable generation-0 repository and final-publication marker used during domain cutover verification. - `admin-shell.tsx` – authenticated human identity and agent lifecycle supervision inside the persistent rail, shared Hraness footer identity, suite-account status, final top-bar appearance action, and query-addressed Tasks/Access stages. - `suite-account-control.tsx` and `suite-account-protocol.ts` – same-origin central OIDC session control, strict receipt parsing, explicit WorkOS-human linking, and verified plan status. - `api/suite-auth/` – exact shared OIDC relying-party catch-all with encrypted server-only token custody and fail-closed configuration. @@ -30,9 +32,11 @@ - Keep route components focused on human supervision: work readiness, task state, agent identity, leases, and review. - Keep `/` public and indexable without mounting WorkOS or Convex. Keep `/app` behind the configured WorkOS proxy and preserve its server-rendered configuration failures before the authenticated control plane mounts. -- Keep `/download` honest about prerelease status. Link only to the exact public GitHub release contract in `site.ts`, disclose ad-hoc signing and missing notarization before the action, and never embed signing credentials or publication authority in the web app. +- Keep `/download` honest about final archived prerelease status. Link only to the exact public GitHub release contract in `site.ts`, disclose ad-hoc signing and missing notarization before the action, and never embed signing credentials or publication authority in the web app. +- Generate `/releases` from the checked root release-history manifest. Preserve every tag from v0.1.7 through v0.1.14, identify v0.1.11 as tag-only, and expose exact object IDs, byte counts, SHA-256 digests, and archive-repository links without treating an older package as current authority. +- Keep `/.well-known/hra.json` stable and public. Bind generation 0 to the checked numeric GitHub repository ID and final v0.1.14 publication; do not derive it from mutable request or provider state. - Keep `/alternatives` and its exact static child routes public. Date each review, cite current first-party sources, distinguish “not documented” from “absent,” and write product-specific analysis rather than name-swapped SEO pages. -- Limit analytics to one personless, cookieless `$pageview` on `/`, `/download`, `/alternatives`, and the exact comparison routes accepted by `isHraPublicComparisonPath`. Require exact Production `https://hra.sh`, use only the slim no-external SDK entry, redact query, fragment, referrer, identity, account, task, command, and provider context, and keep every other event and route inert. +- Limit analytics to one personless, cookieless `$pageview` on `/`, `/download`, `/alternatives`, and the exact comparison routes accepted by `isHraPublicComparisonPath`. Require exact Production `https://hra-weld.vercel.app` and site ID `hra-v0`, use only the slim no-external SDK entry, redact query, fragment, referrer, identity, account, task, command, and provider context, and keep every other event and route inert. - Build public and private discovery output with `@hraness/web-discovery`, while keeping HRA-owned origins, titles, descriptions, routes, dates, JSON-LD facts, and crawler choices in `site.ts` and the route that presents them. Keep homepage identity off the root layout. Keep HTML titles aligned with Open Graph titles. - Serve `Accept: text/markdown` on the existing public HTML documents from the proxy, with `Content-Type: text/markdown; charset=utf-8` and `Vary: Accept`. Return `406` only when a public document rejects every produced type. Keep `/llms.txt` as a dedicated Markdown file. Unknown public paths that prefer Markdown receive a real `404` whose body points at `/`, `/download`, `/alternatives`, `/llms.txt`, and `/sitemap.xml`. Do not invent API, OAuth, GraphQL, MCP, or commerce surfaces to satisfy agent scoring. - Consume task presentation and state from `@hraness/agent-tasks-ui`; keep every Convex, WorkOS, and generated-API import in hosted adapters. diff --git a/apps/web/app/alternatives/[slug]/page.tsx b/apps/web/app/alternatives/[slug]/page.tsx index 2b44f23..7cbd3b2 100644 --- a/apps/web/app/alternatives/[slug]/page.tsx +++ b/apps/web/app/alternatives/[slug]/page.tsx @@ -57,8 +57,8 @@ export async function generateMetadata({ params }: ComparisonPageProps): Promise return createPublicSiteMetadata({ ...hraSearchSite, description: comparison.description, - socialTitle: hraSocialPageTitle(`HRA vs ${comparison.shortName}`), - title: `HRA vs ${comparison.shortName}`, + socialTitle: hraSocialPageTitle(`HRA v0 vs ${comparison.shortName}`), + title: `HRA v0 vs ${comparison.shortName}`, }, { canonicalPath: `/alternatives/${comparison.slug}` }); } @@ -72,21 +72,21 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) { "@context": "https://schema.org", "@type": "WebPage", about: [ - { "@type": "SoftwareApplication", name: "HRA", url: hraSearchSite.origin }, + { "@type": "SoftwareApplication", name: "HRA v0", url: hraSearchSite.origin }, { "@type": "SoftwareApplication", name: comparison.name }, ], dateModified: COMPARISON_REVIEW_DATE, description: comparison.description, - name: `HRA vs ${comparison.shortName}`, + name: `HRA v0 vs ${comparison.shortName}`, url: comparisonUrl, }, { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [ - { "@type": "ListItem", item: hraSearchSite.origin, name: "HRA", position: 1 }, + { "@type": "ListItem", item: hraSearchSite.origin, name: "HRA v0", position: 1 }, { "@type": "ListItem", item: `${hraSearchSite.origin}/alternatives`, name: "Alternatives", position: 2 }, - { "@type": "ListItem", item: comparisonUrl, name: `HRA vs ${comparison.shortName}`, position: 3 }, + { "@type": "ListItem", item: comparisonUrl, name: `HRA v0 vs ${comparison.shortName}`, position: 3 }, ], }, ]; @@ -99,11 +99,11 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) { /> Skip to content
- + - HRA + HRA v0
- + {comparison.rows.map((row) => ( @@ -204,8 +204,8 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) {
-

What HRA cannot do yet

-

Current HRA limitations.

+

HRA v0 limits

+

Final archived limitations.

  • HRA's native host currently supports Apple Silicon Macs only.
  • @@ -225,7 +225,7 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) { {comparison.sources.map((source) =>
  • {source.label} ↗
  • )}

    - “Not documented” means only that a capability was not found in these sources on {COMPARISON_REVIEW_LABEL}. It does not prove the product lacks it. HRA is independent and unaffiliated with {comparison.name}. Report a correction. + “Not documented” means only that a capability was not found in these sources on {COMPARISON_REVIEW_LABEL}. It does not prove the product lacks it. HRA is independent and unaffiliated with {comparison.name}. Report a correction.

@@ -233,7 +233,7 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) {

More comparisons

{hraComparisons.filter((candidate) => candidate.slug !== comparison.slug).slice(0, 3).map((candidate) => ( - HRA vs {candidate.shortName} → + HRA v0 vs {candidate.shortName} → ))}
@@ -242,7 +242,7 @@ export default async function ComparisonPage({ params }: ComparisonPageProps) {
- All HRA comparisons + All HRA v0 comparisons
); diff --git a/apps/web/app/alternatives/comparisons.test.ts b/apps/web/app/alternatives/comparisons.test.ts index 39a62ee..66b1f80 100644 --- a/apps/web/app/alternatives/comparisons.test.ts +++ b/apps/web/app/alternatives/comparisons.test.ts @@ -32,11 +32,9 @@ describe("HRA comparison registry", () => { test("binds every HRA and alternative claim to current HTTPS sources", () => { const hraSourceIds = new Set(hraComparisonSources.map(({ id }) => id)); - const repositoryCitationVersion = HRA_RELEASE.availability === "published" - ? "0.1.14" - : "0.1.13"; + const repositoryCitationVersion = "0.1.14"; const versionedHraPrefix = - `https://github.com/hraness/hra/blob/v${repositoryCitationVersion}/` as const; + `https://github.com/hraness/hra-v0/blob/v${repositoryCitationVersion}/` as const; expect(hraComparisonCitationVersion({ availability: "candidate", diff --git a/apps/web/app/alternatives/comparisons.ts b/apps/web/app/alternatives/comparisons.ts index 7e3d3aa..126a25b 100644 --- a/apps/web/app/alternatives/comparisons.ts +++ b/apps/web/app/alternatives/comparisons.ts @@ -45,7 +45,7 @@ export function hraComparisonCitationVersion( const HRA_COMPARISON_CITATION_VERSION = hraComparisonCitationVersion(HRA_RELEASE); const HRA_VERSIONED_SOURCE_ROOT = - `https://github.com/hraness/hra/blob/v${HRA_COMPARISON_CITATION_VERSION}/` as const; + `https://github.com/hraness/hra-v0/blob/v${HRA_COMPARISON_CITATION_VERSION}/` as const; export const hraComparisonSources = [ { diff --git a/apps/web/app/alternatives/page.tsx b/apps/web/app/alternatives/page.tsx index a321d9e..ad65501 100644 --- a/apps/web/app/alternatives/page.tsx +++ b/apps/web/app/alternatives/page.tsx @@ -5,15 +5,20 @@ import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; -import { HRA_BRAND_ICON_PATH, hraSearchSite, hraSocialPageTitle } from "../site"; +import { + CURRENT_HRA_SITE, + HRA_BRAND_ICON_PATH, + hraSearchSite, + hraSocialPageTitle, +} from "../site"; import { COMPARISON_REVIEW_LABEL, hraComparisons } from "./comparisons"; export const metadata = createPublicSiteMetadata({ ...hraSearchSite, description: - "Compare HRA with Codex app, OpenCode Desktop, Paseo, Conductor, Superset, OpenChamber, and Happy Coder using current first-party sources.", - socialTitle: hraSocialPageTitle("HRA alternatives"), - title: "HRA alternatives", + "Archived HRA v0 comparisons with Codex app, OpenCode Desktop, Paseo, Conductor, Superset, OpenChamber, and Happy Coder.", + socialTitle: hraSocialPageTitle("HRA v0 alternatives"), + title: "HRA v0 alternatives", }, { canonicalPath: "/alternatives" }) satisfies Metadata; export default function AlternativesPage() { @@ -24,9 +29,9 @@ export default function AlternativesPage() { "@type": "ListItem", position: index + 1, url: `${hraSearchSite.origin}/alternatives/${comparison.slug}`, - name: `HRA vs ${comparison.shortName}`, + name: `HRA v0 vs ${comparison.shortName}`, })), - name: "HRA alternatives and comparisons", + name: "HRA v0 alternatives and comparisons", }; return ( @@ -37,17 +42,18 @@ export default function AlternativesPage() { /> Skip to content
- + - HRA + HRA v0
@@ -55,10 +61,10 @@ export default function AlternativesPage() {
-

HRA alternatives

+

Archived HRA v0 comparisons

Choose the layer you actually need.

- Coding-agent tools now overlap. Most can run work in parallel. The useful question is whether you need a first-party Codex app, a multi-provider workspace, a remote client, or a durable metaharness around Codex. + These pages preserve HRA v0's comparisons as reviewed on {COMPARISON_REVIEW_LABEL}. They describe the archived product, not the current HRA at hra.sh.

Sources last reviewed {COMPARISON_REVIEW_LABEL}.

@@ -72,7 +78,7 @@ export default function AlternativesPage() { {hraComparisons.map((comparison) => (

{comparison.shortName}

-

HRA vs {comparison.shortName}

+

HRA v0 vs {comparison.shortName}

{comparison.meaningfulDifference}

Read the comparison →
@@ -106,7 +112,7 @@ export default function AlternativesPage() { ); diff --git a/apps/web/app/analytics.property.test.ts b/apps/web/app/analytics.property.test.ts index 510a467..932182c 100644 --- a/apps/web/app/analytics.property.test.ts +++ b/apps/web/app/analytics.property.test.ts @@ -29,8 +29,8 @@ test("analytics classification never admits a route outside the public allowlist if (route !== null) { expect(route.canonical_path).toBe(canonicalPath); expect(route.analytics_schema_version).toBe(1); - expect(route.canonical_domain).toBe("hra.sh"); - expect(route.site_id).toBe("hra"); + expect(route.canonical_domain).toBe("hra-weld.vercel.app"); + expect(route.site_id).toBe("hra-v0"); } })); }); @@ -81,7 +81,7 @@ test("arbitrary provider properties cannot escape the pageview allowlist", () => for (const key of Object.keys(filtered?.properties ?? {})) { expect(allowedKeys.has(key), key).toBeTrue(); } - expect(filtered?.properties.$current_url).toBe("https://hra.sh/download"); + expect(filtered?.properties.$current_url).toBe("https://hra-weld.vercel.app/download"); expect(filtered?.properties.$pathname).toBe("/download"); expect(filtered?.properties.$process_person_profile).toBeFalse(); expect(filtered?.properties.distinct_id) diff --git a/apps/web/app/analytics.test.ts b/apps/web/app/analytics.test.ts index 27f93c3..75f50ef 100644 --- a/apps/web/app/analytics.test.ts +++ b/apps/web/app/analytics.test.ts @@ -20,10 +20,10 @@ describe("HRA analytics route boundary", () => { test("classifies only canonical public routes", () => { expect(classifyHraAnalyticsRoute("/")).toEqual({ analytics_schema_version: 1, - canonical_domain: "hra.sh", + canonical_domain: "hra-weld.vercel.app", canonical_path: "/", page_kind: "landing", - site_id: "hra", + site_id: "hra-v0", }); expect(classifyHraAnalyticsRoute("/download/")?.canonical_path).toBe("/download"); expect(classifyHraAnalyticsRoute("/alternatives/codex-app/")).toMatchObject({ @@ -38,6 +38,8 @@ describe("HRA analytics route boundary", () => { "/api/suite-auth/session", "/design", "/download/private", + "/releases", + "/.well-known/hra.json", "//", "///", "/download//", @@ -58,7 +60,7 @@ describe("HRA analytics route boundary", () => { "phc_publicproject", )).toBeTrue(); expect(isHraAnalyticsBrowserEligible( - { ...productionLanding, origin: "https://www.hra.sh" }, + { ...productionLanding, origin: "https://www.hra-weld.vercel.app" }, "phc_publicproject", )).toBeFalse(); expect(isHraAnalyticsBrowserEligible( @@ -98,7 +100,7 @@ describe("HRA PostHog pageviews", () => { $lib: "web", $lib_version: "test", $session_id: "session-id", - $current_url: "https://hra.sh/alternatives/codex-app?token=sensitive#private", + $current_url: "https://hra-weld.vercel.app/alternatives/codex-app?token=sensitive#private", $pathname: "/app", $referrer: "https://mail.example/private", $title: "Private task title", @@ -116,16 +118,16 @@ describe("HRA PostHog pageviews", () => { token: "phc_publicproject", distinct_id: "$posthog_cookieless", $cookieless_mode: true, - $current_url: "https://hra.sh/alternatives/codex-app", - $host: "hra.sh", + $current_url: "https://hra-weld.vercel.app/alternatives/codex-app", + $host: "hra-weld.vercel.app", $pathname: "/alternatives/codex-app", $process_person_profile: false, analytics_schema_version: 1, - canonical_domain: "hra.sh", + canonical_domain: "hra-weld.vercel.app", canonical_path: "/alternatives/codex-app", content_slug: "codex-app", page_kind: "alternative", - site_id: "hra", + site_id: "hra-v0", }, uuid: "00000000-0000-4000-8000-000000000000", }); @@ -143,7 +145,7 @@ describe("HRA PostHog pageviews", () => { }; for (const evidence of [ { ...productionLanding, pathname: "/app" }, - { ...productionLanding, origin: "https://www.hra.sh" }, + { ...productionLanding, origin: "https://www.hra-weld.vercel.app" }, { ...productionLanding, production: false }, ]) { expect(createHraPageviewFilter(() => evidence)(event)).toBeNull(); diff --git a/apps/web/app/analytics.ts b/apps/web/app/analytics.ts index a979d48..370c4b4 100644 --- a/apps/web/app/analytics.ts +++ b/apps/web/app/analytics.ts @@ -7,8 +7,8 @@ import type { import { isHraPublicComparisonPath } from "./alternatives/slugs"; export const HRA_ANALYTICS_SCHEMA_VERSION = 1 as const; -export const HRA_ANALYTICS_SITE_ID = "hra" as const; -export const HRA_ANALYTICS_CANONICAL_DOMAIN = "hra.sh" as const; +export const HRA_ANALYTICS_SITE_ID = "hra-v0" as const; +export const HRA_ANALYTICS_CANONICAL_DOMAIN = "hra-weld.vercel.app" as const; export const HRA_ANALYTICS_CANONICAL_ORIGIN = `https://${HRA_ANALYTICS_CANONICAL_DOMAIN}` as const; export const HRA_POSTHOG_INGESTION_HOST = "https://us.i.posthog.com" as const; diff --git a/apps/web/app/api/suite-auth/relying-party.test.ts b/apps/web/app/api/suite-auth/relying-party.test.ts index 3f82156..ae8e54b 100644 --- a/apps/web/app/api/suite-auth/relying-party.test.ts +++ b/apps/web/app/api/suite-auth/relying-party.test.ts @@ -24,7 +24,7 @@ describe("HRA suite OIDC relying party", () => { for (const key of ENVIRONMENT_KEYS) delete process.env[key]; const response = await handleHRASuiteOidc( - new Request("https://hra.sh/api/suite-auth/session"), + new Request("https://hra-weld.vercel.app/api/suite-auth/session"), ); expect(response.status).toBe(503); @@ -40,12 +40,12 @@ describe("HRA suite OIDC relying party", () => { }); test("accepts only the registered HRA origin before exact route dispatch", async () => { - process.env.NEXT_PUBLIC_SITE_URL = "https://hra.sh"; + process.env.NEXT_PUBLIC_SITE_URL = "https://hra-weld.vercel.app"; process.env.SUITE_IDENTITY_RECEIPT_KEY_VERSION = "v1"; process.env.SUITE_OIDC_COOKIE_SECRET = "c".repeat(64); const response = await handleHRASuiteOidc( - new Request("https://hra.sh/api/suite-auth/not-a-route"), + new Request("https://hra-weld.vercel.app/api/suite-auth/not-a-route"), ); expect(response.status).toBe(404); diff --git a/apps/web/app/deployment-identity.ts b/apps/web/app/deployment-identity.ts new file mode 100644 index 0000000..faa5730 --- /dev/null +++ b/apps/web/app/deployment-identity.ts @@ -0,0 +1,55 @@ +import { HRA_RELEASE_HISTORY } from "./release-history"; +import { HRA_RELEASE } from "./site"; + +const finalHistoryEntry = HRA_RELEASE_HISTORY.tags.at(-1); +if ( + finalHistoryEntry === undefined + || finalHistoryEntry.release === null + || finalHistoryEntry.tag !== HRA_RELEASE.tag + || finalHistoryEntry.build !== HRA_RELEASE.build + || finalHistoryEntry.commit !== HRA_RELEASE.source.commit + || finalHistoryEntry.tagObject !== HRA_RELEASE.source.tagObject +) { + throw new Error("The archive deployment identity differs from the final release history."); +} +const finalDmg = finalHistoryEntry.release.assets.find(({ name }) => + name === HRA_RELEASE.asset +); +if (finalDmg === undefined || finalDmg.sha256 !== HRA_RELEASE.sha256) { + throw new Error("The archive deployment identity differs from the final DMG evidence."); +} + +export const HRA_DEPLOYMENT_IDENTITY_PATH = "/.well-known/hra.json" as const; +const providerSourceCommit = process.env.VERCEL_GIT_COMMIT_SHA; +if ( + process.env.VERCEL === "1" + && (providerSourceCommit === undefined || !/^[0-9a-f]{40}$/u.test(providerSourceCommit)) +) { + throw new Error("The archive deployment requires an exact source commit marker."); +} +const deploymentSourceCommit = providerSourceCommit ?? "local"; +if (deploymentSourceCommit !== "local" && !/^[0-9a-f]{40}$/u.test(deploymentSourceCommit)) { + throw new Error("The archive deployment source commit marker is invalid."); +} +export const HRA_DEPLOYMENT_IDENTITY = Object.freeze({ + generation: HRA_RELEASE_HISTORY.generation, + product: "HRA", + publication: Object.freeze({ + build: finalHistoryEntry.build, + dmgSha256: finalDmg.sha256, + publicationCommit: HRA_RELEASE_HISTORY.publicationCommit, + releaseId: finalHistoryEntry.release.id, + sourceCommit: finalHistoryEntry.commit, + tag: finalHistoryEntry.tag, + tagObject: finalHistoryEntry.tagObject, + version: finalHistoryEntry.version, + }), + repository: Object.freeze({ + id: HRA_RELEASE_HISTORY.repositoryId, + path: "hraness/hra-v0", + }), + schemaVersion: 2, + source: Object.freeze({ + commit: deploymentSourceCommit, + }), +}); diff --git a/apps/web/app/download/page.tsx b/apps/web/app/download/page.tsx index ae87be3..ee9b98b 100644 --- a/apps/web/app/download/page.tsx +++ b/apps/web/app/download/page.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; import Image from "next/image"; import { + CURRENT_HRA_SITE, HRA_BRAND_ICON_PATH, HRA_RELEASE, HRA_RELEASE_CHECKSUM_URL, @@ -20,28 +21,23 @@ import { export const metadata = createPublicSiteMetadata({ ...hraSearchSite, description: - "Download the HRA Apple Silicon prerelease, verify its checksum, or build it from public source.", - socialTitle: hraSocialPageTitle("HRA for macOS"), - title: "HRA for macOS", + "Download the archived HRA v0 Apple Silicon prerelease, verify its checksum, or build it from public source.", + socialTitle: hraSocialPageTitle("HRA v0 for macOS"), + title: "HRA v0 for macOS", }, { canonicalPath: "/download" }) satisfies Metadata; export default function DownloadPage() { - const publishedUrls = HRA_RELEASE_URL !== null - && HRA_RELEASE_CHECKSUM_URL !== null - && HRA_RELEASE_MANIFEST_URL !== null - ? { - checksum: HRA_RELEASE_CHECKSUM_URL, - dmg: HRA_RELEASE_URL, - manifest: HRA_RELEASE_MANIFEST_URL, - } - : null; - const published = publishedUrls !== null; + const publishedUrls = { + checksum: HRA_RELEASE_CHECKSUM_URL, + dmg: HRA_RELEASE_URL, + manifest: HRA_RELEASE_MANIFEST_URL, + } as const; return (
Skip to content
- + - HRA + HRA v0
- - Open control plane + + Release history + + Open current HRA +
@@ -66,42 +65,32 @@ export default function DownloadPage() {

-

Download HRA for your Mac.

+

Download HRA v0 for your Mac.

- The native prerelease bundles HRA, Codex, and Git for Apple Silicon Macs running macOS {HRA_RELEASE.minimumMacOS} or newer. + This is the final archived v0.1.14 prerelease. It bundles HRA v0, Codex, and Git for Apple Silicon Macs running macOS {HRA_RELEASE.minimumMacOS} or newer. New users should start with the current HRA.

- Unknown developer. This candidate uses an ad-hoc code seal, but it is not Developer ID signed or notarized by Apple. {published - ? "The published SHA-256 verifies the exact release bytes; macOS will still ask you to approve the app manually." - : "Its release commit, tag, manifest, and artifact hashes are still awaiting publication."} + Unknown developer. This archived release uses an ad-hoc code seal, but it is not Developer ID signed or notarized by Apple. The published SHA-256 verifies the exact release bytes; macOS will still ask you to approve the app manually.

- {publishedUrls !== null - ? ( - - Download the DMG - - ) - : ( -

- Candidate verification in progress. Do not install an unpublished draft asset. -

- )} + + Download the DMG +

Version {HRA_RELEASE.version} ({HRA_RELEASE.build}) · Apple Silicon · macOS {HRA_RELEASE.minimumMacOS}+

-
- {publishedUrls !== null ?
    +
    1. @@ -173,16 +160,7 @@ export default function DownloadPage() {

      Control-click HRA in Finder and choose Open. If macOS still blocks it, use System Settings → Privacy & Security → Open Anyway.

    2. -
    : ( -
    -

    - HRA {HRA_RELEASE.version} ({HRA_RELEASE.build}) is a checked source candidate. Do not drag a second app beside an installed OPRTE predecessor, and do not install an unpublished draft asset. -

    -

    - You can inspect or build the candidate from the canonical HRA repository while release evidence is completed. -

    -
    - )} +

HRA can run coding agents with local filesystem and process authority. Pair only repositories and Codex accounts you intend it to use.

@@ -191,25 +169,26 @@ export default function DownloadPage() {

Build it yourself

-

Build HRA from source.

+

Build HRA v0 from source.

The public repository pins Bun, Zig, Codex, Git, native build inputs, and the package verifier. Build the same app locally if the ad-hoc release boundary is not right for you.

- Read the build instructions → + Read the build instructions →
-

What remains

-

Developer ID signing is not available yet.

+

Distribution boundary

+

Developer ID signing is not available for HRA v0.

- A later release needs a Developer ID certificate and Apple notarization before normal double-click installation can replace the unknown-developer flow. Automatic updates remain disabled until HRA owns a signed update channel. + HRA v0 is archived and will not gain a new update channel. The current HRA continues separately at hra.sh.

diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index eb7788b..7260093 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -2360,6 +2360,188 @@ code { text-underline-offset: 0.3rem; } +.release-history-hero h1 { + max-width: 15ch; +} + +.release-history-list { + display: grid; + gap: 0; + padding-block: clamp(3rem, 7vw, 6rem); +} + +.release-history-entry { + padding: clamp(1.5rem, 4vw, 3rem) 0; + border-top: 1px solid var(--line); +} + +.release-history-entry:last-child { + border-bottom: 1px solid var(--line); +} + +.release-history-entry--final { + margin-inline: calc(-1 * clamp(1rem, 3vw, 2rem)); + padding-inline: clamp(1rem, 3vw, 2rem); + border: 1px solid color-mix(in oklch, var(--accent-ink) 38%, var(--line)); + background: color-mix(in oklch, var(--accent) 7%, var(--surface)); +} + +.release-history-entry > header { + display: flex; + align-items: start; + justify-content: space-between; + gap: 1.5rem; +} + +.release-history-entry h2 { + margin: 0.45rem 0 0; + font-family: var(--editorial-serif); + font-size: clamp(2rem, 4vw, 3.6rem); + font-weight: 500; + letter-spacing: -0.04em; +} + +.release-history-entry h2 span { + color: var(--faint); + font-family: var(--mono); + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.04em; + vertical-align: middle; +} + +.release-history-state { + flex: 0 0 auto; + padding: 0.45rem 0.7rem; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--accent-ink); + font-family: var(--mono); + font-size: 0.64rem; + font-weight: 700; + text-decoration: none; +} + +.release-history-state--tag { + color: var(--faint); +} + +.release-history-git { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 1.6rem 0 0; + border-top: 1px solid var(--line); +} + +.release-history-git > div { + min-width: 0; + padding: 1rem 1rem 1rem 0; + border-bottom: 1px solid var(--line); +} + +.release-history-git dt { + color: var(--faint); + font-family: var(--mono); + font-size: 0.63rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.release-history-git dd { + margin: 0.5rem 0 0; + overflow-x: auto; + color: var(--muted); + font-size: 0.75rem; + white-space: nowrap; +} + +.release-history-git a { + color: inherit; + text-underline-offset: 0.2rem; +} + +.release-history-tag-only { + max-width: 52rem; + margin: 1.4rem 0 0; + color: var(--muted); + line-height: 1.7; +} + +.release-history-assets { + margin-top: 1.4rem; +} + +.release-history-assets summary { + width: fit-content; + cursor: pointer; + color: var(--text); + font-size: 0.76rem; + font-weight: 700; +} + +.release-history-asset-scroll { + margin-top: 1.1rem; + overflow-x: auto; +} + +.release-history-assets table { + width: 100%; + min-width: 68rem; + border-collapse: collapse; + font-size: 0.7rem; + line-height: 1.5; +} + +.release-history-assets th, +.release-history-assets td { + padding: 0.8rem; + border: 1px solid var(--line); + text-align: left; + vertical-align: top; +} + +.release-history-assets thead th { + background: var(--surface-raised); + color: var(--faint); + font-family: var(--mono); + font-size: 0.61rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.release-history-assets tbody th { + width: 32%; + font-weight: 650; +} + +.release-history-assets tbody th a { + color: var(--text); + overflow-wrap: anywhere; + text-underline-offset: 0.2rem; +} + +.release-history-assets tbody td:nth-child(2) { + width: 19%; + color: var(--muted); + white-space: nowrap; +} + +.release-history-assets tbody td:nth-child(2) span { + display: block; + color: var(--faint); +} + +.release-history-assets tbody td:last-child code { + font-size: 0.66rem; + overflow-wrap: anywhere; +} + +.release-history-method p:last-child { + margin: 0; + color: var(--muted); + line-height: 1.75; +} + .global-notice { position: fixed; z-index: 50; @@ -2447,10 +2629,16 @@ code { .alternatives-grid, .comparison-short-answer > div:last-child, - .alternatives-method { + .alternatives-method, + .release-history-git { grid-template-columns: 1fr; } + .release-history-entry > header { + align-items: flex-start; + flex-direction: column; + } + .alternatives-grid article { min-height: 18rem; } diff --git a/apps/web/app/landing-discovery.test.ts b/apps/web/app/landing-discovery.test.ts index c9ffb5e..e72cb1b 100644 --- a/apps/web/app/landing-discovery.test.ts +++ b/apps/web/app/landing-discovery.test.ts @@ -10,6 +10,7 @@ import { generateMetadata as comparisonMetadata } from "./alternatives/[slug]/pa import { metadata as alternativesMetadata } from "./alternatives/page"; import { hraComparisons } from "./alternatives/comparisons"; import { metadata as downloadMetadata } from "./download/page"; +import { metadata as releaseHistoryMetadata } from "./releases/page"; import { metadata as notFoundMetadata } from "./not-found"; import OpenGraphImage from "./opengraph-image"; import { metadata as homepageMetadata } from "./page"; @@ -26,20 +27,20 @@ describe("HRA public discovery contract", () => { test("publishes canonical social metadata for the public product", () => { const product = createPublicSiteMetadata(hraSearchSite); expect(product).toMatchObject({ - alternates: { canonical: "https://hra.sh/" }, - applicationName: "HRA", + alternates: { canonical: "https://hra-weld.vercel.app/" }, + applicationName: "HRA v0", description: hraSearchSite.description, openGraph: { images: [{ - alt: "HRA: a durable metaharness for Codex", + alt: "HRA v0: the archived Codex metaharness", height: 630, - url: "https://hra.sh/opengraph-image", + url: "https://hra-weld.vercel.app/opengraph-image", width: 1200, }], - siteName: "HRA", + siteName: "HRA v0", title: hraSearchSite.title, type: "website", - url: "https://hra.sh/", + url: "https://hra-weld.vercel.app/", }, robots: { follow: true, index: true }, title: { default: hraSearchSite.title, template: hraSearchSite.titleTemplate }, @@ -51,11 +52,11 @@ describe("HRA public discovery contract", () => { test("keeps only inheritable site-wide defaults on the root layout", () => { expect(hraRootMetadata).toMatchObject({ - applicationName: "HRA", + applicationName: "HRA v0", creator: "Hraness", publisher: "Hraness", - title: { default: "HRA", template: "%s · HRA" }, - openGraph: { siteName: "HRA", type: "website" }, + title: { default: "HRA v0", template: "%s · HRA v0" }, + openGraph: { siteName: "HRA v0", type: "website" }, }); expect(hraRootMetadata).not.toHaveProperty("description"); expect(hraRootMetadata).not.toHaveProperty("robots"); @@ -70,30 +71,35 @@ describe("HRA public discovery contract", () => { test("lets the homepage own the indexable product identity", () => { expect(homepageMetadata).toMatchObject({ - alternates: { canonical: "https://hra.sh/" }, + alternates: { canonical: "https://hra-weld.vercel.app/" }, description: hraSearchSite.description, robots: { follow: true, index: true }, title: { absolute: hraSearchSite.title }, }); expect(homepageMetadata.openGraph?.title).toBe(hraSearchSite.title); expect(homepageMetadata.twitter?.title).toBe(hraSearchSite.title); - expect(homepageMetadata.openGraph?.url).toBe("https://hra.sh/"); + expect(homepageMetadata.openGraph?.url).toBe("https://hra-weld.vercel.app/"); }); test("keeps HTML titles aligned with Open Graph titles", () => { - expect(hraSocialPageTitle("HRA for macOS")).toBe("HRA for macOS · HRA"); + expect(hraSocialPageTitle("HRA v0 for macOS")).toBe("HRA v0 for macOS · HRA v0"); expect(downloadMetadata.title).toEqual({ - default: "HRA for macOS", - template: "%s · HRA", + default: "HRA v0 for macOS", + template: "%s · HRA v0", }); - expect(downloadMetadata.openGraph?.title).toBe("HRA for macOS · HRA"); - expect(downloadMetadata.twitter?.title).toBe("HRA for macOS · HRA"); + expect(downloadMetadata.openGraph?.title).toBe("HRA v0 for macOS · HRA v0"); + expect(downloadMetadata.twitter?.title).toBe("HRA v0 for macOS · HRA v0"); + expect(releaseHistoryMetadata.title).toEqual({ + default: "HRA v0 release history", + template: "%s · HRA v0", + }); + expect(releaseHistoryMetadata.openGraph?.title).toBe("HRA v0 release history · HRA v0"); expect(alternativesMetadata.title).toEqual({ - default: "HRA alternatives", - template: "%s · HRA", + default: "HRA v0 alternatives", + template: "%s · HRA v0", }); - expect(alternativesMetadata.openGraph?.title).toBe("HRA alternatives · HRA"); - expect(alternativesMetadata.twitter?.title).toBe("HRA alternatives · HRA"); + expect(alternativesMetadata.openGraph?.title).toBe("HRA v0 alternatives · HRA v0"); + expect(alternativesMetadata.twitter?.title).toBe("HRA v0 alternatives · HRA v0"); }); test("keeps comparison titles aligned with Open Graph titles", async () => { @@ -101,12 +107,12 @@ describe("HRA public discovery contract", () => { params: Promise.resolve({ slug: "codex-app" }), }); expect(metadata.title).toEqual({ - default: "HRA vs Codex app", - template: "%s · HRA", + default: "HRA v0 vs Codex app", + template: "%s · HRA v0", }); - expect(metadata.openGraph?.title).toBe("HRA vs Codex app · HRA"); - expect(metadata.twitter?.title).toBe("HRA vs Codex app · HRA"); - expect(metadata.alternates?.canonical).toBe("https://hra.sh/alternatives/codex-app"); + expect(metadata.openGraph?.title).toBe("HRA v0 vs Codex app · HRA v0"); + expect(metadata.twitter?.title).toBe("HRA v0 vs Codex app · HRA v0"); + expect(metadata.alternates?.canonical).toBe("https://hra-weld.vercel.app/alternatives/codex-app"); }); test("gives unmatched routes a distinct noindex page", () => { @@ -122,49 +128,54 @@ describe("HRA public discovery contract", () => { }, index: false, }, - title: { absolute: "Not found · HRA" }, + title: { absolute: "Not found · HRA v0" }, }); expect(notFoundMetadata).not.toHaveProperty("alternates"); expect(notFoundMetadata).not.toHaveProperty("openGraph"); }); test("keeps crawler files on the existing public product surface", () => { - expect(HRA_LLMS_TXT).toContain("When to use this:"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/sitemap.xml"); - expect(robots().sitemap).toBe("https://hra.sh/sitemap.xml"); - expect(sitemap().some((entry) => entry.url === "https://hra.sh/llms.txt")).toBeFalse(); + expect(HRA_LLMS_TXT).toContain("maintained archive for HRA v0"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/sitemap.xml"); + expect(robots().sitemap).toBe("https://hra-weld.vercel.app/sitemap.xml"); + expect(sitemap().some((entry) => entry.url === "https://hra-weld.vercel.app/llms.txt")).toBeFalse(); }); test("indexes the public product, download, and sourced comparison surfaces", () => { expect(robots()).toEqual({ - host: "https://hra.sh", + host: "https://hra-weld.vercel.app", rules: { allow: "/", disallow: ["/api", "/app", "/auth", "/design"], userAgent: "*", }, - sitemap: "https://hra.sh/sitemap.xml", + sitemap: "https://hra-weld.vercel.app/sitemap.xml", }); expect(sitemap()).toEqual([ { changeFrequency: "weekly", priority: 1, - url: "https://hra.sh/", + url: "https://hra-weld.vercel.app/", }, { changeFrequency: "weekly", priority: 0.8, - url: "https://hra.sh/download", + url: "https://hra-weld.vercel.app/download", + }, + { + changeFrequency: "monthly", + priority: 0.8, + url: "https://hra-weld.vercel.app/releases", }, { changeFrequency: "monthly", priority: 0.8, - url: "https://hra.sh/alternatives", + url: "https://hra-weld.vercel.app/alternatives", }, ...hraComparisons.map(({ slug }) => ({ changeFrequency: "monthly" as const, priority: 0.7, - url: `https://hra.sh/alternatives/${slug}`, + url: `https://hra-weld.vercel.app/alternatives/${slug}`, })), ]); }); @@ -177,8 +188,8 @@ describe("HRA public discovery contract", () => { }); expect(website).toMatchObject({ "@type": "WebSite", - name: "HRA", - url: "https://hra.sh/", + name: "HRA v0", + url: "https://hra-weld.vercel.app/", }); expect(application).toMatchObject({ "@type": "WebApplication", @@ -191,10 +202,10 @@ describe("HRA public discovery contract", () => { test("pins the generated social card dimensions and copy", async () => { const image = await Bun.file(new URL("./opengraph-image.tsx", import.meta.url)).text(); - expect(image).toContain('export const alt = "HRA: a durable metaharness for Codex"'); + expect(image).toContain('export const alt = "HRA v0: archived Codex metaharness"'); expect(image).toContain("height: 630, width: 1200"); - expect(image).toContain("Give Codex a team, a memory, and a budget."); - expect(image).toContain("Authorized accounts · Durable delegation · Recoverable work"); + expect(image).toContain("HRA v0 is preserved here."); + expect(image).toContain("Final v0.1.14 prerelease · Public source · Preserved history"); }); test("renders the local phoenix into the full-size social card", async () => { diff --git a/apps/web/app/landing-routing.test.ts b/apps/web/app/landing-routing.test.ts index b9973dd..e022af1 100644 --- a/apps/web/app/landing-routing.test.ts +++ b/apps/web/app/landing-routing.test.ts @@ -13,8 +13,10 @@ describe("HRA public and control-plane route boundary", () => { "/alternatives", "/alternatives/codex-app", "/download", + "/.well-known/hra.json", "/llms.txt", "/opengraph-image", + "/releases", "/robots.txt", "/sitemap.xml", ]) { @@ -27,6 +29,8 @@ describe("HRA public and control-plane route boundary", () => { "/auth/sign-in", "/design", "/download/private", + "/.well-known/hra.json/private", + "/releases/private", "/alternative", "/alternatives/missing", ]) { @@ -91,7 +95,7 @@ describe("HRA public and control-plane route boundary", () => { }); test("returns auth and every internal control-plane link to app", async () => { - const [callback, signIn, signUp, shell, suiteAccount, download, notFound] = await Promise.all([ + const [callback, signIn, signUp, shell, suiteAccount, download, notFound, releases] = await Promise.all([ source("./auth/callback/route.ts"), source("./auth/sign-in/route.ts"), source("./auth/sign-up/route.ts"), @@ -99,6 +103,7 @@ describe("HRA public and control-plane route boundary", () => { source("./suite-account-control.tsx"), source("./download/page.tsx"), source("./not-found.tsx"), + source("./releases/page.tsx"), ]); expect(callback).toContain('returnPathname: "/app"'); @@ -109,7 +114,8 @@ describe("HRA public and control-plane route boundary", () => { expect(shell).toContain('window.location.replace("/app")'); expect(shell).toContain('signOut({ returnTo: "/" })'); expect(suiteAccount).toContain('href="/api/suite-auth/start?return_to=/app"'); - expect(download).toContain('className="download-control-plane-link" href="/app"'); + expect(download).toContain('className="download-control-plane-link" href={CURRENT_HRA_SITE}'); + expect(releases).toContain("HRA_RELEASE_HISTORY.tags.toReversed()"); expect(notFound).toContain('href="/app" variant="primary">Open control plane'); }); }); diff --git a/apps/web/app/landing.test.ts b/apps/web/app/landing.test.ts index 9240aee..7d5060f 100644 --- a/apps/web/app/landing.test.ts +++ b/apps/web/app/landing.test.ts @@ -41,8 +41,8 @@ describe("HRA public landing", () => { for (const brandedSurface of [page, download, adminShell]) { expect(brandedSurface).not.toContain("{HRA_BRAND_EMOJI}"); } - expect(page).toContain('aria-label="HRA home"'); - expect(download).toContain('aria-label="HRA download"'); + expect(page).toContain('aria-label="HRA v0 home"'); + expect(download).toContain('aria-label="HRA v0 download"'); expect(adminShell).not.toContain('className="brand-mark" aria-hidden="true">OP'); expect(await sha256("./icon.png")).toBe( "17f58b8c253691f5302d5a742f540e04e7b8105bad1032cd1f1320a9388029e1", @@ -58,16 +58,16 @@ describe("HRA public landing", () => { test("leads with the outcome and exposes the complete public decision path", async () => { const page = await source("./page.tsx"); - expect(page).toContain("A metaharness for Codex"); - expect(page).toContain("Give Codex a team, a memory, and a budget."); - expect(page).toContain("Codex accounts you already use"); + expect(page).toContain("Archived HRA v0 · final prerelease"); + expect(page).toContain("HRA v0 is preserved here."); + expect(page).toContain("The current HRA is a separate project"); expect(page).toContain("Delegate work with structure"); expect(page).toContain("Spend reasoning deliberately"); expect(page).toContain("Recover the work, not just the window"); expect(page.match(/Download for macOS'); - expect(page).toContain('href="/alternatives">Compare HRA'); - expect(page).toContain('href="https://github.com/hraness/hra"'); + expect(page).toContain('href="/download">Download HRA v0'); + expect(page).toContain("href={CURRENT_HRA_SITE}>Go to current HRA"); + expect(page).toContain("href={HRA_V0_REPOSITORY}"); expect(page).toContain("Let the Mac keep the authority."); expect(page).toContain("HRA is intentionally narrower than an AI IDE."); expect(page).toContain("A provider limit ends the affected turn."); @@ -85,37 +85,27 @@ describe("HRA public landing", () => { checksumAsset: "HRA-0.1.14-15-macos-arm64.dmg.sha256", manifestAsset: "HRA-0.1.14-15-release-manifest.json", minimumMacOS: "13", - repository: "https://github.com/hraness/hra", + historicalPublicationRepository: "https://github.com/hraness/hra", + repository: "https://github.com/hraness/hra-v0", tag: "v0.1.14", version: "0.1.14", }); - if (HRA_RELEASE.availability === "candidate") { - expect(HRA_RELEASE.source).toEqual({ - commit: null, - runtimeTreeSha256: null, - tagObject: null, - }); - expect(HRA_RELEASE_URL).toBeNull(); - expect(HRA_RELEASE_CHECKSUM_URL).toBeNull(); - expect(HRA_RELEASE_MANIFEST_URL).toBeNull(); - } else { - expect(HRA_RELEASE.source.commit).toMatch(/^[0-9a-f]{40}$/u); - expect(HRA_RELEASE.source.runtimeTreeSha256).toMatch(/^[0-9a-f]{64}$/u); - expect(HRA_RELEASE.source.tagObject).toMatch(/^[0-9a-f]{40}$/u); - expect(HRA_RELEASE_URL).toBe( - "https://github.com/hraness/hra/releases/download/v0.1.14/HRA-0.1.14-15-macos-arm64.dmg", - ); - expect(HRA_RELEASE_CHECKSUM_URL).toBe(`${HRA_RELEASE_URL}.sha256`); - expect(HRA_RELEASE_MANIFEST_URL).toBe( - "https://github.com/hraness/hra/releases/download/v0.1.14/HRA-0.1.14-15-release-manifest.json", - ); - } + expect(HRA_RELEASE.availability).toBe("published"); + expect(HRA_RELEASE.source.commit).toMatch(/^[0-9a-f]{40}$/u); + expect(HRA_RELEASE.source.runtimeTreeSha256).toMatch(/^[0-9a-f]{64}$/u); + expect(HRA_RELEASE.source.tagObject).toMatch(/^[0-9a-f]{40}$/u); + expect(HRA_RELEASE_URL).toBe( + "https://github.com/hraness/hra-v0/releases/download/v0.1.14/HRA-0.1.14-15-macos-arm64.dmg", + ); + expect(HRA_RELEASE_CHECKSUM_URL).toBe(`${HRA_RELEASE_URL}.sha256`); + expect(HRA_RELEASE_MANIFEST_URL).toBe( + "https://github.com/hraness/hra-v0/releases/download/v0.1.14/HRA-0.1.14-15-release-manifest.json", + ); expect(download).toContain("Unknown developer."); expect(download).toContain("not Developer ID signed or notarized"); expect(download).toContain("HRA_RELEASE_MANIFEST_URL"); - expect(download).toContain("Candidate verification in progress."); - expect(download).toContain("Do not install an unpublished draft asset."); - expect(download).toContain("Do not drag a second app beside an installed OPRTE predecessor"); + expect(download).not.toContain("Candidate verification in progress."); + expect(download).not.toContain("Do not install an unpublished draft asset."); }); test("keeps navigation, sections, disclosure, and structured data semantic", async () => { @@ -151,18 +141,18 @@ describe("HRA public landing", () => { test("positions the repository around concrete outcomes and boundaries", async () => { const readme = await source("../../../README.md"); - expect(readme).toContain("# HRA"); - expect(readme).toContain("**A metaharness for Codex.**"); + expect(readme).toContain("# HRA v0"); + expect(readme).toContain("**The original metaharness for Codex.**"); expect(readme).toContain("one durable system for planning work, delegating it, running it in parallel"); - expect(readme).toContain("[Website](https://hra.sh)"); - expect(readme).toContain("[![HRA](https://hra.sh/opengraph-image)](https://hra.sh)"); - expect(readme).toContain("[Download for macOS](https://hra.sh/download)"); - expect(readme).toContain("[Compare HRA](https://hra.sh/alternatives)"); - expect(readme).toContain("[Open HRA](https://hra.sh/app)"); + expect(readme).toContain("[Website](https://hra-weld.vercel.app)"); + expect(readme).toContain("[![HRA](https://hra-weld.vercel.app/opengraph-image)](https://hra-weld.vercel.app)"); + expect(readme).toContain("[Download for macOS](https://hra-weld.vercel.app/download)"); + expect(readme).toContain("[Historical comparisons](https://hra-weld.vercel.app/alternatives)"); + expect(readme).toContain("[Current HRA](https://hra.sh)"); expect(readme).toContain("## Why HRA exists"); expect(readme).toContain("Several authorized accounts, kept separate."); expect(readme).toContain("HRA does not combine subscriptions or bypass provider limits."); expect(readme).toContain("See [Security architecture](SECURITY_ARCHITECTURE.md)"); - expect(readme).toContain("HRA is under active development."); + expect(readme).toContain("HRA v0 is archived."); }); }); diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 06df075..10c4e75 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -10,7 +10,7 @@ import { StandaloneThemeHeader } from "./standalone-theme-header"; export const metadata = { description: "This page does not exist.", robots: NOINDEX_ROBOTS, - title: { absolute: "Not found · HRA" }, + title: { absolute: "Not found · HRA v0" }, } satisfies Metadata; export default function NotFound() { @@ -20,7 +20,7 @@ export default function NotFound() { Open control plane} className="state-card" - description="The requested HRA surface does not exist or is no longer available." + description="The requested HRA v0 surface does not exist or is no longer available." icon="404" title="Control-plane route not found" /> diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx index 9531478..6201a89 100644 --- a/apps/web/app/opengraph-image.tsx +++ b/apps/web/app/opengraph-image.tsx @@ -5,7 +5,7 @@ import { ImageResponse } from "next/og"; import { HRA_BRAND_EMOJI } from "./site"; -export const alt = "HRA: a durable metaharness for Codex"; +export const alt = "HRA v0: archived Codex metaharness"; export const contentType = "image/png"; export const size = { height: 630, width: 1200 }; @@ -63,20 +63,20 @@ export default async function OpenGraphImage() { {HRA_BRAND_EMOJI}
- HRA + HRA v0
- A METAHARNESS FOR CODEX + ARCHIVED CODEX METAHARNESS
- Give Codex a team, a memory, and a budget. + HRA v0 is preserved here.
- Authorized accounts · Durable delegation · Recoverable work - hra.sh + Final v0.1.14 prerelease · Public source · Preserved history + hra-weld.vercel.app
), diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 7226501..69410cb 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -11,7 +11,10 @@ import Link from "next/link"; import Image from "next/image"; import { + CURRENT_HRA_REPOSITORY, + CURRENT_HRA_SITE, HRA_BRAND_ICON_PATH, + HRA_V0_REPOSITORY, hraHomepageKeywords, hraSearchSite, } from "./site"; @@ -130,7 +133,7 @@ export default function LandingPage() { /> Skip to content
- + - HRA + HRA v0
- GitHub - Open HRA + v0 source + Open current HRA
@@ -157,24 +161,24 @@ export default function LandingPage() {
-

A metaharness for Codex

-

Give Codex a team, a memory, and a budget.

+

Archived HRA v0 · final prerelease

+

HRA v0 is preserved here.

- HRA turns the Codex accounts you already use into one durable system for planning work, delegating it, running it in parallel, and bringing it back for review. + This site and repository preserve the original HRA macOS metaharness, its source, and its final v0.1.14 prerelease.

- It is for projects that outgrow independent sessions: work has dependencies, follow-ups need continuity, account identities stay separate, and an interrupted run needs a recoverable answer. + The current HRA is a separate project. New users should start at hra.sh or read its current source. Existing v0 users can keep using the archive and download below.

- Download for macOS - Compare HRA - - View source + Go to current HRA + Download HRA v0 + + View v0 source
  • Apple Silicon
  • -
  • Public prerelease
  • +
  • Archived prerelease
  • Apache 2.0
  • Local execution
@@ -290,15 +294,15 @@ export default function LandingPage() {

The harness should be inspectable.

The public repository includes the product source, security architecture, build checks, and the exact boundaries around local execution.

- - Open GitHub + + Open HRA v0 on GitHub
-

A durable metaharness for Codex.

+

HRA v0 archive. For current HRA, visit hra.sh.

Privacy and analytics

diff --git a/apps/web/app/public-markdown.test.ts b/apps/web/app/public-markdown.test.ts index f08e8fb..00f1897 100644 --- a/apps/web/app/public-markdown.test.ts +++ b/apps/web/app/public-markdown.test.ts @@ -7,6 +7,7 @@ import { createDownloadMarkdown, createLandingMarkdown, createNotFoundMarkdown, + createReleaseHistoryMarkdown, HRA_LLMS_TXT, HRA_LLMS_TXT_PATH, isAuthProtectedTree, @@ -18,45 +19,45 @@ import { HRA_RELEASE, hraSearchSite } from "./site"; describe("HRA public markdown representations", () => { test("publishes an llms.txt with when-to-use guidance and public page lists", () => { - expect(HRA_LLMS_TXT.startsWith("# HRA\n")).toBeTrue(); + expect(HRA_LLMS_TXT.startsWith("# HRA v0 archive\n")).toBeTrue(); expect(HRA_LLMS_TXT).toContain(`> ${hraSearchSite.description}`); - expect(HRA_LLMS_TXT).toContain("When to use this:"); - expect(HRA_LLMS_TXT).toContain("several coordinated Codex sessions"); - expect(HRA_LLMS_TXT).toContain("How an agent should call HRA:"); + expect(HRA_LLMS_TXT).toContain("maintained archive for HRA v0"); + expect(HRA_LLMS_TXT).toContain("final v0.1.14 macOS prerelease"); + expect(HRA_LLMS_TXT).toContain("How an agent should use this archive:"); expect(HRA_LLMS_TXT).toContain("Accept: text/markdown"); expect(HRA_LLMS_TXT).not.toContain("OAuth client"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/download"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/alternatives"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/llms.txt"); - expect(HRA_LLMS_TXT).toContain("https://hra.sh/sitemap.xml"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/download"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/releases"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/.well-known/hra.json"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/alternatives"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/llms.txt"); + expect(HRA_LLMS_TXT).toContain("https://hra-weld.vercel.app/sitemap.xml"); for (const comparison of hraComparisons) { - expect(HRA_LLMS_TXT).toContain(`https://hra.sh/alternatives/${comparison.slug}`); + expect(HRA_LLMS_TXT).toContain(`https://hra-weld.vercel.app/alternatives/${comparison.slug}`); } expect(HRA_LLMS_TXT_PATH).toBe("/llms.txt"); }); test("keeps landing markdown aligned with the public product copy", () => { const markdown = createLandingMarkdown(); - expect(markdown).toContain("# Give Codex a team, a memory, and a budget."); + expect(markdown).toContain("# HRA v0 is preserved here."); expect(markdown).toContain("## When to use HRA"); expect(markdown).toContain("Delegate work with structure"); expect(markdown).toContain("A provider limit ends the affected turn."); - expect(markdown).toContain("https://hra.sh/llms.txt"); - expect(markdown).toContain("https://hra.sh/sitemap.xml"); + expect(markdown).toContain("https://hra-weld.vercel.app/llms.txt"); + expect(markdown).toContain("https://hra-weld.vercel.app/sitemap.xml"); }); test("keeps download markdown honest about the current release contract", () => { const markdown = createDownloadMarkdown(); - expect(markdown).toContain("# Download HRA for your Mac."); + expect(markdown).toContain("# Download HRA v0 for your Mac."); expect(markdown).toContain(`macOS ${HRA_RELEASE.minimumMacOS}`); expect(markdown).toContain("not Developer ID signed or notarized"); - expect(markdown).toContain("https://hra.sh/llms.txt"); - if (HRA_RELEASE.availability === "candidate") { - expect(markdown).toContain("Do not install an unpublished draft asset."); - } else { - expect(markdown).toContain(HRA_RELEASE.asset); - } + expect(markdown).toContain("https://hra-weld.vercel.app/llms.txt"); + expect(HRA_RELEASE.availability).toBe("published"); + expect(markdown).toContain(HRA_RELEASE.asset); + expect(markdown).not.toContain("Do not install an unpublished draft asset."); }); test("renders every comparison from the existing first-party rows", () => { @@ -65,18 +66,26 @@ describe("HRA public markdown representations", () => { ); for (const comparison of hraComparisons) { const markdown = publicDocumentMarkdown(`/alternatives/${comparison.slug}`); - expect(markdown, comparison.slug).toContain(`# HRA vs ${comparison.shortName}`); + expect(markdown, comparison.slug).toContain(`# HRA v0 vs ${comparison.shortName}`); expect(markdown, comparison.slug).toContain(comparison.meaningfulDifference); expect(markdown, comparison.slug).toContain(comparison.rows[0]?.hra ?? ""); } }); + test("serves the checked compatibility ledger as Markdown", () => { + const markdown = createReleaseHistoryMarkdown(); + expect(markdown).toContain("# HRA v0 release history"); + expect(markdown).toContain("v0.1.11 was a tagged candidate only"); + expect(markdown).toContain(HRA_RELEASE.source.tagObject); + expect(publicDocumentMarkdown("/releases")).toBe(markdown); + }); + test("gives unmatched public paths a markdown 404 with recovery links", () => { const markdown = createNotFoundMarkdown(); expect(markdown).toContain("# Not found"); - expect(markdown).toContain("https://hra.sh/sitemap.xml"); - expect(markdown).toContain("https://hra.sh/llms.txt"); - expect(markdown).toContain("https://hra.sh/"); + expect(markdown).toContain("https://hra-weld.vercel.app/sitemap.xml"); + expect(markdown).toContain("https://hra-weld.vercel.app/llms.txt"); + expect(markdown).toContain("https://hra-weld.vercel.app/"); }); }); @@ -84,6 +93,7 @@ describe("HRA public discovery decisions", () => { test("classifies only the existing public HTML documents", () => { expect(isPublicHtmlDocumentPath("/")).toBeTrue(); expect(isPublicHtmlDocumentPath("/download/")).toBeTrue(); + expect(isPublicHtmlDocumentPath("/releases")).toBeTrue(); expect(isPublicHtmlDocumentPath("/alternatives/codex-app")).toBeTrue(); expect(isPublicHtmlDocumentPath("/alternatives/missing")).toBeFalse(); expect(isPublicHtmlDocumentPath("/app")).toBeFalse(); @@ -105,7 +115,7 @@ describe("HRA public discovery decisions", () => { status: 200, }); if (home.action === "markdown") { - expect(home.body).toContain("Give Codex a team, a memory, and a budget."); + expect(home.body).toContain("HRA v0 is preserved here."); } }); @@ -133,7 +143,7 @@ describe("HRA public discovery decisions", () => { }); expect(missing).toMatchObject({ action: "markdown", status: 404 }); if (missing.action === "markdown") { - expect(missing.body).toContain("https://hra.sh/sitemap.xml"); + expect(missing.body).toContain("https://hra-weld.vercel.app/sitemap.xml"); } }); @@ -146,6 +156,7 @@ describe("HRA public discovery decisions", () => { ["/robots.txt", "text/markdown"], ["/sitemap.xml", "text/markdown"], ["/llms.txt", "text/markdown"], + ["/.well-known/hra.json", "text/markdown"], ["/icon.png", "text/markdown"], ] as const) { expect(resolvePublicDiscovery({ diff --git a/apps/web/app/public-markdown.ts b/apps/web/app/public-markdown.ts index 4a2346e..6a49fb6 100644 --- a/apps/web/app/public-markdown.ts +++ b/apps/web/app/public-markdown.ts @@ -13,6 +13,18 @@ import { } from "./alternatives/comparisons"; import { isHraPublicComparisonPath } from "./alternatives/slugs"; import { + HRA_DEPLOYMENT_IDENTITY_PATH, +} from "./deployment-identity"; +import { + HRA_RELEASE_HISTORY, + hraCommitUrl, + hraReleaseAssetUrl, + hraReleaseTagUrl, + hraTagObjectUrl, +} from "./release-history"; +import { + CURRENT_HRA_REPOSITORY, + CURRENT_HRA_SITE, HRA_RELEASE, HRA_RELEASE_CHECKSUM_URL, HRA_RELEASE_MANIFEST_URL, @@ -56,6 +68,7 @@ export function isPublicHtmlDocumentPath(pathname: string): boolean { if (canonicalPath === null) return false; return canonicalPath === "/" || canonicalPath === "/download" + || canonicalPath === "/releases" || isHraPublicComparisonPath(canonicalPath); } @@ -91,22 +104,23 @@ export function pathLooksLikeStaticAsset(pathname: string): boolean { export function createHraLlmsTxt(): string { const pages = [ - `- [HRA home](${absoluteUrl("/")}): Product overview, fit, and limits`, - `- [Download for macOS](${absoluteUrl("/download")}): Apple Silicon prerelease status and source-build guidance`, - `- [HRA alternatives](${absoluteUrl("/alternatives")}): First-party-sourced comparisons`, + `- [HRA v0 archive](${absoluteUrl("/")}): Archive status, current-HRA handoff, and original product overview`, + `- [HRA v0 download](${absoluteUrl("/download")}): Final Apple Silicon prerelease and source-build guidance`, + `- [HRA v0 release history](${absoluteUrl("/releases")}): Exact tags, commits, releases, assets, sizes, checksums, and download links`, + `- [HRA v0 alternatives](${absoluteUrl("/alternatives")}): Historical first-party-sourced comparisons`, ...hraComparisons.map((comparison) => `- [HRA vs ${comparison.shortName}](${absoluteUrl(`/alternatives/${comparison.slug}`)}): ${comparison.description}`), ]; return [ - "# HRA", + "# HRA v0 archive", "", `> ${hraSearchSite.description}`, "", - "When to use this: reach for HRA when one project needs several coordinated Codex sessions, you have separate authorized Codex accounts to keep isolated, child work must rejoin a durable parent task, or restarts and ambiguous effects need explicit recovery.", + `This is the maintained archive for HRA v0. New users should start with [current HRA](${CURRENT_HRA_SITE}) and its [current source](${CURRENT_HRA_REPOSITORY}). Use this site for the final v0.1.14 macOS prerelease, historical source, and v0 documentation.`, "", "Choose something simpler when you want the first-party Codex experience for a few independent sessions, your team needs one desktop for many model providers, the main problem is remote access from a phone, or a worktree launcher and diff viewer already solve the job.", "", - "How an agent should call HRA: request `Accept: text/markdown` on the public pages below, or start from this file. Point humans at the website for product decisions, `/download` for the Mac app, and `/app` for the hosted control plane. Do not treat hra.sh as an execution, OAuth, GraphQL, MCP, or commerce API. Provider credentials, repositories, commands, and raw transcripts stay on the paired Mac. The public source is https://github.com/hraness/hra.", + "How an agent should use this archive: request `Accept: text/markdown` on the public pages below, or start from this file. Point new users at https://hra.sh. Point v0 users at `/download` and https://github.com/hraness/hra-v0. Do not treat hra-weld.vercel.app as an execution, OAuth, GraphQL, MCP, or commerce API.", "", "## Pages", "", @@ -117,8 +131,10 @@ export function createHraLlmsTxt(): string { `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)}): This file`, `- [XML sitemap](${absoluteUrl("/sitemap.xml")}): Indexable public HTML pages`, `- [Robots](${absoluteUrl("/robots.txt")}): Crawler allow and deny rules`, - "- [Public source](https://github.com/hraness/hra): Product source, security architecture, and build checks", - "- [Hosted control plane](https://hra.sh/app): Authenticated human supervision; not a public API", + `- [Deployment identity](${absoluteUrl(HRA_DEPLOYMENT_IDENTITY_PATH)}): Stable generation and publication marker for cutover checks`, + "- [HRA v0 source](https://github.com/hraness/hra-v0): Archived product source, security architecture, and build checks", + `- [Current HRA](${CURRENT_HRA_SITE}): Current product and documentation`, + `- [Current HRA source](${CURRENT_HRA_REPOSITORY}): Current repository`, "", ].join("\n"); } @@ -127,13 +143,13 @@ export const HRA_LLMS_TXT = createHraLlmsTxt(); export function createLandingMarkdown(): string { return [ - "# Give Codex a team, a memory, and a budget.", + "# HRA v0 is preserved here.", "", hraSearchSite.description, "", - "HRA turns the Codex accounts you already use into one durable system for planning work, delegating it, running it in parallel, and bringing it back for review.", + "This site and repository preserve the original HRA macOS metaharness, its source, and its final v0.1.14 prerelease.", "", - "It is for projects that outgrow independent sessions: work has dependencies, follow-ups need continuity, account identities stay separate, and an interrupted run needs a recoverable answer.", + `New users should start with [current HRA](${CURRENT_HRA_SITE}) or its [current source](${CURRENT_HRA_REPOSITORY}). Existing v0 users can use this archive and its final download.`, "", "## When to use HRA", "", @@ -190,49 +206,38 @@ export function createLandingMarkdown(): string { "## Public pages", "", `- [Download for macOS](${absoluteUrl("/download")})`, + `- [Release history](${absoluteUrl("/releases")})`, `- [Compare HRA](${absoluteUrl("/alternatives")})`, `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)})`, `- [XML sitemap](${absoluteUrl("/sitemap.xml")})`, - "- [Public source](https://github.com/hraness/hra)", + "- [HRA v0 source](https://github.com/hraness/hra-v0)", + `- [Current HRA](${CURRENT_HRA_SITE})`, "", ].join("\n"); } export function createDownloadMarkdown(): string { - const published = HRA_RELEASE_URL !== null - && HRA_RELEASE_CHECKSUM_URL !== null - && HRA_RELEASE_MANIFEST_URL !== null; - const statusLines = published - ? [ - `Download the DMG: ${HRA_RELEASE_URL}`, - `SHA-256 file: ${HRA_RELEASE_CHECKSUM_URL}`, - `Release manifest: ${HRA_RELEASE_MANIFEST_URL}`, - "", - "Unknown developer. This candidate uses an ad-hoc code seal, but it is not Developer ID signed or notarized by Apple. The published SHA-256 verifies the exact release bytes; macOS will still ask you to approve the app manually.", - "", - "## Install the prerelease", - "", - `1. Download both files. Save the DMG and its SHA-256 file in the same folder.`, - `2. Check the bytes. In Terminal, run \`shasum -a 256 -c ${HRA_RELEASE.asset}.sha256\`. Continue only when it prints \`OK\`.`, - "3. Copy HRA to Applications. Open the DMG and drag HRA into the Applications folder.", - "4. Approve the unknown developer. Control-click HRA in Finder and choose Open. If macOS still blocks it, use System Settings → Privacy & Security → Open Anyway.", - ] - : [ - "Candidate verification in progress. Do not install an unpublished draft asset.", - "", - "Unknown developer. This candidate uses an ad-hoc code seal, but it is not Developer ID signed or notarized by Apple. Its release commit, tag, manifest, and artifact hashes are still awaiting publication.", - "", - `HRA ${HRA_RELEASE.version} (${HRA_RELEASE.build}) is a checked source candidate. Do not drag a second app beside an installed OPRTE predecessor, and do not install an unpublished draft asset.`, - "", - `You can inspect or build the candidate from ${HRA_RELEASE.repository} while release evidence is completed.`, - ]; + const statusLines = [ + `Download the DMG: ${HRA_RELEASE_URL}`, + `SHA-256 file: ${HRA_RELEASE_CHECKSUM_URL}`, + `Release manifest: ${HRA_RELEASE_MANIFEST_URL}`, + "", + "Unknown developer. This archived release uses an ad-hoc code seal, but it is not Developer ID signed or notarized by Apple. The published SHA-256 verifies the exact release bytes; macOS will still ask you to approve the app manually.", + "", + "## Install the prerelease", + "", + `1. Download both files. Save the DMG and its SHA-256 file in the same folder.`, + `2. Check the bytes. In Terminal, run \`shasum -a 256 -c ${HRA_RELEASE.asset}.sha256\`. Continue only when it prints \`OK\`.`, + "3. Copy HRA to Applications. Open the DMG and drag HRA into the Applications folder.", + "4. Approve the unknown developer. Control-click HRA in Finder and choose Open. If macOS still blocks it, use System Settings → Privacy & Security → Open Anyway.", + ]; return [ - "# Download HRA for your Mac.", + "# Download HRA v0 for your Mac.", "", - `The native prerelease bundles HRA, Codex, and Git for Apple Silicon Macs running macOS ${HRA_RELEASE.minimumMacOS} or newer.`, + `This is the final archived v0.1.14 prerelease. It bundles HRA v0, Codex, and Git for Apple Silicon Macs running macOS ${HRA_RELEASE.minimumMacOS} or newer. New users should start with ${CURRENT_HRA_SITE}.`, "", - `Version ${HRA_RELEASE.version} (${HRA_RELEASE.build}) · Apple Silicon · macOS ${HRA_RELEASE.minimumMacOS}+ · ${published ? "Published prerelease" : "Candidate"} · Ad-hoc · not notarized.`, + `Version ${HRA_RELEASE.version} (${HRA_RELEASE.build}) · Apple Silicon · macOS ${HRA_RELEASE.minimumMacOS}+ · Published prerelease · Ad-hoc · not notarized.`, "", ...statusLines, "", @@ -240,19 +245,66 @@ export function createDownloadMarkdown(): string { "", "## Build it yourself", "", - "The public repository pins Bun, Zig, Codex, Git, native build inputs, and the package verifier. Build the same app locally if the ad-hoc release boundary is not right for you: https://github.com/hraness/hra#develop-hra", + "The public repository pins Bun, Zig, Codex, Git, native build inputs, and the package verifier. Build the same app locally if the ad-hoc release boundary is not right for you: https://github.com/hraness/hra-v0#develop-hra", "", - "Developer ID signing is not available yet. A later release needs a Developer ID certificate and Apple notarization before normal double-click installation can replace the unknown-developer flow. Automatic updates remain disabled until HRA owns a signed update channel.", + `HRA v0 is archived and will not gain a new update channel. The current HRA continues separately at ${CURRENT_HRA_SITE}.`, "", "## Public pages", "", - `- [HRA home](${absoluteUrl("/")})`, + `- [HRA v0 archive](${absoluteUrl("/")})`, + `- [Release history](${absoluteUrl("/releases")})`, + `- [Current HRA](${CURRENT_HRA_SITE})`, `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)})`, `- [XML sitemap](${absoluteUrl("/sitemap.xml")})`, "", ].join("\n"); } +export function createReleaseHistoryMarkdown(): string { + const entries = HRA_RELEASE_HISTORY.tags.toReversed().flatMap((entry) => { + const identity = [ + `## ${entry.tag} (build ${entry.build})`, + "", + `- Tag object: annotated [\`${entry.tagObject}\`](${hraTagObjectUrl(entry)})`, + `- Peeled commit: [\`${entry.commit}\`](${hraCommitUrl(entry)})`, + ]; + if (entry.release === null) { + return [ + ...identity, + "- Publication: tag only; no GitHub release or downloadable assets", + "", + "v0.1.11 was a tagged candidate only. It is not prior installed authority.", + "", + ]; + } + return [ + ...identity, + `- Publication: [immutable GitHub prerelease #${entry.release.id}](${hraReleaseTagUrl(entry)})`, + `- Published: ${entry.release.publishedAt}`, + "", + "### Assets", + "", + ...entry.release.assets.map((asset) => + `- [${asset.name}](${hraReleaseAssetUrl(entry, asset)}); ${asset.bytes} bytes; SHA-256 \`${asset.sha256}\``), + "", + ]; + }); + return [ + "# HRA v0 release history", + "", + "This checked compatibility ledger records every HRA v0 tag from v0.1.7 through v0.1.14, every GitHub release that exists, and all attached assets. v0.1.14 is the final archived prerelease.", + "", + ...entries, + "## Public pages", + "", + `- [HRA v0 archive](${absoluteUrl("/")})`, + `- [Download for macOS](${absoluteUrl("/download")})`, + `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)})`, + `- [Deployment identity](${absoluteUrl(HRA_DEPLOYMENT_IDENTITY_PATH)})`, + "", + ].join("\n"); +} + export function createAlternativesIndexMarkdown(): string { return [ "# Choose the layer you actually need.", @@ -282,6 +334,7 @@ export function createAlternativesIndexMarkdown(): string { "", `- [HRA home](${absoluteUrl("/")})`, `- [Download for macOS](${absoluteUrl("/download")})`, + `- [Release history](${absoluteUrl("/releases")})`, `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)})`, `- [XML sitemap](${absoluteUrl("/sitemap.xml")})`, "", @@ -305,7 +358,7 @@ export function createComparisonMarkdown(comparison: HraComparison): string { `- [HRA vs ${candidate.shortName}](${absoluteUrl(`/alternatives/${candidate.slug}`)})`); return [ - `# HRA vs ${comparison.shortName}`, + `# HRA v0 vs ${comparison.shortName}`, "", `Last verified ${COMPARISON_REVIEW_LABEL}.`, "", @@ -346,7 +399,7 @@ export function createComparisonMarkdown(comparison: HraComparison): string { "", ...sources, "", - `“Not documented” means only that a capability was not found in these sources on ${COMPARISON_REVIEW_LABEL}. It does not prove the product lacks it. HRA is independent and unaffiliated with ${comparison.name}. Report a correction: https://github.com/hraness/hra/issues`, + `“Not documented” means only that a capability was not found in these sources on ${COMPARISON_REVIEW_LABEL}. It does not prove the product lacks it. HRA is independent and unaffiliated with ${comparison.name}. Report a correction: https://github.com/hraness/hra-v0/issues`, "", "## More comparisons", "", @@ -369,6 +422,7 @@ export function createNotFoundMarkdown(): string { "", `- [HRA home](${absoluteUrl("/")})`, `- [Download for macOS](${absoluteUrl("/download")})`, + `- [Release history](${absoluteUrl("/releases")})`, `- [Comparisons](${absoluteUrl("/alternatives")})`, `- [Agent guide](${absoluteUrl(HRA_LLMS_TXT_PATH)})`, `- [XML sitemap](${absoluteUrl("/sitemap.xml")})`, @@ -381,6 +435,7 @@ export function publicDocumentMarkdown(pathname: string): string | null { if (canonicalPath === null) return null; if (canonicalPath === "/") return createLandingMarkdown(); if (canonicalPath === "/download") return createDownloadMarkdown(); + if (canonicalPath === "/releases") return createReleaseHistoryMarkdown(); if (canonicalPath === "/alternatives") return createAlternativesIndexMarkdown(); if (canonicalPath.startsWith("/alternatives/")) { const comparison = comparisonForSlug(canonicalPath.slice("/alternatives/".length)); diff --git a/apps/web/app/release-history.test.ts b/apps/web/app/release-history.test.ts new file mode 100644 index 0000000..af99023 --- /dev/null +++ b/apps/web/app/release-history.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { GET as getDeploymentIdentity, HEAD as headDeploymentIdentity } from "./.well-known/hra.json/route"; +import { + HRA_DEPLOYMENT_IDENTITY, + HRA_DEPLOYMENT_IDENTITY_PATH, +} from "./deployment-identity"; +import { createReleaseHistoryMarkdown } from "./public-markdown"; +import { + HRA_RELEASE_HISTORY, + hraReleaseAssetUrl, +} from "./release-history"; +import ReleasesPage from "./releases/page"; +import { HRA_RELEASE } from "./site"; + +describe("HRA v0 public release history", () => { + test("binds the exact eight tags, tag-only candidate, and 49 published assets", () => { + expect(HRA_RELEASE_HISTORY.tags.map(({ tag }) => tag)).toEqual([ + "v0.1.7", + "v0.1.8", + "v0.1.9", + "v0.1.10", + "v0.1.11", + "v0.1.12", + "v0.1.13", + "v0.1.14", + ]); + expect(HRA_RELEASE_HISTORY.tags[4]?.release).toBeNull(); + expect(HRA_RELEASE_HISTORY.tags.reduce( + (total, entry) => total + (entry.release?.assets.length ?? 0), + 0, + )).toBe(49); + const finalEntry = HRA_RELEASE_HISTORY.tags.at(-1); + expect(finalEntry).toMatchObject({ + build: HRA_RELEASE.build, + commit: HRA_RELEASE.source.commit, + tag: HRA_RELEASE.tag, + tagObject: HRA_RELEASE.source.tagObject, + }); + const finalDmg = finalEntry?.release?.assets.find(({ name }) => name === HRA_RELEASE.asset); + expect(finalDmg?.sha256).toBe(HRA_RELEASE.sha256); + }); + + test("renders every exact asset link, byte count, and digest from the checked ledger", () => { + const html = renderToStaticMarkup(ReleasesPage()); + const markdown = createReleaseHistoryMarkdown(); + expect(html).toContain("Final archived prerelease"); + expect(html).toContain("v0.1.11 was a tagged candidate only"); + for (const entry of HRA_RELEASE_HISTORY.tags) { + expect(html).toContain(entry.tagObject); + expect(markdown).toContain(entry.commit); + for (const asset of entry.release?.assets ?? []) { + const url = hraReleaseAssetUrl(entry, asset); + expect(html).toContain(url); + expect(html).toContain(asset.sha256); + expect(markdown).toContain(`${asset.bytes} bytes`); + expect(markdown).toContain(url); + } + } + }); + + test("serves a stable generation-0 cutover identity without request state", async () => { + expect(HRA_DEPLOYMENT_IDENTITY_PATH).toBe("/.well-known/hra.json"); + expect(HRA_DEPLOYMENT_IDENTITY).toMatchObject({ + generation: 0, + product: "HRA", + publication: { + build: 15, + releaseId: 374980441, + sourceCommit: "7b39c459827b2acf45aa2d911c94fdb5d4f37860", + tag: "v0.1.14", + tagObject: "37ed37afb39cacfd6a51044cf7f3c1b873571aa3", + }, + repository: { id: 1_334_876_494, path: "hraness/hra-v0" }, + schemaVersion: 2, + source: { commit: "local" }, + }); + const response = getDeploymentIdentity(); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json; charset=utf-8"); + expect(await response.json()).toEqual(HRA_DEPLOYMENT_IDENTITY); + const head = headDeploymentIdentity(); + expect(head.status).toBe(200); + expect(await head.text()).toBe(""); + }); +}); diff --git a/apps/web/app/release-history.ts b/apps/web/app/release-history.ts new file mode 100644 index 0000000..2737bbb --- /dev/null +++ b/apps/web/app/release-history.ts @@ -0,0 +1,106 @@ +import { z } from "@hra-internal/schema"; + +import releaseHistory from "../../../release-history.json"; + +const objectIdSchema = z.string().regex(/^[0-9a-f]{40}$/u); +const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u); +const assetSchema = z.object({ + bytes: z.number().int().positive().safe(), + id: z.number().int().positive().safe(), + name: z.string().min(1).max(200), + sha256: digestSchema, +}).strict(); +const publishedReleaseSchema = z.object({ + assets: z.array(assetSchema).length(7), + id: z.number().int().positive().safe(), + immutable: z.literal(true), + prerelease: z.literal(true), + publishedAt: z.string().regex(/^2026-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/u), +}).strict(); +const tagSchema = z.object({ + build: z.number().int().positive().safe(), + commit: objectIdSchema, + objectKind: z.literal("annotated"), + release: publishedReleaseSchema.nullable(), + tag: z.string().regex(/^v0\.1\.(?:7|8|9|10|11|12|13|14)$/u), + tagObject: objectIdSchema, + version: z.string().regex(/^0\.1\.(?:7|8|9|10|11|12|13|14)$/u), +}).strict(); +const releaseHistorySchema = z.object({ + generation: z.literal(0), + publicationCommit: z.literal("6221f79b745f154882080936b961ff431569f33e"), + repository: z.literal("https://github.com/hraness/hra-v0"), + repositoryId: z.literal(1_334_876_494), + schemaVersion: z.literal(1), + tags: z.array(tagSchema).length(8), +}).strict().superRefine((history, context) => { + const expectedVersions = ["0.1.7", "0.1.8", "0.1.9", "0.1.10", "0.1.11", "0.1.12", "0.1.13", "0.1.14"] as const; + for (const [index, version] of expectedVersions.entries()) { + const entry = history.tags[index]; + if ( + entry === undefined + || entry.version !== version + || entry.tag !== `v${version}` + || entry.build !== index + 8 + || (entry.release === null) !== (version === "0.1.11") + ) { + context.addIssue({ + code: "custom", + message: "Release history must contain the exact ordered v0.1.7–v0.1.14 tag and release sequence.", + }); + break; + } + if (entry.release !== null) { + const assetIds = new Set(entry.release.assets.map(({ id }) => id)); + const assetNames = entry.release.assets.map(({ name }) => name); + const expectedProductAssets = [ + `HRA-${version}-${entry.build}-macos-arm64.dmg`, + `HRA-${version}-${entry.build}-macos-arm64.dmg.sha256`, + `HRA-${version}-${entry.build}-release-manifest.json`, + ]; + if ( + assetIds.size !== entry.release.assets.length + || new Set(assetNames).size !== assetNames.length + || !expectedProductAssets.every((name) => assetNames.includes(name)) + || assetNames.toSorted().join("\0") !== assetNames.join("\0") + ) { + context.addIssue({ + code: "custom", + message: `Release ${entry.tag} must contain one sorted, unique seven-asset inventory.`, + }); + } + } + } +}); + +export type HraReleaseHistory = z.infer; +export type HraReleaseHistoryTag = HraReleaseHistory["tags"][number]; +export type HraReleaseHistoryAsset = NonNullable["assets"][number]; + +export const HRA_RELEASE_HISTORY = releaseHistorySchema.parse(releaseHistory); + +export function hraReleaseTagUrl(entry: HraReleaseHistoryTag): string { + return `${HRA_RELEASE_HISTORY.repository}/releases/tag/${entry.tag}`; +} + +export function hraReleaseAssetUrl( + entry: HraReleaseHistoryTag, + asset: HraReleaseHistoryAsset, +): string { + return `${HRA_RELEASE_HISTORY.repository}/releases/download/${entry.tag}/${asset.name}`; +} + +export function hraTagObjectUrl(entry: HraReleaseHistoryTag): string { + return `https://api.github.com/repos/hraness/hra-v0/git/tags/${entry.tagObject}`; +} + +export function hraCommitUrl(entry: HraReleaseHistoryTag): string { + return `${HRA_RELEASE_HISTORY.repository}/commit/${entry.commit}`; +} + +export function formatReleaseBytes(bytes: number): string { + if (bytes < 1_000) return `${bytes} B`; + if (bytes < 1_000_000) return `${(bytes / 1_000).toFixed(1)} kB`; + if (bytes < 1_000_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`; + return `${(bytes / 1_000_000_000).toFixed(2)} GB`; +} diff --git a/apps/web/app/releases/page.tsx b/apps/web/app/releases/page.tsx new file mode 100644 index 0000000..4e5d901 --- /dev/null +++ b/apps/web/app/releases/page.tsx @@ -0,0 +1,148 @@ +import { ThemeMenuButton } from "@hra-internal/design-kit/react"; +import { createPublicSiteMetadata } from "@hraness/web-discovery"; +import type { Metadata } from "next"; +import Link from "next/link"; + +import { + formatReleaseBytes, + HRA_RELEASE_HISTORY, + hraCommitUrl, + hraReleaseAssetUrl, + hraReleaseTagUrl, + hraTagObjectUrl, + type HraReleaseHistoryTag, +} from "../release-history"; +import { + CURRENT_HRA_SITE, + hraSearchSite, + hraSocialPageTitle, +} from "../site"; + +export const metadata = createPublicSiteMetadata({ + ...hraSearchSite, + description: + "Exact HRA v0 tags, source commits, immutable GitHub releases, asset sizes, checksums, and download links.", + socialTitle: hraSocialPageTitle("HRA v0 release history"), + title: "HRA v0 release history", +}, { canonicalPath: "/releases" }) satisfies Metadata; + +function ReleaseEntry({ entry }: { readonly entry: HraReleaseHistoryTag }) { + const finalRelease = entry.tag === "v0.1.14"; + return ( +

+
+
+

+ {finalRelease ? "Final archived prerelease" : entry.release === null ? "Tag only" : "Immutable prerelease"} +

+

{entry.tag} build {entry.build}

+
+ {entry.release === null ? ( + No GitHub release + ) : ( + + GitHub release #{entry.release.id} + + )} +
+ +
+
+
Tag object
+
Annotated · {entry.tagObject}
+
+
+
Peeled commit
+
{entry.commit}
+
+
+ + {entry.release === null ? ( +

+ v0.1.11 was a tagged candidate only. It has no GitHub release or downloadable assets and is not prior installed authority. +

+ ) : ( +
+ + {entry.release.assets.length} assets · published {entry.release.publishedAt.slice(0, 10)} + +
+
DecisionHRA{comparison.shortName}
DecisionHRA v0{comparison.shortName}
+ + + + + + + + + {entry.release.assets.map((asset) => ( + + + + + + ))} + +
AssetSizeSHA-256
{asset.name}{formatReleaseBytes(asset.bytes)} ({asset.bytes.toLocaleString("en-US")} bytes){asset.sha256}
+
+ + )} + + ); +} + +export default function ReleasesPage() { + return ( +
+ Skip to content +
+ + HRA v0 + + +
+ +
+
+ +
+
+

Compatibility ledger · v0.1.7–v0.1.14

+

HRA v0 release history

+

+ This ledger records every preserved tag from v0.1.7 through v0.1.14, every GitHub release that exists, and every attached asset. The checked archive manifest supplies the object IDs, byte counts, checksums, and links below. +

+

+ Seven immutable prereleases · eight annotated tags · v0.1.11 tag only +

+
+ +
+ {HRA_RELEASE_HISTORY.tags.toReversed().map((entry) => ( + + ))} +
+ +
+
+

Verification boundary

+

The archive checks the remote objects.

+

+ The credential-free remote release gate reads the renamed repository, verifies the exact tag objects and peeled commits, requires v0.1.11 to remain release-free, and compares every immutable release and asset with the checked ledger. The final v0.1.14 gate also checks its checksum and release manifest contents. +

+
+
+
+ + +
+ ); +} diff --git a/apps/web/app/site.ts b/apps/web/app/site.ts index d27a38a..755a27b 100644 --- a/apps/web/app/site.ts +++ b/apps/web/app/site.ts @@ -8,11 +8,6 @@ import type { Metadata } from "next"; import releaseDownload from "../../../release-download.json"; -const emptyArtifactSchema = z.object({ - bytes: z.null(), - name: z.string().min(1), - sha256: z.null(), -}).strict(); const publishedArtifactSchema = z.object({ bytes: z.number().int().positive().safe(), name: z.string().min(1), @@ -26,37 +21,21 @@ const commonReleaseShape = { version: z.string().regex(/^[0-9]+\.[0-9]+\.[0-9]+$/u), } as const; const releaseDownloadSchema = z.object({ - release: z.discriminatedUnion("availability", [ - z.object({ - ...commonReleaseShape, - artifacts: z.object({ - checksum: emptyArtifactSchema, - dmg: emptyArtifactSchema, - manifest: emptyArtifactSchema, - }).strict(), - availability: z.literal("candidate"), - source: z.object({ - commit: z.null(), - runtimeTreeSha256: z.null(), - tagObject: z.null(), - }).strict(), + release: z.object({ + ...commonReleaseShape, + artifacts: z.object({ + checksum: publishedArtifactSchema, + dmg: publishedArtifactSchema, + manifest: publishedArtifactSchema, }).strict(), - z.object({ - ...commonReleaseShape, - artifacts: z.object({ - checksum: publishedArtifactSchema, - dmg: publishedArtifactSchema, - manifest: publishedArtifactSchema, - }).strict(), - availability: z.literal("published"), - source: z.object({ - commit: z.string().regex(/^[0-9a-f]{40}$/u), - runtimeTreeSha256: z.string().regex(/^[0-9a-f]{64}$/u), - tagObject: z.string().regex(/^[0-9a-f]{40}$/u), - }).strict(), + availability: z.literal("published"), + source: z.object({ + commit: z.string().regex(/^[0-9a-f]{40}$/u), + runtimeTreeSha256: z.string().regex(/^[0-9a-f]{64}$/u), + tagObject: z.string().regex(/^[0-9a-f]{40}$/u), }).strict(), - ]), - repository: z.literal("https://github.com/hraness/hra"), + }).strict(), + repository: z.literal("https://github.com/hraness/hra-v0"), schemaVersion: z.literal(1), }).strict().superRefine((contract, context) => { const { release } = contract; @@ -77,7 +56,14 @@ const releaseDownloadSchema = z.object({ export const HRA_BRAND_EMOJI = "🐦‍🔥" as const; export const HRA_BRAND_ICON_PATH = "/icon.png" as const; -const releaseContract = releaseDownloadSchema.parse(releaseDownload as unknown); +export const HRA_V0_REPOSITORY = + "https://github.com/hraness/hra-v0" as const; +export const HRA_V0_HISTORICAL_PUBLICATION_REPOSITORY = + "https://github.com/hraness/hra" as const; +export const CURRENT_HRA_REPOSITORY = + "https://github.com/hraness/hra" as const; +export const CURRENT_HRA_SITE = "https://hra.sh" as const; +const releaseContract = releaseDownloadSchema.parse(releaseDownload); export const HRA_RELEASE = Object.freeze({ architecture: releaseContract.release.architecture, asset: releaseContract.release.artifacts.dmg.name, @@ -86,36 +72,35 @@ export const HRA_RELEASE = Object.freeze({ checksumAsset: releaseContract.release.artifacts.checksum.name, manifestAsset: releaseContract.release.artifacts.manifest.name, minimumMacOS: releaseContract.release.minimumMacOS, + historicalPublicationRepository: HRA_V0_HISTORICAL_PUBLICATION_REPOSITORY, repository: releaseContract.repository, + sha256: releaseContract.release.artifacts.dmg.sha256, source: releaseContract.release.source, tag: releaseContract.release.tag, version: releaseContract.release.version, }); -export const HRA_RELEASE_URL = HRA_RELEASE.availability === "published" - ? `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.asset}` - : null; -export const HRA_RELEASE_CHECKSUM_URL = HRA_RELEASE_URL === null - ? null - : `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.checksumAsset}`; -export const HRA_RELEASE_MANIFEST_URL = HRA_RELEASE_URL === null - ? null - : `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.manifestAsset}`; +export const HRA_RELEASE_URL = + `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.asset}`; +export const HRA_RELEASE_CHECKSUM_URL = + `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.checksumAsset}`; +export const HRA_RELEASE_MANIFEST_URL = + `${HRA_RELEASE.repository}/releases/download/${HRA_RELEASE.tag}/${HRA_RELEASE.manifestAsset}`; export const hraSearchSite = { description: - "A metaharness for Codex: coordinate authorized accounts, delegate durable work, preserve continuity, and recover parallel sessions on your Mac.", - applicationName: "HRA", + "The archived HRA v0 metaharness for Codex, preserved with its final macOS prerelease and public source.", + applicationName: "HRA v0", category: "DeveloperApplication", creator: "Hraness", - name: "HRA", - origin: "https://hra.sh", + name: "HRA v0", + origin: "https://hra-weld.vercel.app", publisher: "Hraness", socialImage: { - alt: "HRA: a durable metaharness for Codex", + alt: "HRA v0: the archived Codex metaharness", path: "/opengraph-image", }, - title: "HRA: a metaharness for Codex", - titleTemplate: "%s · HRA", + title: "HRA v0: archived Codex metaharness", + titleTemplate: "%s · HRA v0", } as const satisfies SearchSite; const rootSocialImage = { diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts index f706a0a..ed37107 100644 --- a/apps/web/app/sitemap.ts +++ b/apps/web/app/sitemap.ts @@ -15,6 +15,11 @@ export default function sitemap() { path: "/download", priority: 0.8, }, + { + changeFrequency: "monthly", + path: "/releases", + priority: 0.8, + }, { changeFrequency: "monthly", path: "/alternatives", diff --git a/apps/web/app/theme-menu-contract.test.tsx b/apps/web/app/theme-menu-contract.test.tsx index d4b8686..be6fecb 100644 --- a/apps/web/app/theme-menu-contract.test.tsx +++ b/apps/web/app/theme-menu-contract.test.tsx @@ -42,14 +42,14 @@ test("public and gallery surfaces keep the sole menu as their final header actio expect(themeMenuUsages(page)).toBe(1); expect(page).not.toContain("\s*\s*<\/div>/u); + expect(landing).toMatch(/Open current HRA<\/a>\s*\s*<\/div>/u); expect(alternatives).toMatch( - /GitHub<\/a>\s*<\/nav>\s*\s*<\/div>/u, + /Current HRA<\/a>\s*<\/nav>\s*\s*<\/div>/u, ); expect(comparison).toMatch( /Download<\/Link>\s*<\/nav>\s*\s*<\/div>/u, ); - expect(download).toMatch(/Open control plane[\s\S]*?<\/Link>\s*\s*<\/div>/u); + expect(download).toMatch(/Open current HRA[\s\S]*?<\/a>\s*\s*<\/div>/u); expect(designPage).toContain("actions={}"); expect(designPage).not.toContain('position="sticky"'); expect(gallery).not.toContain(" { }); test("adds source-bound delivery proof after every existing HRA header rule", async () => { - expect(hraVercelProjectName).toBe("hra"); + expect(hraVercelProjectName).toBe("hra-v0"); const config = createHraNextConfig({ VERCEL: "1", VERCEL_DEPLOYMENT_ID: deliveryIdentity.deploymentId, diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index f2438d8..0da40e6 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -95,7 +95,7 @@ export function withHraProductionIconBoundary(config }; return config; } -export const hraVercelProjectName = "hra"; +export const hraVercelProjectName = "hra-v0"; export function createHraNextConfig( environment: ProductionDeliveryProofEnvironment = process.env, diff --git a/apps/web/package.json b/apps/web/package.json index f221ea8..628da6b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,12 +6,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "apps/web" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "scripts": { diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index a17383d..10d0f5a 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -23,12 +23,15 @@ const AUTH_PROXY_EXCLUDED_EXACT_PATHS: ReadonlySet = new Set([ "/apple-icon.png", "/download", "/download/", + "/.well-known/hra.json", "/favicon.ico", "/icon", "/icon.png", "/llms.txt", "/llms.txt/", "/opengraph-image", + "/releases", + "/releases/", "/robots.txt", "/sitemap.xml", ]); diff --git a/apps/web/scripts/vercel-build.property.test.ts b/apps/web/scripts/vercel-build.property.test.ts index 3b44ece..896a908 100644 --- a/apps/web/scripts/vercel-build.property.test.ts +++ b/apps/web/scripts/vercel-build.property.test.ts @@ -16,7 +16,7 @@ const productionEnvironment = { "https://benevolent-akita-439.convex.site", NEXT_PUBLIC_CONVEX_URL: "https://benevolent-akita-439.convex.cloud", - NEXT_PUBLIC_SITE_URL: "https://hra.sh", + NEXT_PUBLIC_SITE_URL: "https://hra-weld.vercel.app", SUITE_IDENTITY_RECEIPT_KEY_VERSION: "v1", SUITE_OIDC_COOKIE_SECRET: "c".repeat(64), VERCEL: "1", @@ -73,7 +73,7 @@ test("Production never runs with Convex-only custody in Vercel", () => { "https://benevolent-akita-439.convex.site", NEXT_PUBLIC_CONVEX_URL: "https://benevolent-akita-439.convex.cloud", - NEXT_PUBLIC_SITE_URL: "https://hra.sh", + NEXT_PUBLIC_SITE_URL: "https://hra-weld.vercel.app", SUITE_IDENTITY_RECEIPT_KEY_VERSION: "v1", SUITE_OIDC_COOKIE_SECRET: "c".repeat(64), VERCEL: "1", diff --git a/apps/web/scripts/vercel-build.test.ts b/apps/web/scripts/vercel-build.test.ts index ca6c3ed..a731a5a 100644 --- a/apps/web/scripts/vercel-build.test.ts +++ b/apps/web/scripts/vercel-build.test.ts @@ -10,6 +10,7 @@ import { previewForbiddenEnvironmentVariables, previewSurfaceOriginEnvironmentVariable, releasePublicationCommitEnvironmentVariable, + releaseSurfaceCommitEnvironmentVariable, runVercelAppBuild, runVercelConvexBuild, type VercelConvexBuildLauncher, @@ -19,10 +20,11 @@ const deployment = "benevolent-akita-439"; const marker = { CONVEX_PRODUCTION_DEPLOYMENT_NAME: deployment, [releasePublicationCommitEnvironmentVariable]: "a".repeat(40), - VERCEL_GIT_COMMIT_SHA: "a".repeat(40), + [releaseSurfaceCommitEnvironmentVariable]: "b".repeat(40), + VERCEL_GIT_COMMIT_SHA: "b".repeat(40), VERCEL_GIT_PROVIDER: "github", VERCEL_GIT_REPO_OWNER: "hraness", - VERCEL_GIT_REPO_SLUG: "hra", + VERCEL_GIT_REPO_SLUG: "hra-v0", } as const; const publicConvex = { NEXT_PUBLIC_CONVEX_SITE_URL: `https://${deployment}.convex.site`, @@ -32,8 +34,8 @@ const productionEnvironment = { ...marker, ...publicConvex, CONVEX_PROVIDER_AUTHORITY: `prod:${deployment}|secret`, - NEXT_PUBLIC_POSTHOG_KEY: "phc_hra_public", - NEXT_PUBLIC_SITE_URL: "https://hra.sh", + NEXT_PUBLIC_POSTHOG_KEY: "phc_hra_v0_public", + NEXT_PUBLIC_SITE_URL: "https://hra-weld.vercel.app", SUITE_IDENTITY_RECEIPT_KEY_VERSION: "v1", SUITE_OIDC_COOKIE_SECRET: "c".repeat(64), VERCEL: "1", @@ -94,10 +96,10 @@ describe("HRA Vercel Convex target plans", () => { })).toEqual({ environmentMode: "deploy-convex", kind: "run" }); for (const value of [ "", - "phx_hra_public", + "phx_hra_v0_public", "phc_short", - "phc_hra public", - "phc_hra_public!", + "phc_hra_v0 public", + "phc_hra_v0_public!", `phc_${"a".repeat(513)}`, ]) { expect(planVercelConvexBuild({ @@ -277,7 +279,7 @@ describe("provider process boundary", () => { }); test("refuses a malformed Production PostHog key before launching Next", async () => { - for (const value of ["", "phx_hra_public", "phc_short", "phc_hra public"]) { + for (const value of ["", "phx_hra_v0_public", "phc_short", "phc_hra_v0 public"]) { const observed = recorder(); const reasons: string[] = []; expect(await runVercelAppBuild({ @@ -321,6 +323,9 @@ describe("provider process boundary", () => { expect( observed.calls[0]?.environment[releasePublicationCommitEnvironmentVariable], ).toBe("a".repeat(40)); + expect( + observed.calls[0]?.environment[releaseSurfaceCommitEnvironmentVariable], + ).toBe("b".repeat(40)); }); test("the checked nested Production build strips secrets and retains public literals", async () => { @@ -348,14 +353,17 @@ describe("provider process boundary", () => { expect(observed.calls[0]?.environment.NEXT_PUBLIC_CONVEX_SITE_URL) .toBe(publicConvex.NEXT_PUBLIC_CONVEX_SITE_URL); expect(observed.calls[0]?.environment.NEXT_PUBLIC_SITE_URL) - .toBe("https://hra.sh"); + .toBe("https://hra-weld.vercel.app"); expect(observed.calls[0]?.environment.NEXT_PUBLIC_POSTHOG_KEY) - .toBe("phc_hra_public"); + .toBe("phc_hra_v0_public"); expect(observed.calls[0]?.environment.SUITE_IDENTITY_RECEIPT_KEY_VERSION) .toBe("v1"); expect( observed.calls[0]?.environment[releasePublicationCommitEnvironmentVariable], ).toBeUndefined(); + expect( + observed.calls[0]?.environment[releaseSurfaceCommitEnvironmentVariable], + ).toBeUndefined(); }); test("Preview skips Convex and strips every authority selector", async () => { @@ -386,6 +394,9 @@ describe("provider process boundary", () => { expect( observed.calls[0]?.environment[releasePublicationCommitEnvironmentVariable], ).toBeUndefined(); + expect( + observed.calls[0]?.environment[releaseSurfaceCommitEnvironmentVariable], + ).toBeUndefined(); }); test("nested app build revalidates the source-bound declaration", async () => { @@ -426,5 +437,8 @@ describe("checked provider wiring", () => { expect(releasePublicationCommitEnvironmentVariable).toBe( "HRA_RELEASE_PUBLICATION_COMMIT_ALLOWLIST", ); + expect(releaseSurfaceCommitEnvironmentVariable).toBe( + "HRA_V0_SURFACE_COMMIT_ALLOWLIST", + ); }); }); diff --git a/apps/web/scripts/vercel-build.ts b/apps/web/scripts/vercel-build.ts index 5b3cb60..33e3fbf 100644 --- a/apps/web/scripts/vercel-build.ts +++ b/apps/web/scripts/vercel-build.ts @@ -1,5 +1,6 @@ import { releasePublicationCommitAllowlistEnvironmentVariable, + releaseSurfaceCommitAllowlistEnvironmentVariable, verifyReleaseSourceGate, verifyVercelReleaseSourceGate, } from "../../desktop/runtime/release-download-contract"; @@ -26,7 +27,7 @@ const keyVersionPattern = /^[a-z0-9][a-z0-9._-]{0,31}$/u; const vercelHostnamePattern = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+vercel\.app$/u; -const HRA_PRODUCTION_SITE_URL = "https://hra.sh"; +const HRA_PRODUCTION_SITE_URL = "https://hra-weld.vercel.app"; const HRA_PRODUCTION_RECEIPT_KEY_VERSION = "v1"; export const productionDeploymentNameEnvironmentVariable = @@ -67,6 +68,8 @@ export const convexOnlyEnvironmentVariables = [ ] as const; export const releasePublicationCommitEnvironmentVariable = releasePublicationCommitAllowlistEnvironmentVariable; +export const releaseSurfaceCommitEnvironmentVariable = + releaseSurfaceCommitAllowlistEnvironmentVariable; /** * These values are either deployment authority or complete a production-only @@ -450,6 +453,7 @@ function applicationEnvironment( delete child[variable]; } delete child[releasePublicationCommitEnvironmentVariable]; + delete child[releaseSurfaceCommitEnvironmentVariable]; if (surfaceOrigin === undefined) { delete child[previewSurfaceOriginEnvironmentVariable]; } else { diff --git a/apps/web/suite-account-configuration.ts b/apps/web/suite-account-configuration.ts index 4563463..7711165 100644 --- a/apps/web/suite-account-configuration.ts +++ b/apps/web/suite-account-configuration.ts @@ -1,4 +1,4 @@ -export const HRA_SITE_URL = "https://hra.sh" as const; +export const HRA_SITE_URL = "https://hra-weld.vercel.app" as const; export const HRA_SUITE_ACCOUNTS_ORIGIN = "https://account.hraness.com" as const; export const HRA_SUITE_OIDC_CLIENT_ID = diff --git a/apps/web/suite-account-contracts.test.ts b/apps/web/suite-account-contracts.test.ts index 4f68dff..0cc8729 100644 --- a/apps/web/suite-account-contracts.test.ts +++ b/apps/web/suite-account-contracts.test.ts @@ -108,11 +108,11 @@ describe("HRA-owned suite account contracts", () => { }); test("pins only HRA's public site and canonical Accounts origin", () => { - expect(HRA_SITE_URL).toBe("https://hra.sh"); + expect(HRA_SITE_URL).toBe("https://hra-weld.vercel.app"); expect(HRA_SUITE_ACCOUNTS_ORIGIN).toBe("https://account.hraness.com"); expect(HRA_SUITE_OIDC_CLIENT_ID).toBe("hraness:hra:production:v1"); expect(HRA_SUITE_OIDC_CALLBACK_URL).toBe( - "https://hra.sh/api/suite-auth/callback", + "https://hra-weld.vercel.app/api/suite-auth/callback", ); expect(HRA_SUITE_OIDC_PROVIDER.issuer).toBe(HRA_SUITE_ACCOUNTS_ORIGIN); expect(hraSuiteAccountUrl("account")).toBe( diff --git a/hra-legacy-identifiers.manifest.json b/hra-legacy-identifiers.manifest.json index 1451364..727e617 100644 --- a/hra-legacy-identifiers.manifest.json +++ b/hra-legacy-identifiers.manifest.json @@ -2,7 +2,7 @@ "entries": [ { "category": "compatibility", - "matchingLinesSha256": "dbb6ca57d23291846906756f03ae499bc9c7d0a1a739d88d026624b71838801c", + "matchingLinesSha256": "859491b2204ff629f260b5c297b90d6d19e3a03f6c2f71d39382f387f8002525", "occurrences": { "kitchen": 1, "operateStylized": 0, @@ -1932,24 +1932,14 @@ }, { "category": "compatibility", - "matchingLinesSha256": "df639c666e11f0546c5581d47d804fa5eaecc1b9afd15c65f10512acb0aeeb40", + "matchingLinesSha256": "9444393a7880aa0fdfa173050cd6736cfbd2bb8197207aced374753650ab9ca6", "occurrences": { - "kitchen": 2, + "kitchen": 1, "operateStylized": 0, - "oprte": 16 + "oprte": 3 }, "path": "apps/web/README.md" }, - { - "category": "compatibility", - "matchingLinesSha256": "036e28462b9140134091226a42d8e3eefc555af2419a8d6e8064e7db4e1e4712", - "occurrences": { - "kitchen": 0, - "operateStylized": 0, - "oprte": 1 - }, - "path": "apps/web/app/download/page.tsx" - }, { "category": "compatibility", "matchingLinesSha256": "462e3a6a9296a41151c135959074225eac3e4a8bb62f1ad31ca7f319e2f24984", @@ -1970,26 +1960,6 @@ }, "path": "apps/web/app/hosted-mutation-attempt-journal.ts" }, - { - "category": "compatibility", - "matchingLinesSha256": "85607be23ebddd945ce36063c8da6a3a04a1c139cea1b2105604bf79d4586cb3", - "occurrences": { - "kitchen": 0, - "operateStylized": 0, - "oprte": 1 - }, - "path": "apps/web/app/landing.test.ts" - }, - { - "category": "compatibility", - "matchingLinesSha256": "d0a500b1c4ff0c643e1bdf2d64157390b29ef6d01774a95ba6be829c7d4ad691", - "occurrences": { - "kitchen": 0, - "operateStylized": 0, - "oprte": 1 - }, - "path": "apps/web/app/public-markdown.ts" - }, { "category": "compatibility", "matchingLinesSha256": "f1a9328d3bb1ba0e23b29226225f8b995f382e39fc46c966476f67c8db11f87d", @@ -2262,7 +2232,7 @@ }, { "category": "compatibility", - "matchingLinesSha256": "7015f2a93be67fcb1a49bdb14f80599645259a4f6a637210c6128f843d0fafd0", + "matchingLinesSha256": "7505d683b6d35e25ffe20d7985795033b8c382630fd9ca8937c8aa090a777446", "occurrences": { "kitchen": 0, "operateStylized": 0, @@ -2272,7 +2242,7 @@ }, { "category": "compatibility", - "matchingLinesSha256": "75bdff64058b8ce1ca5d348142c9e34a455d8bad89c08934bcb7bfba93dfc25f", + "matchingLinesSha256": "9c3380188e58d1d9c767fe244c8bdb8bc09e648cdc393f42fbfd6524f92ca178", "occurrences": { "kitchen": 0, "operateStylized": 0, diff --git a/kb/plans/hra-task-dispatch-prompt.md b/kb/plans/hra-task-dispatch-prompt.md index b0cae95..efe60d4 100644 --- a/kb/plans/hra-task-dispatch-prompt.md +++ b/kb/plans/hra-task-dispatch-prompt.md @@ -16,6 +16,9 @@ repository_scopes: # One workflow prompt for local and cloud task dispatch +> Archived HRA v0 plan. Current HRA development continues in +> . + ## Outcome Every local and cloud task dispatch starts from the same deterministic, @@ -224,7 +227,7 @@ bundled-Git cases and one session-sync case. Their untouched files passed build passed for desktop, CLI, and web. GitHub Actions run -[`32422524366`](https://github.com/hraness/hra/actions/runs/32422524366) +[`32422524366`](https://github.com/hraness/hra-v0/actions/runs/32422524366) then passed the full source, test, production-build, native macOS test, ad-hoc package, macOS 26 Codex signature, and aggregate `Required` gates on implementation commit `319fca3698eff1e1959bd5e20f9ca359003efe3d`. The @@ -288,7 +291,7 @@ The proposal, implementation, and public boundary each received an independent review. Their findings are resolved. A final complete-diff review found no actionable correctness, parity, durability, privacy, ownership, attribution, or test-coverage issue. Pull request -[`#23`](https://github.com/hraness/hra/pull/23) is the delivery boundary; its +[`#23`](https://github.com/hraness/hra-v0/pull/23) is the delivery boundary; its terminalizing commit must retain a clean diff, a passing current-head `Required` check, no unresolved review thread, and mergeability before merge. diff --git a/package.json b/package.json index e47977a..8a05b7f 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git" + "url": "git+https://github.com/hraness/hra-v0.git" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "keywords": [ "codex", diff --git a/packages/human-client/package.json b/packages/human-client/package.json index 794c2e3..fbc9c2d 100644 --- a/packages/human-client/package.json +++ b/packages/human-client/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/human-client" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/brand-ui/package.json b/packages/internal/brand-ui/package.json index 03ce443..8275148 100644 --- a/packages/internal/brand-ui/package.json +++ b/packages/internal/brand-ui/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/brand-ui" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "sideEffects": ["./src/*.css"], diff --git a/packages/internal/browser-storage/package.json b/packages/internal/browser-storage/package.json index 59422be..4fbb566 100644 --- a/packages/internal/browser-storage/package.json +++ b/packages/internal/browser-storage/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/browser-storage" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/codex-app-sdk/package.json b/packages/internal/codex-app-sdk/package.json index 2c1de0f..1a9ddbe 100644 --- a/packages/internal/codex-app-sdk/package.json +++ b/packages/internal/codex-app-sdk/package.json @@ -6,12 +6,12 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/codex-app-sdk" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "sideEffects": [ diff --git a/packages/internal/convex/package.json b/packages/internal/convex/package.json index 41289a7..2f2daca 100644 --- a/packages/internal/convex/package.json +++ b/packages/internal/convex/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/convex" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/design-kit/package.json b/packages/internal/design-kit/package.json index 525c478..58088b5 100644 --- a/packages/internal/design-kit/package.json +++ b/packages/internal/design-kit/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/design-kit" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "sideEffects": ["./src/*.css", "./vendor/jelly-ui/jelly.js"], diff --git a/packages/internal/eslint-config/package.json b/packages/internal/eslint-config/package.json index 2a6f868..a74f3b3 100644 --- a/packages/internal/eslint-config/package.json +++ b/packages/internal/eslint-config/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/eslint-config" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/schema/package.json b/packages/internal/schema/package.json index fb67650..e09a81d 100644 --- a/packages/internal/schema/package.json +++ b/packages/internal/schema/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/schema" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/test/package.json b/packages/internal/test/package.json index a6bc34c..2f352cc 100644 --- a/packages/internal/test/package.json +++ b/packages/internal/test/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/test" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/internal/typescript-config/package.json b/packages/internal/typescript-config/package.json index 7b61756..2397b63 100644 --- a/packages/internal/typescript-config/package.json +++ b/packages/internal/typescript-config/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/internal/typescript-config" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/task-domain/package.json b/packages/task-domain/package.json index e76445b..2522686 100644 --- a/packages/task-domain/package.json +++ b/packages/task-domain/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/task-domain" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/task-protocol/package.json b/packages/task-protocol/package.json index 7614e12..12662ad 100644 --- a/packages/task-protocol/package.json +++ b/packages/task-protocol/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/task-protocol" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "exports": { diff --git a/packages/task-ui/package.json b/packages/task-ui/package.json index 36b43a3..9cd65ca 100644 --- a/packages/task-ui/package.json +++ b/packages/task-ui/package.json @@ -5,12 +5,12 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/hraness/hra.git", + "url": "git+https://github.com/hraness/hra-v0.git", "directory": "packages/task-ui" }, - "homepage": "https://hra.sh", + "homepage": "https://hra-weld.vercel.app", "bugs": { - "url": "https://github.com/hraness/hra/issues" + "url": "https://github.com/hraness/hra-v0/issues" }, "type": "module", "sideEffects": ["./src/styles.css"], diff --git a/release-download.json b/release-download.json index 5c804af..388bdce 100644 --- a/release-download.json +++ b/release-download.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "repository": "https://github.com/hraness/hra", + "repository": "https://github.com/hraness/hra-v0", "release": { "architecture": "Apple Silicon", "artifacts": { diff --git a/release-history.json b/release-history.json new file mode 100644 index 0000000..78b40f9 --- /dev/null +++ b/release-history.json @@ -0,0 +1,179 @@ +{ + "schemaVersion": 1, + "generation": 0, + "publicationCommit": "6221f79b745f154882080936b961ff431569f33e", + "repository": "https://github.com/hraness/hra-v0", + "repositoryId": 1334876494, + "tags": [ + { + "tag": "v0.1.7", + "version": "0.1.7", + "build": 8, + "tagObject": "b9789e1104b6943a36edcc7d61b28635141e3be0", + "commit": "4fa78a8c6141446be343be13df056381c3b5a224", + "objectKind": "annotated", + "release": { + "id": 371487477, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-17T02:46:24Z", + "assets": [ + { "id": 517449745, "name": "HRA-0.1.7-8-macos-arm64.dmg", "bytes": 212183245, "sha256": "a57594b66aab1db5a7dc4aebd34fb50b345242a73c03488731077618278cd639" }, + { "id": 517449738, "name": "HRA-0.1.7-8-macos-arm64.dmg.sha256", "bytes": 94, "sha256": "c077ef1ad797a3fa903f1329acf93c16bc0505508cc93d2ba671727988c69415" }, + { "id": 517449744, "name": "HRA-0.1.7-8-release-manifest.json", "bytes": 28178, "sha256": "507c08beccd798c3685d467329a05265fcc2bd8a1fe562954f5319d939c8d56a" }, + { "id": 517449746, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 517449737, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 517449752, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 517449757, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.8", + "version": "0.1.8", + "build": 9, + "tagObject": "0b3d3aa7e88e5537cc4b6f85b2dbf3969dd12c60", + "commit": "a3a142452921b9b9299d880f251d66dbe51c823b", + "objectKind": "annotated", + "release": { + "id": 371977411, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-17T22:06:30Z", + "assets": [ + { "id": 518557736, "name": "HRA-0.1.8-9-macos-arm64.dmg", "bytes": 210477806, "sha256": "e87945b7571b83e4a042553655cd77b12bdaa12e08d6f368f95b48e59939eb5c" }, + { "id": 518557732, "name": "HRA-0.1.8-9-macos-arm64.dmg.sha256", "bytes": 94, "sha256": "8287c9d18682a680b4903fcc21a658a6425027ddc3d0062bf005056ab1c0ab10" }, + { "id": 518557734, "name": "HRA-0.1.8-9-release-manifest.json", "bytes": 28178, "sha256": "7304dce3a066c8b1a5664a8640ae5a13f723d4687e27d28d22c80fd0936fa520" }, + { "id": 518557735, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 518557733, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 518557742, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 518557741, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.9", + "version": "0.1.9", + "build": 10, + "tagObject": "1b62548881f190272d0095f3900d683f43c34a69", + "commit": "531eb23e165852e2921282862c019770bb3eb914", + "objectKind": "annotated", + "release": { + "id": 372074589, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-18T03:40:01Z", + "assets": [ + { "id": 518880485, "name": "HRA-0.1.9-10-macos-arm64.dmg", "bytes": 211501935, "sha256": "51ab5bb5138066a443a5c81db3657396bb4ec9dcced5462b82752f3e87caf45b" }, + { "id": 518880488, "name": "HRA-0.1.9-10-macos-arm64.dmg.sha256", "bytes": 95, "sha256": "99314f9ba714e2a51ce1b4756497a420d4176a3e6545003e46c4000cdec7298b" }, + { "id": 518880486, "name": "HRA-0.1.9-10-release-manifest.json", "bytes": 28181, "sha256": "d05c8bc088e6d9990dbbffcce336d525fe90288adc5d3666c77336f2924ad848" }, + { "id": 518880496, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 518880500, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 518880547, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 518880539, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.10", + "version": "0.1.10", + "build": 11, + "tagObject": "9abeb033d6d965bf214b062c3c6266bc600ec76b", + "commit": "2457962b31b873b9b0521ca5606b9ad3746404de", + "objectKind": "annotated", + "release": { + "id": 372110643, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-18T05:45:51Z", + "assets": [ + { "id": 518993763, "name": "HRA-0.1.10-11-macos-arm64.dmg", "bytes": 211494496, "sha256": "0c5fac9eaa8ef1e8d7b94a63448dc43e2d813f86e1445338d21bc9339b85a6de" }, + { "id": 518993765, "name": "HRA-0.1.10-11-macos-arm64.dmg.sha256", "bytes": 96, "sha256": "a241f3276279f0301c5b641be5b1040538cd16944f707d8e55bc06459a286053" }, + { "id": 518993761, "name": "HRA-0.1.10-11-release-manifest.json", "bytes": 28184, "sha256": "ef6f67762b8d28233a663b7362a6c4c78a28c272c086d203d194408a5af546fa" }, + { "id": 518993767, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 518993766, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 518993775, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 518993779, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.11", + "version": "0.1.11", + "build": 12, + "tagObject": "e4c171e33e414d74a36791fc8577cbfbcef8e52e", + "commit": "5a2a9842cacc75fee42ab8e23ca8c215a643e21e", + "objectKind": "annotated", + "release": null + }, + { + "tag": "v0.1.12", + "version": "0.1.12", + "build": 13, + "tagObject": "626be494d24733d12e53d09932cb5cc6218bc2fe", + "commit": "9ab991d08d1507fd73c9e7ef5fb4a37baee9c014", + "objectKind": "annotated", + "release": { + "id": 374867227, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-22T07:54:37Z", + "assets": [ + { "id": 524806089, "name": "HRA-0.1.12-13-macos-arm64.dmg", "bytes": 213069200, "sha256": "246edd7ce995f7e5318aded0d35064041c0a062a98d4cd781ce746133f877dd4" }, + { "id": 524806092, "name": "HRA-0.1.12-13-macos-arm64.dmg.sha256", "bytes": 96, "sha256": "1a7f4d353e8eaf7e2537b15828dc0b803528cd60ed396275e368b3610deae042" }, + { "id": 524806088, "name": "HRA-0.1.12-13-release-manifest.json", "bytes": 31307, "sha256": "908f43a41ae2ff3def55518a9d06a35c57c6d14beb358502d3d41a8baabcdf1b" }, + { "id": 524806084, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 524806090, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 524806100, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 524806101, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.13", + "version": "0.1.13", + "build": 14, + "tagObject": "44f00fd5c5e00bc8dcded0c9b176a8e37ada90f3", + "commit": "9ba06a441c9b12b448cfe34784432592dbeccb19", + "objectKind": "annotated", + "release": { + "id": 374920071, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-22T12:10:37Z", + "assets": [ + { "id": 525000691, "name": "HRA-0.1.13-14-macos-arm64.dmg", "bytes": 212565759, "sha256": "db25ea7d9a814e568bbf34e168a40d085fc1a5bfb7890648a1d001a328544772" }, + { "id": 525000692, "name": "HRA-0.1.13-14-macos-arm64.dmg.sha256", "bytes": 96, "sha256": "e7a4a464d1fb8ab8007913b201b65956d1b24905c09645a7af123fd7b6f8fb3f" }, + { "id": 525000694, "name": "HRA-0.1.13-14-release-manifest.json", "bytes": 31307, "sha256": "ba815e5245014cbbc44e2dcaf1ed6f32701257fb5c42f839e18ba43efe9e1fdf" }, + { "id": 525000690, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 525000693, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 525000699, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 525000696, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + }, + { + "tag": "v0.1.14", + "version": "0.1.14", + "build": 15, + "tagObject": "37ed37afb39cacfd6a51044cf7f3c1b873571aa3", + "commit": "7b39c459827b2acf45aa2d911c94fdb5d4f37860", + "objectKind": "annotated", + "release": { + "id": 374980441, + "immutable": true, + "prerelease": true, + "publishedAt": "2026-08-22T16:46:26Z", + "assets": [ + { "id": 525223753, "name": "HRA-0.1.14-15-macos-arm64.dmg", "bytes": 213613720, "sha256": "7ff49500de3d1fc768c17454ef7642c51f6662dfa5bf0e2ba183a85bb67fcd03" }, + { "id": 525223750, "name": "HRA-0.1.14-15-macos-arm64.dmg.sha256", "bytes": 96, "sha256": "3ac713a656f9caf4c04c257909f1e6a4289638a45e34fee0c00bc5dbaba7dc51" }, + { "id": 525223751, "name": "HRA-0.1.14-15-release-manifest.json", "bytes": 31307, "sha256": "21346db3de75d0f2edf289fc4088cda7e839e3bb9760e3b080b704cce220f0fb" }, + { "id": 525223749, "name": "bun-0d9b296af33f2b851fcbf4df3e9ec89751734ba4-source.tar.gz", "bytes": 168162466, "sha256": "3c349132dee8226d33ec169062064e66cc292a1bcb05ccb19fed84f435eac529" }, + { "id": 525223752, "name": "bun-webkit-5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b-source.tar.gz", "bytes": 1975795848, "sha256": "797d0f9dd1ba58325e198359a1d55f3070b792d2e70069fbac5c97e784e9a05c" }, + { "id": 525223760, "name": "dugite-native-f49d0098409aa243de8b9162127025ab0bb07a88-source.tar.gz", "bytes": 44387, "sha256": "9467050831b32aa3086f8fcb76990f99f081e819c54518cb4cb5e44f3bdd796e" }, + { "id": 525223758, "name": "git-67ad42147a7acc2af6074753ebd03d904476118f-source.tar.gz", "bytes": 12659571, "sha256": "cc7f69bb55dbfad74a7914a3616a2dd1d50779a6ed483f3e823127a3b6a92977" } + ] + } + } + ] +} diff --git a/scripts/check-public-boundary.test.ts b/scripts/check-public-boundary.test.ts index fd3ea1e..5d9833d 100644 --- a/scripts/check-public-boundary.test.ts +++ b/scripts/check-public-boundary.test.ts @@ -86,6 +86,11 @@ describe("public repository boundary", () => { test("allows reviewed UI tokens, public client origins, and runtime compatibility bytes", () => { expect(publicBoundaryErrors([ + { + kind: "file", + path: "release-history.json", + source: ['{"repository', 'Id":', "13348", "76494}"].join(""), + }, { kind: "file", path: "packages/ui/styles.css", diff --git a/scripts/check-public-boundary.ts b/scripts/check-public-boundary.ts index 89750dc..3012b65 100644 --- a/scripts/check-public-boundary.ts +++ b/scripts/check-public-boundary.ts @@ -117,6 +117,9 @@ const legacyExcludedPaths = new Set([ "scripts/check-public-boundary.ts", "scripts/public-tree.manifest.json", ]); +const reviewedPublicRepositoryIdentifierPaths = new Set([ + "release-history.json", +]); export type PublicEntryKind = "directory" | "file" | "special" | "symlink"; @@ -222,7 +225,10 @@ function sourceErrors(path: string, source: string): readonly string[] { errors.push(`${path}: contains a provider deployment identifier`); } numericRepositoryIdentifierPattern.lastIndex = 0; - if (numericRepositoryIdentifierPattern.test(source)) { + if ( + numericRepositoryIdentifierPattern.test(source) + && !reviewedPublicRepositoryIdentifierPaths.has(path) + ) { errors.push(`${path}: contains a numeric repository identifier`); } forbiddenCredentialPattern.lastIndex = 0; diff --git a/scripts/check-public-structure.test.ts b/scripts/check-public-structure.test.ts index 8f2d6cb..ce6a44e 100644 --- a/scripts/check-public-structure.test.ts +++ b/scripts/check-public-structure.test.ts @@ -5,9 +5,9 @@ import { type WorkspaceManifest, } from "./check-public-structure"; -const repositoryUrl = "git+https://github.com/hraness/hra.git"; -const homepage = "https://hra.sh"; -const bugs = { url: "https://github.com/hraness/hra/issues" } as const; +const repositoryUrl = "git+https://github.com/hraness/hra-v0.git"; +const homepage = "https://hra-weld.vercel.app"; +const bugs = { url: "https://github.com/hraness/hra-v0/issues" } as const; const names = new Map([ ["apps/cli", "@hraness/hra-cli"], diff --git a/scripts/check-public-structure.ts b/scripts/check-public-structure.ts index 64adee9..e1d9e1b 100644 --- a/scripts/check-public-structure.ts +++ b/scripts/check-public-structure.ts @@ -3,9 +3,9 @@ import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const repositoryRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); -const publicRepositoryUrl = "git+https://github.com/hraness/hra.git"; -const publicHomepage = "https://hra.sh"; -const publicBugsUrl = "https://github.com/hraness/hra/issues"; +const publicRepositoryUrl = "git+https://github.com/hraness/hra-v0.git"; +const publicHomepage = "https://hra-weld.vercel.app"; +const publicBugsUrl = "https://github.com/hraness/hra-v0/issues"; const expectedNamedWorkspaces: ReadonlyMap = new Map([ ["apps/cli", "@hraness/hra-cli"], ["apps/desktop", "@hraness/hra"], diff --git a/scripts/public-tree.manifest.json b/scripts/public-tree.manifest.json index 0a5a8d6..0ffb597 100644 --- a/scripts/public-tree.manifest.json +++ b/scripts/public-tree.manifest.json @@ -1277,6 +1277,7 @@ "apps/desktop/runtime/prepare-package-output.ts", "apps/desktop/runtime/reactive-baseline.ts", "apps/desktop/runtime/release-download-contract.ts", + "apps/desktop/runtime/release-history-contract.ts", "apps/desktop/runtime/release-identity.ts", "apps/desktop/runtime/release-provenance.ts", "apps/desktop/runtime/run-native.ts", @@ -1769,6 +1770,7 @@ "apps/desktop/runtime/test/reasoning-summary-accumulator.test.ts", "apps/desktop/runtime/test/release-compatibility.test.ts", "apps/desktop/runtime/test/release-download-contract.test.ts", + "apps/desktop/runtime/test/release-history-contract.test.ts", "apps/desktop/runtime/test/release-provenance.test.ts", "apps/desktop/runtime/test/renamed-environment.test.ts", "apps/desktop/runtime/test/root-turn-routing-policy-v1.test.ts", @@ -1841,6 +1843,7 @@ "apps/web/.gitignore", "apps/web/AGENTS.md", "apps/web/README.md", + "apps/web/app/.well-known/hra.json/route.ts", "apps/web/app/AGENTS.md", "apps/web/app/accept-negotiation.property.test.ts", "apps/web/app/accept-negotiation.test.ts", @@ -1870,6 +1873,7 @@ "apps/web/app/convex-task-workspace-adapter-ui.test.tsx", "apps/web/app/convex-task-workspace-adapter.test.ts", "apps/web/app/convex-task-workspace-adapter.tsx", + "apps/web/app/deployment-identity.ts", "apps/web/app/design/page.tsx", "apps/web/app/download/page.tsx", "apps/web/app/error.tsx", @@ -1901,6 +1905,9 @@ "apps/web/app/proxy-routing.test.ts", "apps/web/app/public-markdown.test.ts", "apps/web/app/public-markdown.ts", + "apps/web/app/release-history.test.ts", + "apps/web/app/release-history.ts", + "apps/web/app/releases/page.tsx", "apps/web/app/robots.ts", "apps/web/app/site.ts", "apps/web/app/sitemap.ts", @@ -2446,6 +2453,7 @@ "patches/@native-sdk%2Fcli@0.5.3.patch", "patches/react-aria@3.50.0.patch", "release-download.json", + "release-history.json", "scripts/check-agent-guides.test.ts", "scripts/check-agent-guides.ts", "scripts/check-public-boundary.test.ts",