From f08b6a4e681d6f4f6415885fbe43ad3afc25e0fd Mon Sep 17 00:00:00 2001 From: Poordeveloper Date: Fri, 11 Sep 2026 16:10:18 +0800 Subject: [PATCH 1/4] ci(release): a tag becomes a draft release, verified on both platforms at that commit .github/workflows/release.yml runs on v* tags: the tag must name the workspace version; ubuntu-24.04 and a pinned macos-15 each run verify, package, the install-surface smoke and the disposable-account smoke on the tagged commit; the release job merges the two checksum manifests, re-checks every line, and creates the draft a human publishes. MACOS_MINIMUM becomes 15.0, the pinned image's major (Q16). verify-release's multi-platform line points at the workflow; README carries the install command, the support claim and uninstall; scheduled.yml's weekly macOS run is named regression evidence only. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Upz8uJ32B4RyE1AkthMQJX --- .github/workflows/release.yml | 148 ++++++++++++++++++++++++++++++++ .github/workflows/scheduled.yml | 8 +- README.md | 29 ++++++- scripts/package | 5 +- scripts/verify-release | 5 +- 5 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..eda9423 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,148 @@ +# A tag becomes a draft release a human publishes (M1 completion grill Q18). +# +# Everything here is exact-commit: the tag must name the workspace version; +# Ubuntu and macOS both run ./scripts/verify on the tagged commit — a green +# scheduled run of another commit is never release evidence (Q11, Q13) — +# then package, then the install-surface smoke, then the production +# lifecycle on the runner's throwaway account. The release job only +# collects what those produced, re-checks the checksums, and creates the +# draft. CI calls the scripts; it never re-implements what they decide +# (AGENTS.md §Verification). +name: Release + +on: + push: + tags: ["v*"] + +# Keep in lockstep with ci.yml so every platform verifies with the same tool. +env: + CARGO_DENY_VERSION: 0.20.2 + +permissions: + contents: read + +jobs: + version: + name: tag names the workspace version + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + # cargo's answer, the same one scripts/package bakes into the asset + # names and the plist: a tag that disagrees with it would publish an + # archive whose contents say another version. + - name: The tag equals v + run: | + workspace="$(cargo pkgid -p corral)" + workspace="${workspace##*#}" + workspace="${workspace##*@}" + if [ "$GITHUB_REF_NAME" != "v$workspace" ]; then + echo "release: tag $GITHUB_REF_NAME does not name the workspace version $workspace; bump Cargo.toml or retag" >&2 + exit 1 + fi + echo "release: $GITHUB_REF_NAME names workspace version $workspace" + + build: + name: verify, package, smoke (${{ matrix.platform }}) + needs: version + strategy: + fail-fast: false + matrix: + include: + - platform: linux-x86_64 + runner: ubuntu-24.04 + # Pinned, never macos-latest: this image's major is the lowest + # macOS the release verifies, and MACOS_MINIMUM in scripts/package + # — LSMinimumSystemVersion and the deployment target — is that + # number (Q16). Moving the pin moves the constant, in one change. + - platform: macos-arm64 + runner: macos-15 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: cargo-${{ runner.os }}- + + # gpui's Linux backends link against these at build time. Environment + # setup only, the same list ci.yml installs. + - name: Desktop build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update -q + sudo apt-get install -y -q --no-install-recommends \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libx11-xcb-dev libfontconfig1-dev + + - name: Install cargo-deny + run: | + cargo deny --version 2>/dev/null | grep -qF "cargo-deny $CARGO_DENY_VERSION" \ + || cargo install cargo-deny --locked --version "$CARGO_DENY_VERSION" + + - run: ./scripts/verify + + # Ad hoc until the signing secrets exist (Q9): the packaging PR merges + # without credentials, a public macOS release does not ship without + # them. When they are added, export CORRAL_SIGNING_IDENTITY and + # CORRAL_NOTARY_PROFILE here from repository secrets. + - run: ./scripts/package + + - run: ./scripts/package-smoke dist + + # The runner's account is the throwaway the disposable level exists + # for: install into its real home, activate the real daemon under its + # real ~/.corral, list, uninstall, and see the daemon leave. + - run: ./scripts/package-smoke --disposable-account dist + + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.platform }} + path: | + dist/*.zip + dist/*.tar.gz + dist/SHA256SUMS + if-no-files-found: error + + release: + name: draft release + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: assets + + # One manifest over both platforms' archives, which is what install.sh + # downloads beside the archive it picks. Each platform's package run + # wrote a manifest naming only its own archive, so concatenation is the + # whole merge — and every line is re-checked against the bytes that + # actually arrived here before anything is attached. + - name: Assemble dist and re-check SHA256SUMS + run: | + mkdir dist + cp assets/dist-*/*.zip assets/dist-*/*.tar.gz dist/ + cat assets/dist-*/SHA256SUMS > dist/SHA256SUMS + (cd dist && sha256sum -c SHA256SUMS) + ls -l dist + + - name: Create the draft + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --draft --verify-tag \ + --title "Corral $GITHUB_REF_NAME" \ + --notes "$(printf 'Built from %s by the release workflow. A human publishes this draft.\n\nInstall (macOS on Apple Silicon, Ubuntu 24.04 on x86_64):\n\n curl -fsSL https://raw.githubusercontent.com/%s/main/install.sh | sh\n' "$GITHUB_SHA" "$GITHUB_REPOSITORY")" \ + dist/*.zip dist/*.tar.gz dist/SHA256SUMS diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml index c2c2146..d1107b2 100644 --- a/.github/workflows/scheduled.yml +++ b/.github/workflows/scheduled.yml @@ -1,8 +1,10 @@ # macOS coverage and scheduled evidence amplification. # -# Per-PR CI runs on Linux; macOS runs after merge and on a schedule, and is -# required by scripts/verify-release. Local development already verifies on -# macOS, and a private repository bills macOS runners at ten times the rate. +# Per-PR CI runs on Linux; macOS runs after merge and on a schedule as +# regression evidence. Release evidence is release.yml, which verifies both +# platforms on the exact tagged commit: a green run here of another commit +# never stands in for that. Local development already verifies on macOS, +# and a private repository bills macOS runners at ten times the rate. # # A failure here never retroactively invalidates merged history. It produces a # finding, may freeze the affected owner's autonomous merge pending triage, and diff --git a/README.md b/README.md index ba29113..5b864c0 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,33 @@ running on your machine, tells you which ones are blocked on you, and lets you answer them without hunting through terminals. You keep your own terminal, editor, and machines. -**Status: pre-release.** M1 is under construction and nothing is packaged -yet. See `ROADMAP.md` for what the current phase includes. +**Status: pre-release.** M1 is under construction; see `ROADMAP.md` for +what the current phase includes and what it must prove before it ships. + +## Install + +macOS on Apple Silicon, and Ubuntu 24.04 on x86_64: + +```sh +curl -fsSL https://raw.githubusercontent.com/Poordeveloper/corral/main/install.sh | sh +``` + +The installer places `Corral.app` in `~/Applications` (macOS) or Corral's +executables under `~/.local/share/corral` (Linux), links `corral` into +`~/.local/bin`, and — after saying which — enables Corral's integration +with the Claude Code and Codex installs it finds. Nothing runs in the +background until you use it. `CORRAL_VERSION=vX.Y.Z` installs that release +instead of the latest; every artifact is on the +[releases page](https://github.com/Poordeveloper/corral/releases). On +Linux the CLI and the terminal session list are what is supported; the +Desktop is built and included but not yet validated there. + +To remove it: `corral uninstall`. It refuses while Corral still manages a +running session, then takes Corral's entries back out of your agents' +configuration, stops `corrald`, and removes what install placed. Your +`~/.corral` stays unless you pass `--purge`. + +Install is not upgrade: to move to a new release, uninstall and install. ## Documentation diff --git a/scripts/package b/scripts/package index ce417e5..d23e463 100755 --- a/scripts/package +++ b/scripts/package @@ -18,8 +18,9 @@ cd "$(dirname "$0")/.." # One constant: the lowest macOS major the release workflow verifies, as # LSMinimumSystemVersion and as the deployment target of the build (grill -# Q16). The release workflow pins the runner image that fixes this number. -MACOS_MINIMUM="14.0" +# Q16). .github/workflows/release.yml pins that runner — macos-15 — and the +# pin and this number move together, in one change. +MACOS_MINIMUM="15.0" # cargo's own answer, so a version edited in Cargo.toml cannot disagree with # the one the binaries report. The id reads `…/crates/corral#0.0.0`, or diff --git a/scripts/verify-release b/scripts/verify-release index b7a3c99..dded0af 100755 --- a/scripts/verify-release +++ b/scripts/verify-release @@ -30,9 +30,12 @@ cat >&2 <<'MISSING' verify-release: INCOMPLETE — the following gates are not implemented yet: supported provider/version matrix (PR4-PR6) - multi-platform release checks (macOS + Linux) dogfood release-gate evidence (ROADMAP.md §5) +Multi-platform release checks cannot run here: one machine is one OS. They +are .github/workflows/release.yml, which runs this tree's verify, package +and smoke on Ubuntu 24.04 and macOS 15 on the exact tagged commit. + Implement the gate with the work it covers; do not make this script pass by removing the check. MISSING From 84e074635596d2a23d50a41d8ca6263cda48a080 Mon Sep 17 00:00:00 2001 From: Poordeveloper Date: Fri, 11 Sep 2026 16:19:13 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(decisions):=20Q16=20amended=20?= =?UTF-8?q?=E2=80=94=20the=20minimum=20macOS=20is=20the=20deployment=20tar?= =?UTF-8?q?get,=20not=20the=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release runner is pinned to macos-15 because macos-14 is marked deprecated, and Q16 as written would have made the support claim 15.0. The founder ruled that the runner's major does not bound what the build supports: MACOS_MINIMUM is 14.0, the deployment target the binaries compile against, verified on the pinned image. Recorded in the grill's Amendments with the Q16 row marked; README says macOS 14 or later. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Upz8uJ32B4RyE1AkthMQJX --- .github/workflows/release.yml | 9 +++++---- README.md | 2 +- .../2026-09-06-m1-completion-grill.md | 18 +++++++++++++++++- scripts/package | 10 +++++----- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eda9423..04151c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,10 +51,11 @@ jobs: include: - platform: linux-x86_64 runner: ubuntu-24.04 - # Pinned, never macos-latest: this image's major is the lowest - # macOS the release verifies, and MACOS_MINIMUM in scripts/package - # — LSMinimumSystemVersion and the deployment target — is that - # number (Q16). Moving the pin moves the constant, in one change. + # Pinned, never macos-latest: this image is where the release + # verifies. The oldest macOS the binaries run on is MACOS_MINIMUM + # in scripts/package, the deployment target they compile against + # on this image (Q16 as amended 2026-09-11); the pin can move + # without moving that number. - platform: macos-arm64 runner: macos-15 runs-on: ${{ matrix.runner }} diff --git a/README.md b/README.md index 5b864c0..8fd3c07 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ what the current phase includes and what it must prove before it ships. ## Install -macOS on Apple Silicon, and Ubuntu 24.04 on x86_64: +macOS 14 or later on Apple Silicon, and Ubuntu 24.04 on x86_64: ```sh curl -fsSL https://raw.githubusercontent.com/Poordeveloper/corral/main/install.sh | sh diff --git a/docs/decisions/2026-09-06-m1-completion-grill.md b/docs/decisions/2026-09-06-m1-completion-grill.md index befd2d9..5688e95 100644 --- a/docs/decisions/2026-09-06-m1-completion-grill.md +++ b/docs/decisions/2026-09-06-m1-completion-grill.md @@ -872,7 +872,7 @@ Digest of what round 2 froze: | Q | Ruling | |---|---| | Q15 | **(a), semantics pinned.** A false-item dispute binds only the current active attention item. No current item → non-zero exit, no journal record, message `No current attention item. If Corral failed to surface an item, use --missed.` `--missed` is a distinct statement of fact — "should have appeared, did not" — not a dispute without an id. A false positive that has already ended is never attributed by guessing the most recent item; the evidence review matches it by session and wall-clock time against `born` / `ended`. Item ids stay out of the normal product UI; a diagnostics surface, if ever needed, is its own task. | -| Q16 | **Accepted with a narrowed Linux claim.** macOS: Apple Silicon only; minimum version = the lowest macOS major M1 CI actually verifies, filled into `LSMinimumSystemVersion` after the runner image is pinned. Linux: **Ubuntu 24.04 x86_64 only** for M1 — `ubuntu-latest` plus the PRoot host prove nothing about Debian, Fedora, Arch, or older Ubuntu; a glibc baseline may replace the distro claim only if the build actually establishes one. aarch64 Linux is explicitly unsupported and unclaimed. | +| Q16 | **Amended 2026-09-11 (§Amendments): the minimum macOS is the deployment target the build compiles against, not the runner's major.** Accepted with a narrowed Linux claim. macOS: Apple Silicon only; minimum version = the lowest macOS major M1 CI actually verifies, filled into `LSMinimumSystemVersion` after the runner image is pinned. Linux: **Ubuntu 24.04 x86_64 only** for M1 — `ubuntu-latest` plus the PRoot host prove nothing about Debian, Fedora, Arch, or older Ubuntu; a glibc baseline may replace the distro claim only if the build actually establishes one. aarch64 Linux is explicitly unsupported and unclaimed. | | Q17 | **Accepted, artifact pin distinguished from installer pin.** GitHub Releases is the canonical distribution; the root `install.sh` is a bootstrap served from `main`; default = latest published, non-draft, non-prerelease; `CORRAL_VERSION=vX.Y.Z` pins the release artifact, **not** the revision of `install.sh`, which is a moving target. No installer release asset in M1 and no claim of a fully reproducible pinned installation; freezing `install.sh` at the tag arrives with a later supply-chain task. | | Q18 | **Accepted with two invariants.** (1) tag `vX.Y.Z` must mechanically equal `workspace.package.version = X.Y.Z`, else the workflow fails before any draft release. (2) draft → publish is the only human release authority: human pushes tag → CI verifies the exact tagged commit → package → smoke → checksums → draft release → release-gate evidence accepted → human publishes. "CI ran" leaves the human evidence document; the machine proves it. `0.0.0 → 0.1.0` for M1. | | Q19 | **(a), count unit pinned.** ≥ 100 trusted activations in aggregate across the declared provider set — not journal transitions, sessions, attention items, or daemon runs. Trusted activation = `to == needs_you && born.is_some() && assurance ∈ {deterministic, attested} && sealed == true`. Coverage: Claude + Codex total ≥ 100 **and** each declared-supported provider ≥ 1 trusted activation; one is not maturity, it prevents claiming a provider the window never exercised. No second per-provider threshold; missing coverage narrows the claim or extends the window (Q4). | @@ -1131,3 +1131,19 @@ database, and the advance is a human-only dedicated PR. Why: the epoch is registry schema; a runner with nothing to run is speculative infrastructure. If a migration is ever wanted, the change that needs one writes the first, as `corral-state::schema` already says. + +### 2026-09-11 — Q16: the minimum macOS is the deployment target, not the runner's major + +Founder, on being told the release runner would be pinned to `macos-15` +because `macos-14` is marked deprecated, and that Q16 would then make the +claim 15.0: 「runner是15,不影响编译支持14」. + +What changes: `MACOS_MINIMUM` in `scripts/package` — `MACOSX_DEPLOYMENT_TARGET` +and `LSMinimumSystemVersion` — is 14.0, the deployment target the release +build compiles against; the release workflow verifies on the pinned +`macos-15` image. The two numbers are no longer one: the runner pin says +where the tests ran, the constant says the oldest macOS the binaries are +built to run on. What stays: nothing runs the suite on macOS 14, so the +14.0 claim rests on the deployment target and on the founder's own +dogfood, not on CI; a macOS-14 failure report is a bug, not a contract +violation. Q16's Linux claim and its Intel exclusion are unchanged. diff --git a/scripts/package b/scripts/package index d23e463..d578dfe 100755 --- a/scripts/package +++ b/scripts/package @@ -16,11 +16,11 @@ set -euo pipefail cd "$(dirname "$0")/.." -# One constant: the lowest macOS major the release workflow verifies, as -# LSMinimumSystemVersion and as the deployment target of the build (grill -# Q16). .github/workflows/release.yml pins that runner — macos-15 — and the -# pin and this number move together, in one change. -MACOS_MINIMUM="15.0" +# One constant: the oldest macOS the binaries are built to run on, as the +# deployment target of the build and as LSMinimumSystemVersion. Not the +# release runner's major — .github/workflows/release.yml verifies on +# macos-15 and compiles against this (grill Q16 as amended 2026-09-11). +MACOS_MINIMUM="14.0" # cargo's own answer, so a version edited in Cargo.toml cannot disagree with # the one the binaries report. The id reads `…/crates/corral#0.0.0`, or From 9361ea60ace0fa2044c9749e7c9a0500e3d9fb4b Mon Sep 17 00:00:00 2001 From: Poordeveloper Date: Fri, 11 Sep 2026 16:37:07 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(decisions):=20Q7=20amended=20=E2=80=94?= =?UTF-8?q?=20the=20bundle=20identifier=20is=20com.carriez.corral?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q7 froze com.poordeveloper.corral; the founder changed it while the bundle is still unreleased. The plist template and the key the package smoke asserts follow. The Q7 transcript is left as it was and the Amendments section is the authority, which also carries the consequence for the notification probe: "the real bundle identity" it must use is this one. Now is the free moment — TCC keys authorization on the identifier, so a change after the probe would discard every grant it established. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Upz8uJ32B4RyE1AkthMQJX --- .../2026-09-06-m1-completion-grill.md | 18 +++++++++++++++++- packaging/macos/Info.plist.in | 2 +- scripts/package-smoke | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/decisions/2026-09-06-m1-completion-grill.md b/docs/decisions/2026-09-06-m1-completion-grill.md index 5688e95..e5187c5 100644 --- a/docs/decisions/2026-09-06-m1-completion-grill.md +++ b/docs/decisions/2026-09-06-m1-completion-grill.md @@ -77,7 +77,7 @@ Digest of what round 1 froze: | Q4 | **Every unresolved release-relevant noise row needs an explicit ship-time disposition**, and dogfood missed-item evidence participates. For the three miss-shaped rows the disposition is one of: fixed; correctly suppressed or reclassified; capability narrowed so the state is no longer claimed supported; documented non-systematic limitation supported by dogfood evidence. "Probably not systematic" without measurement is not available. Stronger rule: a measured condition that causes a repeatable miss inside a capability Corral still claims is systematic by construction — fix it or narrow the claim; low absolute frequency does not make it non-systematic. Genuinely intermittent misses record opportunities where measurable, confirmed misses, affected provider/version/surface, and the human disposition. No numeric miss-rate threshold is introduced; the bar stays *no systematic missed states inside a claimed supported capability*. | | Q5 | **(a).** The journal stays diagnostic, deletable, non-authoritative, outside epoch migration protection; it is not promoted to durable truth because release evaluation reads it. Retention 30 → 90 days. At the end of each window the canonical report is generated immediately and frozen into `docs/evidence/`; the durable release claim is the reviewed evidence artifact, never the local journal's survival. Any INCOMPLETE day inside the 14-consecutive-day attention window breaks continuity; counting restarts from the next complete day; missing instrumentation is never read as "probably zero bad events", and the failure is itself reliability evidence. This does not silently redefine the cohort's 4-week rule, which owns its own calendar. Invariant: *diagnostic evidence may be bounded and deletable; release evidence may never be silently incomplete.* | | Q6 | **Part 1 withdrawn 2026-09-10 (§Amendments): no migration runner, no fixture gate, no ADR; a schema change after the epoch advance is an approved reset. Parts 2 and 3 stand.** Original ruling: **Accepted in three parts, with a non-vacuous migration gate.** (1) `corral-state` gains a versioned forward migration runner: inspect the stored version; current → open; older supported → apply contiguous forward migrations, all steps and the version update in one transaction, failure → full rollback; newer than this binary → refuse; downgrade never attempted; no destructive fallback to a fresh database. `DOGFOOD_BASELINE_SCHEMA = 5` is frozen: at the epoch's advance schema 5 becomes the first protected baseline; zero real migrations exist and that is fine. A permanent representative schema-5 registry fixture is opened by the current build under `verify-release`; when the current schema exceeds 5 the complete chain must apply and the expected Corral-owned facts must survive, so a schema 6 cannot release without a working 5 → 6. Runner tests: rollback on failure, missing step rejected, newer schema rejected, old-build/new-DB refusal. (2) The local schema-1 database is dev-era state a human removes before the advance — allowed only while the epoch is `dev`; never encoded as startup behaviour; unavailable after dogfood begins. (3) The advance `dev → dogfood` is a human-only, repository-visible change in a dedicated commit/PR; an agent never advances it. Invariant: *dogfood starts only after schema 5 has become a migration-supported baseline future releases are obligated to preserve.* | -| Q7 | **Bundle accepted; identifier frozen: `com.poordeveloper.corral`.** `Corral.app/Contents/{Info.plist, MacOS/{corral-desktop, corrald, corral}}`, `CFBundleExecutable` = `corral-desktop`, the three executables siblings so sibling-only daemon resolution holds for the Desktop and for the CLI symlink after canonicalization. Install to `~/Applications/Corral.app`; `~/.local/bin/corral` → `Corral.app/Contents/MacOS/corral`. Regular Dock app; no LSUIElement conversion. | +| Q7 | **Amended 2026-09-11 (§Amendments): the identifier is `com.carriez.corral`.** Bundle accepted; identifier frozen: `com.poordeveloper.corral`. `Corral.app/Contents/{Info.plist, MacOS/{corral-desktop, corrald, corral}}`, `CFBundleExecutable` = `corral-desktop`, the three executables siblings so sibling-only daemon resolution holds for the Desktop and for the CLI symlink after canonicalization. Install to `~/Applications/Corral.app`; `~/.local/bin/corral` → `Corral.app/Contents/MacOS/corral`. Regular Dock app; no LSUIElement conversion. | | Q8 | **Install accepted; uninstall gains a safety gate.** Installer: identify OS/arch → resolve the exact release artifact → download → fetch and check the SHA-256 manifest → verify before extraction → place → CLI symlink → PATH action if needed → detect supported providers → disclose which integrations it intends to enable → `corral integration enable` per detected provider → per-provider status. An integration conflict never overwrites user-owned configuration, reports Limited awareness and the resolution path, and never rolls back an otherwise valid install. A checksum from the same release authority is integrity, not an independent trust root. **Uninstall preflight**: query runtime truth first; any managed runtime Running or Unknown → default uninstall refuses ("Corral is still managing N running sessions" / "could not verify whether U managed sessions have ended"); no destructive `--force` in M1. Successful uninstall: connect/activate → uninstall Corral-owned integrations → verify cleanup → request daemon shutdown → wait for ownership to end → remove symlink → remove `.app`/binaries; if cleanup cannot complete safely, fail before deleting binaries. Default preserves `~/.corral`; `--purge` removes state and diagnostics only after the preconditions succeed and is explicit destructive intent after `dogfood`. | | Q9 | **Modified: three build classes.** Ad-hoc signing serves local developer dogfood and proves packaging mechanics only. The external cohort requires Developer ID Application signing, Hardened Runtime as the distribution design requires, a secure timestamp, notarization, and a Gatekeeper launch test on a clean user environment — never right-click Open, quarantine removal, or a Security Settings bypass as the onboarding path. Public M1 macOS release: Developer ID + notarization is a release gate, not polish; the packaging PR may merge without credentials, the cohort and public release cannot pass without them. Architecture: universal only if Intel is claimed; if claimed, every shipped executable carries coherent x86_64 + arm64 slices and both are validated; never a universal Desktop beside an arm64-only daemon or CLI. | | Q10 | **Accepted with support-boundary wording.** `~/.local/share/corral/bin/{corral, corrald, corral-desktop}` as siblings; `~/.local/bin/corral` symlink; no `.desktop`, no Linux tray. `corral-desktop` is included and buildable but unvalidated and is neither placed on PATH nor marketed. Linux TUI/CLI supported per its tested matrix. Asset names carry the architecture; no generic `linux` artifact; only architectures actually built and tested may be claimed. | @@ -1147,3 +1147,19 @@ built to run on. What stays: nothing runs the suite on macOS 14, so the 14.0 claim rests on the deployment target and on the founder's own dogfood, not on CI; a macOS-14 failure report is a bug, not a contract violation. Q16's Linux claim and its Intel exclusion are unchanged. + +### 2026-09-11 — Q7: the bundle identifier is `com.carriez.corral` + +Founder, unprompted, while the bundle was still unreleased: +「请把com.poordevelper.corral改成com.carriez.corral」. + +What changes: `packaging/macos/Info.plist.in` and the key the package smoke +asserts. What it reaches: the notification probe this file specifies under +Q22 uses "the real bundle identity" — that identity is now +`com.carriez.corral`, and `m1-notifications` seals the mechanism against +it. The Q7 body above is the transcript of that day and is left as it was; +this section is the authority. Why now is free: TCC keys authorization on +the bundle identifier, so a change after the notification probe would +discard every grant the probe established, and nothing has been released +under the old identifier. The freeze holds from here — the identifier +becomes load-bearing at the first published release, not before. diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index ea45ba1..5d1ae4c 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -14,7 +14,7 @@ CFBundleExecutable corral-desktop CFBundleIdentifier - com.poordeveloper.corral + com.carriez.corral CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/scripts/package-smoke b/scripts/package-smoke index 4b336bc..459facf 100755 --- a/scripts/package-smoke +++ b/scripts/package-smoke @@ -71,7 +71,7 @@ case "$PLATFORM" in actual="$(plist_key "$1")" [ "$actual" = "$2" ] || fail "Info.plist $1 is '$actual', expected '$2'" } - expect_key CFBundleIdentifier com.poordeveloper.corral + expect_key CFBundleIdentifier com.carriez.corral expect_key CFBundleExecutable corral-desktop expect_key CFBundleName Corral expect_key CFBundlePackageType APPL From 9572f6046b48bce75830877b0edb4a8de1c231c3 Mon Sep 17 00:00:00 2001 From: Poordeveloper Date: Fri, 11 Sep 2026 17:11:40 +0800 Subject: [PATCH 4/4] build(package): notarization authenticates with an App Store Connect API key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key is a JSON file carrying issuer_id, key_id and private_key, read with plutil so no new dependency enters the release path, and defaulted to the maintainer's ~/.p12/api-key.json so a signed build notarizes without being told. notarytool wants the private key as a file, so the field is rebuilt into one inside a 0700 directory that goes away with the script and never lands in dist/. Only read beside a Developer ID identity: an ad-hoc bundle cannot be notarized, so an ad-hoc run — which is what verify-release does — ignores the key entirely. Stapling is validated rather than assumed, because a ticket that is issued but not attached leaves a machine offline from Apple seeing an unnotarized app. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Upz8uJ32B4RyE1AkthMQJX --- .github/workflows/release.yml | 5 +- scripts/package | 88 +++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04151c8..77843c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,8 +91,9 @@ jobs: # Ad hoc until the signing secrets exist (Q9): the packaging PR merges # without credentials, a public macOS release does not ship without - # them. When they are added, export CORRAL_SIGNING_IDENTITY and - # CORRAL_NOTARY_PROFILE here from repository secrets. + # them. When they are added: import the Developer ID identity into a + # runner keychain and export CORRAL_SIGNING_IDENTITY, and write the App + # Store Connect API key JSON to a file CORRAL_NOTARY_API_KEY names. - run: ./scripts/package - run: ./scripts/package-smoke dist diff --git a/scripts/package b/scripts/package index d578dfe..e272e28 100755 --- a/scripts/package +++ b/scripts/package @@ -11,11 +11,30 @@ # D3). Signing class follows the credentials present: none → ad hoc, which # proves the packaging mechanics and nothing about Gatekeeper (grill Q9); # CORRAL_SIGNING_IDENTITY → Developer ID with hardened runtime and a secure -# timestamp; CORRAL_NOTARY_PROFILE on top → notarized and stapled. Missing -# credentials are not a failure; a failed notarization is. +# timestamp; an App Store Connect API key beside it → notarized and stapled. +# Missing credentials are not a failure; a failed notarization is. set -euo pipefail cd "$(dirname "$0")/.." +# The App Store Connect API key notarization authenticates with: JSON +# carrying `issuer_id`, `key_id`, and `private_key` — the .p8 body, with or +# without PEM armor. Defaulted to the maintainer's release-machine location +# so a signed build notarizes without being told; CI points it at a file it +# writes from a secret. Only ever read beside a Developer ID identity: an +# ad-hoc bundle cannot be notarized, so an ad-hoc run ignores it entirely. +NOTARY_API_KEY="${CORRAL_NOTARY_API_KEY:-$HOME/.p12/api-key.json}" + +# The rebuilt private key lives in one 0700 directory for the length of the +# submission and goes away with the script however it ends. It is never +# written into dist/, which is uploaded. +NOTARY_WORKDIR="" +cleanup() { + if [ -n "$NOTARY_WORKDIR" ]; then + rm -rf "$NOTARY_WORKDIR" + fi +} +trap cleanup EXIT + # One constant: the oldest macOS the binaries are built to run on, as the # deployment target of the build and as LSMinimumSystemVersion. Not the # release runner's major — .github/workflows/release.yml verifies on @@ -54,6 +73,48 @@ done rm -rf "$DIST" mkdir -p "$DIST" +# Submit the bundle and staple the ticket Apple issues. +# +# The key file is the one input: `notarytool` wants the private key as a +# file, so the JSON's `private_key` is rebuilt into one. Stapling is what +# makes the ticket travel with the archive — without it a machine that +# cannot reach Apple's service sees an unnotarized app. +notarize() { + local app="$1" + NOTARY_WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}"/corral-notary.XXXXXX)" + local key="$NOTARY_WORKDIR/AuthKey.p8" + local issuer key_id + + key_id="$(plutil -extract key_id raw -o - "$NOTARY_API_KEY")" + issuer="$(plutil -extract issuer_id raw -o - "$NOTARY_API_KEY")" + [ -n "$key_id" ] && [ -n "$issuer" ] || { + echo "package: $NOTARY_API_KEY does not carry key_id and issuer_id" >&2 + exit 1 + } + + # Tolerant of both shapes the field is found in: armored PEM, or the bare + # base64 body that has to be folded and wrapped before a parser accepts it. + if plutil -extract private_key raw -o - "$NOTARY_API_KEY" | grep -q 'BEGIN PRIVATE KEY'; then + plutil -extract private_key raw -o - "$NOTARY_API_KEY" > "$key" + else + { + echo "-----BEGIN PRIVATE KEY-----" + plutil -extract private_key raw -o - "$NOTARY_API_KEY" | tr -d '\n' | fold -w 64 + echo "" + echo "-----END PRIVATE KEY-----" + } > "$key" + fi + + echo "==> notarize (key $key_id)" + local submission="$NOTARY_WORKDIR/notarize.zip" + ditto -c -k --keepParent "$app" "$submission" + xcrun notarytool submit "$submission" \ + --key "$key" --key-id "$key_id" --issuer "$issuer" --wait + xcrun stapler staple "$app" + # Proof the ticket is attached rather than merely issued. + xcrun stapler validate "$app" +} + package_macos() { local app="$DIST/Corral.app" local archive="Corral-$VERSION-macos-arm64.zip" @@ -74,10 +135,6 @@ package_macos() { codesign --force --sign "$CORRAL_SIGNING_IDENTITY" --options runtime --timestamp "$@" } else - if [ -n "${CORRAL_NOTARY_PROFILE:-}" ]; then - echo "package: CORRAL_NOTARY_PROFILE is set without CORRAL_SIGNING_IDENTITY; an ad-hoc bundle cannot be notarized" >&2 - exit 1 - fi class="ad-hoc" sign() { codesign --force --sign - "$@" @@ -92,14 +149,15 @@ package_macos() { sign "$app" codesign --verify --deep --strict "$app" - if [ -n "${CORRAL_NOTARY_PROFILE:-}" ]; then - echo "==> notarize" - local submission="$DIST/notarize.zip" - ditto -c -k --keepParent "$app" "$submission" - xcrun notarytool submit "$submission" --keychain-profile "$CORRAL_NOTARY_PROFILE" --wait - rm -f "$submission" - xcrun stapler staple "$app" - class="$class, notarized" + # Only a Developer ID bundle is submittable; an ad-hoc one is rejected by + # Apple, so the key is not even read on that path. + if [ "$class" = "Developer ID" ]; then + if [ -f "$NOTARY_API_KEY" ]; then + notarize "$app" + class="$class, notarized" + else + echo "package: $NOTARY_API_KEY not found — signed but not notarized; a download from a browser will still be refused by Gatekeeper" + fi fi echo "==> archive $DIST/$archive" @@ -107,7 +165,7 @@ package_macos() { (cd "$DIST" && shasum -a 256 "$archive" > SHA256SUMS) echo "package: $DIST/$archive ($class)" if [ "$class" = "ad-hoc" ]; then - echo "package: ad-hoc signed — proves packaging, not Gatekeeper; set CORRAL_SIGNING_IDENTITY (and CORRAL_NOTARY_PROFILE) for a distributable build" + echo "package: ad-hoc signed — proves packaging, not Gatekeeper; set CORRAL_SIGNING_IDENTITY for a distributable build, notarized from $NOTARY_API_KEY" fi }