diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 614373e..f0dc504 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: T4 Code version - placeholder: "0.1.24" + placeholder: "0.1.25" validations: required: true - type: dropdown diff --git a/README.md b/README.md index 8160e3a..fd40fe8 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ T4 Code is a free, open-source (MIT) desktop app for [Oh My Pi](https://github.c ![T4 Code main window](docs/assets/t4-code-main.png) -[**Download v0.1.24**](https://github.com/LycaonLLC/t4-code/releases/tag/v0.1.24) · [**Docs**](https://t4code.net/docs) · [**Get the source**](#build-from-source) +[**Download v0.1.25**](https://github.com/LycaonLLC/t4-code/releases/tag/v0.1.25) · [**Docs**](https://t4code.net/docs) · [**Get the source**](#build-from-source) ## Requirements -T4 Code needs an OMP build with desktop appserver support. For v0.1.24, use the public integration build below. +T4 Code needs an OMP build with desktop appserver support. For v0.1.25, use the public integration build below. -T4 Code v0.1.24 was verified with OMP 17.0.5 built from [`772e5e41`](https://github.com/lyc-aon/oh-my-pi/commit/772e5e41eb1537177349247add96a851721c5bfa), tagged [`t4code-17.0.5-appserver-5`](https://github.com/lyc-aon/oh-my-pi/tree/t4code-17.0.5-appserver-5). That public integration is based on the official upstream [`v17.0.5`](https://github.com/can1357/oh-my-pi/tree/v17.0.5) tag at [`9fd6e971`](https://github.com/can1357/oh-my-pi/commit/9fd6e97113f5ed3a847e66d346970efdf8afcad9). It adds faster appserver startup, cross-session attention and transcript search, the negotiated browser-preview command surface, redacted Codex transport diagnostics, the versioned Agent View lifecycle contract, session-owned cancellation, macOS system-temp aliases, workspace-native build artifacts, retry-safe release metadata, lock-aware session observation, complete transcript reconciliation, missing-lock-only promotion, the cooperative `/continue-in-t4` handoff, and deterministic session ordering. Fork CI verifies the exact upstream base, ancestry, release gates, and published binaries. The official upstream v17.0.5 tag has no `appserver` command, so it cannot host T4 Code. The verified runtime is a normal build from the public `lyc-aon/oh-my-pi` source. T4 Code vendors `@oh-my-pi/app-wire` 0.6.1 from integration commit [`e3e15c03`](https://github.com/lyc-aon/oh-my-pi/commit/e3e15c03ae95ebbda5f26495cd21213cc53518b1), source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`. +T4 Code v0.1.25 was verified with OMP 17.0.5 built from [`772e5e41`](https://github.com/lyc-aon/oh-my-pi/commit/772e5e41eb1537177349247add96a851721c5bfa), tagged [`t4code-17.0.5-appserver-5`](https://github.com/lyc-aon/oh-my-pi/tree/t4code-17.0.5-appserver-5). That public integration is based on the official upstream [`v17.0.5`](https://github.com/can1357/oh-my-pi/tree/v17.0.5) tag at [`9fd6e971`](https://github.com/can1357/oh-my-pi/commit/9fd6e97113f5ed3a847e66d346970efdf8afcad9). It adds faster appserver startup, cross-session attention and transcript search, the negotiated browser-preview command surface, redacted Codex transport diagnostics, the versioned Agent View lifecycle contract, session-owned cancellation, macOS system-temp aliases, workspace-native build artifacts, retry-safe release metadata, lock-aware session observation, complete transcript reconciliation, missing-lock-only promotion, the cooperative `/continue-in-t4` handoff, and deterministic session ordering. Fork CI verifies the exact upstream base, ancestry, release gates, and published binaries. The official upstream v17.0.5 tag has no `appserver` command, so it cannot host T4 Code. The verified runtime is a normal build from the public `lyc-aon/oh-my-pi` source. T4 Code vendors `@oh-my-pi/app-wire` 0.6.1 from integration commit [`e3e15c03`](https://github.com/lyc-aon/oh-my-pi/commit/e3e15c03ae95ebbda5f26495cd21213cc53518b1), source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`. The current source tree advances the vendored contract to `@oh-my-pi/app-wire` 0.6.1 from integration commit [`e3e15c03`](https://github.com/lyc-aon/oh-my-pi/commit/e3e15c03ae95ebbda5f26495cd21213cc53518b1), source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`. It supplies bounded cross-session transcript search, historical context, and the browser-preview wire contract. @@ -20,11 +20,12 @@ The current source tree advances the vendored contract to `@oh-my-pi/app-wire` 0 | Linux | x86_64 | `.deb`, AppImage | | macOS | Apple Silicon (arm64) | `.dmg`, `.zip` (**signed and notarized**) | -No Windows build and no Intel Mac build in v0.1.24. The iOS TestFlight build is coming soon. +No Windows build and no Intel Mac build in v0.1.25. The iOS TestFlight build is coming soon. -## What changed in v0.1.24 +## What changed in v0.1.25 -- macOS downloads are now signed with the project's pinned Developer ID identity, notarized by Apple, stapled, and checked by Gatekeeper before publication. +- Signed Mac builds now install their bundled OMP backend correctly. The app verifies the backend's exact Developer ID certificate before copying the signed bytes, while packaging still verifies the original public OMP download hash. +- macOS downloads remain signed with the project's pinned Developer ID identity, notarized by Apple, stapled, and checked by Gatekeeper before publication. - The new attention inbox gathers sessions that need a decision, confirmation, or reply, while keeping the host's state authoritative. - Session transport health now explains reconnecting, delayed, and degraded connections instead of reducing them to a generic disconnected state. - Browser preview opens session-linked pages in a permission-gated workspace with bounded captures, coordinate-mapped input, and lease-based concurrency control. @@ -46,7 +47,7 @@ No Windows build and no Intel Mac build in v0.1.24. The iOS TestFlight build is ### Android 1. On the Android phone, sign in to Tailscale with an account that can reach the T4 Code host. -2. Download [`T4-Code-0.1.24-android.apk`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-android.apk). +2. Download [`T4-Code-0.1.25-android.apk`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-android.apk). 3. If Android asks, allow your browser or file manager to install unknown apps, then install the APK. 4. Open T4 Code and enter the host's HTTPS Tailscale address, including its port. The app saves the address; you can add more hosts later and switch between them. @@ -55,8 +56,8 @@ The APK does not contain an appserver or expose one to the public internet. It c ### Linux (Debian/Ubuntu) ```sh -wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-linux-amd64.deb -sudo apt install ./T4-Code-0.1.24-linux-amd64.deb +wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-linux-amd64.deb +sudo apt install ./T4-Code-0.1.25-linux-amd64.deb ``` Use `apt install` rather than `dpkg -i` so system dependencies resolve automatically. @@ -64,14 +65,14 @@ Use `apt install` rather than `dpkg -i` so system dependencies resolve automatic ### Linux (AppImage) ```sh -wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-linux-x86_64.AppImage -chmod +x T4-Code-0.1.24-linux-x86_64.AppImage -./T4-Code-0.1.24-linux-x86_64.AppImage +wget https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-linux-x86_64.AppImage +chmod +x T4-Code-0.1.25-linux-x86_64.AppImage +./T4-Code-0.1.25-linux-x86_64.AppImage ``` ### macOS (Apple Silicon) -1. Download [`T4-Code-0.1.24-mac-arm64.dmg`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-mac-arm64.dmg) (or [`T4-Code-0.1.24-mac-arm64.zip`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-mac-arm64.zip)). +1. Download [`T4-Code-0.1.25-mac-arm64.dmg`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-mac-arm64.dmg) (or [`T4-Code-0.1.25-mac-arm64.zip`](https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-mac-arm64.zip)). 2. Drag `T4 Code.app` into `/Applications`. 3. Open T4 Code normally. The release workflow verifies the pinned publisher, hardened runtime, secure timestamp, Apple notarization, stapled ticket, and Gatekeeper acceptance before publication. diff --git a/SECURITY.md b/SECURITY.md index a12a8f5..d80214d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,5 +26,5 @@ We read every report and will reply to tell you what happens next. This is a sma - T4 Code is a desktop client. The OMP runtime is a separate project; runtime vulnerabilities belong at . - Pairing credentials are encrypted with the OS keychain via Electron `safeStorage`. Reports about credential handling, the pairing flow, or the `t4-code://` deep-link handler are especially welcome. -- The macOS v0.1.24 build is signed with Apple Developer ID and notarized by Apple. Reports of certificate, Team ID, hardened-runtime, timestamp, Gatekeeper, or stapled-ticket drift are security-relevant. +- The macOS v0.1.25 build is signed with Apple Developer ID and notarized by Apple. Reports of certificate, Team ID, hardened-runtime, timestamp, Gatekeeper, or stapled-ticket drift are security-relevant. - Starting with v0.1.24, the release workflow requires the pinned Developer ID identity, hardened runtime, Apple notarization, a stapled ticket, and a successful Gatekeeper assessment before publishing macOS artifacts. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7cb6963..b42b431 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/desktop", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "main": "dist-electron/main.cjs", diff --git a/apps/desktop/src/bundled-runtime.ts b/apps/desktop/src/bundled-runtime.ts index 59e546a..ce84d3b 100644 --- a/apps/desktop/src/bundled-runtime.ts +++ b/apps/desktop/src/bundled-runtime.ts @@ -1,6 +1,17 @@ +import { execFile } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; -import { chmod, copyFile, mkdir, readFile, rename, stat, unlink } from "node:fs/promises"; +import { createReadStream } from "node:fs"; +import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, stat, unlink } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const SIGNED_RUNTIME_TEAM_ID = "WJLM3D3DK6"; +const SIGNED_RUNTIME_CERTIFICATE = "Developer ID Application: Michael Schoenberger (WJLM3D3DK6)"; +const SIGNED_RUNTIME_CERTIFICATE_AUTHORITY = "Developer ID Certification Authority"; +const SIGNED_RUNTIME_CERTIFICATE_SHA256 = + "7f946ec45aabcb895a46df233f326d8a878d1e874b2d546bfa781d4bd7b081ef"; export interface BundledRuntimeManifest { readonly version: 1; @@ -29,36 +40,118 @@ function decodeManifest(value: unknown): BundledRuntimeManifest { return record as BundledRuntimeManifest; } -async function matches(path: string, manifest: BundledRuntimeManifest): Promise { +interface RuntimeIntegrity { + readonly size: number; + readonly sha256: string; +} + +async function inspectIntegrity(path: string): Promise { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return { size: (await stat(path)).size, sha256: hash.digest("hex") }; +} + +async function matches(path: string, integrity: RuntimeIntegrity): Promise { try { - if ((await stat(path)).size !== manifest.size) return false; - const hash = createHash("sha256").update(await readFile(path)).digest("hex"); - return hash === manifest.sha256; + const actual = await inspectIntegrity(path); + return actual.size === integrity.size && actual.sha256 === integrity.sha256; } catch { return false; } } +async function verifySignedDeveloperIdRuntime(path: string): Promise { + const certificateDirectory = await mkdtemp(join(tmpdir(), "t4-runtime-certificate-")); + const certificatePrefix = join(certificateDirectory, "certificate"); + try { + await execFileAsync("/usr/bin/codesign", ["--verify", "--strict", "--verbose=2", path], { + maxBuffer: 1024 * 1024, + }); + const display = await execFileAsync( + "/usr/bin/codesign", + ["--display", "--verbose=4", `--extract-certificates=${certificatePrefix}`, path], + { maxBuffer: 1024 * 1024 }, + ); + const output = `${display.stdout}\n${display.stderr}`; + const leafCertificate = await readFile(`${certificatePrefix}0`); + const certificateSha256 = createHash("sha256").update(leafCertificate).digest("hex"); + const cdHash = /^CDHash=([0-9a-f]+)$/imu.exec(output)?.[1]; + if ( + !output.includes("Identifier=omp") || + !output.includes(`TeamIdentifier=${SIGNED_RUNTIME_TEAM_ID}`) || + !output.includes(`Authority=${SIGNED_RUNTIME_CERTIFICATE}`) || + !output.includes(`Authority=${SIGNED_RUNTIME_CERTIFICATE_AUTHORITY}`) || + (!output.includes("flags=0x10000(runtime)") && !output.includes("Runtime Version=")) || + !output.includes("Timestamp=") || + !cdHash || + certificateSha256 !== SIGNED_RUNTIME_CERTIFICATE_SHA256 + ) throw new Error("signed bundled OMP runtime identity is invalid"); + return cdHash; + } finally { + await rm(certificateDirectory, { recursive: true, force: true }); + } +} + +async function readSignedRuntimeCodeHash(path: string): Promise { + const display = await execFileAsync( + "/usr/bin/codesign", + ["--display", "--verbose=4", path], + { maxBuffer: 1024 * 1024 }, + ); + const output = `${display.stdout}\n${display.stderr}`; + const cdHash = /^CDHash=([0-9a-f]+)$/imu.exec(output)?.[1]; + if (!cdHash) throw new Error("bundled OMP runtime code identity is invalid"); + return cdHash; +} + export async function installBundledOmpRuntime(options: { readonly resourcesPath: string; readonly applicationSupportPath: string; + readonly verifySignedRuntime?: (path: string) => Promise; }): Promise { const sourceRoot = join(options.resourcesPath, "runtime"); const manifest = decodeManifest(JSON.parse(await readFile(join(sourceRoot, "manifest.json"), "utf8"))); const source = join(sourceRoot, manifest.executable); - if (!(await matches(source, manifest))) throw new Error("bundled OMP runtime failed its integrity check"); const destinationRoot = join(options.applicationSupportPath, "runtime", manifest.tag); const destination = join(destinationRoot, "omp"); - if (await matches(destination, manifest)) { - await chmod(destination, 0o755); - return destination; + const verifySignedRuntime = options.verifySignedRuntime ?? verifySignedDeveloperIdRuntime; + + try { + const destinationCdHash = await verifySignedRuntime(destination); + const sourceCdHash = options.verifySignedRuntime + ? await options.verifySignedRuntime(source) + : await readSignedRuntimeCodeHash(source); + if (destinationCdHash === sourceCdHash) { + await chmod(destination, 0o755); + return destination; + } + } catch { + if (await matches(destination, manifest)) { + await chmod(destination, 0o755); + return destination; + } + } + + let sourceIntegrity: RuntimeIntegrity; + try { + sourceIntegrity = await inspectIntegrity(source); + if ( + sourceIntegrity.size !== manifest.size || + sourceIntegrity.sha256 !== manifest.sha256 + ) { + await verifySignedRuntime(source); + } + } catch { + throw new Error("bundled OMP runtime failed its integrity check"); } await mkdir(destinationRoot, { recursive: true, mode: 0o700 }); const temporary = join(destinationRoot, `.omp-${randomUUID()}.partial`); try { await copyFile(source, temporary); await chmod(temporary, 0o755); - if (!(await matches(temporary, manifest))) throw new Error("installed OMP runtime failed its integrity check"); + if (!(await matches(temporary, sourceIntegrity))) { + throw new Error("installed OMP runtime failed its integrity check"); + } await rename(temporary, destination); } finally { await unlink(temporary).catch(() => {}); diff --git a/apps/desktop/src/target-manager.ts b/apps/desktop/src/target-manager.ts index ee1c0a8..453bb41 100644 --- a/apps/desktop/src/target-manager.ts +++ b/apps/desktop/src/target-manager.ts @@ -383,7 +383,7 @@ export class DesktopTargetManager { capabilities: requestedCapabilities, requestedFeatures: REQUESTED_FEATURES, compatibilityRequestedFeatures: COMPATIBILITY_FEATURES, - client: { name: "T4 Code", version: "0.1.24", build: "desktop", platform: process.platform }, + client: { name: "T4 Code", version: "0.1.25", build: "desktop", platform: process.platform }, reconnect: { baseMs: 250, maxMs: 10_000 }, }; const client = createOmpClient(clientOptions); diff --git a/apps/desktop/test/bundled-runtime.test.ts b/apps/desktop/test/bundled-runtime.test.ts index b6682c4..9f507d7 100644 --- a/apps/desktop/test/bundled-runtime.test.ts +++ b/apps/desktop/test/bundled-runtime.test.ts @@ -39,7 +39,59 @@ describe("bundled OMP runtime", () => { executable: "omp", size: 5, sha256: "0".repeat(64), })); - await expect(installBundledOmpRuntime({ resourcesPath, applicationSupportPath: join(root, "support") })) + await expect(installBundledOmpRuntime({ + resourcesPath, + applicationSupportPath: join(root, "support"), + verifySignedRuntime: async () => { throw new Error("not signed"); }, + })) .rejects.toThrow("integrity check"); }); + + it("installs the exact signed bytes after verifying their Developer ID identity", async () => { + const root = await mkdtemp(join(tmpdir(), "t4-bundled-runtime-signed-")); + const resourcesPath = join(root, "resources"); + const supportPath = join(root, "support"); + const runtimeRoot = join(resourcesPath, "runtime"); + await mkdir(runtimeRoot, { recursive: true }); + const unsignedBytes = Buffer.from("unsigned release artifact"); + const signedBytes = Buffer.from("signed release artifact with a code signature"); + await writeFile(join(runtimeRoot, "omp"), signedBytes); + await writeFile(join(runtimeRoot, "manifest.json"), JSON.stringify({ + version: 1, + tag: "t4code-17.0.5-appserver-5", + platform: "darwin", + arch: "arm64", + executable: "omp", + size: unsignedBytes.length, + sha256: createHash("sha256").update(unsignedBytes).digest("hex"), + })); + const verified: string[] = []; + + const installed = await installBundledOmpRuntime({ + resourcesPath, + applicationSupportPath: supportPath, + verifySignedRuntime: async (path) => { + await stat(path); + verified.push(path); + return "signed-code-directory-hash"; + }, + }); + const reused = await installBundledOmpRuntime({ + resourcesPath, + applicationSupportPath: supportPath, + verifySignedRuntime: async (path) => { + await stat(path); + verified.push(path); + return "signed-code-directory-hash"; + }, + }); + + expect(reused).toBe(installed); + expect(verified).toEqual([ + join(runtimeRoot, "omp"), + installed, + join(runtimeRoot, "omp"), + ]); + expect(await readFile(installed)).toEqual(signedBytes); + }); }); diff --git a/apps/mobile/capacitor.config.json b/apps/mobile/capacitor.config.json index 6bfc18e..258909e 100644 --- a/apps/mobile/capacitor.config.json +++ b/apps/mobile/capacitor.config.json @@ -3,7 +3,7 @@ "appName": "T4 Code", "webDir": "dist", "loggingBehavior": "debug", - "appendUserAgent": " T4CodeMobile/0.1.24", + "appendUserAgent": " T4CodeMobile/0.1.25", "android": { "path": "android", "minWebViewVersion": 60, diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8bb7b38..359c477 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/mobile", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "scripts": { diff --git a/apps/site/package.json b/apps/site/package.json index b30bc16..c00c637 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/site", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "scripts": { diff --git a/apps/site/src/release.ts b/apps/site/src/release.ts index 4962e12..6080e51 100644 --- a/apps/site/src/release.ts +++ b/apps/site/src/release.ts @@ -13,8 +13,8 @@ export const OMP_UPSTREAM_TAG = "v17.0.5"; export const OMP_UPSTREAM_COMMIT = "9fd6e97113f5ed3a847e66d346970efdf8afcad9"; export const OMP_UPSTREAM_URL = `${OMP_URL}/tree/${OMP_UPSTREAM_TAG}`; export const APP_WIRE_VERSION = "0.6.1"; -export const RELEASE_TAG = "v0.1.24"; -export const RELEASE_VERSION = "0.1.24"; +export const RELEASE_TAG = "v0.1.25"; +export const RELEASE_VERSION = "0.1.25"; export const RELEASES_URL = `${REPO_URL}/releases/tag/${RELEASE_TAG}`; export const RELEASE_MANIFEST_URL = `${SITE_URL}/releases/latest.json`; @@ -49,11 +49,11 @@ function asset( } export const RELEASE_ASSETS: readonly ReleaseAsset[] = [ - asset("android", "apk", "universal", "T4-Code-0.1.24-android.apk", "Android APK"), - asset("linux", "deb", "x86_64", "T4-Code-0.1.24-linux-amd64.deb", "Linux .deb"), - asset("linux", "appimage", "x86_64", "T4-Code-0.1.24-linux-x86_64.AppImage", "Linux AppImage"), - asset("mac", "dmg", "arm64", "T4-Code-0.1.24-mac-arm64.dmg", "macOS .dmg"), - asset("mac", "zip", "arm64", "T4-Code-0.1.24-mac-arm64.zip", "macOS .zip"), + asset("android", "apk", "universal", "T4-Code-0.1.25-android.apk", "Android APK"), + asset("linux", "deb", "x86_64", "T4-Code-0.1.25-linux-amd64.deb", "Linux .deb"), + asset("linux", "appimage", "x86_64", "T4-Code-0.1.25-linux-x86_64.AppImage", "Linux AppImage"), + asset("mac", "dmg", "arm64", "T4-Code-0.1.25-mac-arm64.dmg", "macOS .dmg"), + asset("mac", "zip", "arm64", "T4-Code-0.1.25-mac-arm64.zip", "macOS .zip"), ]; export function assetsFor(platform: Platform): readonly ReleaseAsset[] { diff --git a/apps/site/test/release.test.ts b/apps/site/test/release.test.ts index a8da30d..5b335b1 100644 --- a/apps/site/test/release.test.ts +++ b/apps/site/test/release.test.ts @@ -1,4 +1,4 @@ -// Release contract guard: exact v0.1.24 asset names and URLs, and the +// Release contract guard: exact v0.1.25 asset names and URLs, and the // platform-detection rule the hero download button relies on. import { describe, expect, it } from "vite-plus/test"; import { @@ -20,13 +20,13 @@ import { } from "../src/release.ts"; describe("release assets", () => { - it("carries the five contracted v0.1.24 filenames", () => { + it("carries the five contracted v0.1.25 filenames", () => { expect(RELEASE_ASSETS.map((a) => a.filename)).toEqual([ - "T4-Code-0.1.24-android.apk", - "T4-Code-0.1.24-linux-amd64.deb", - "T4-Code-0.1.24-linux-x86_64.AppImage", - "T4-Code-0.1.24-mac-arm64.dmg", - "T4-Code-0.1.24-mac-arm64.zip", + "T4-Code-0.1.25-android.apk", + "T4-Code-0.1.25-linux-amd64.deb", + "T4-Code-0.1.25-linux-x86_64.AppImage", + "T4-Code-0.1.25-mac-arm64.dmg", + "T4-Code-0.1.25-mac-arm64.zip", ]); }); @@ -38,8 +38,8 @@ describe("release assets", () => { it("targets the public LycaonLLC repo", () => { expect(REPO_URL).toBe("https://github.com/LycaonLLC/t4-code"); - expect(RELEASE_TAG).toBe("v0.1.24"); - expect(RELEASE_VERSION).toBe("0.1.24"); + expect(RELEASE_TAG).toBe("v0.1.25"); + expect(RELEASE_VERSION).toBe("0.1.25"); expect(RELEASE_MANIFEST_URL).toBe("https://t4code.net/releases/latest.json"); }); diff --git a/apps/web/package.json b/apps/web/package.json index 1efe08b..a9329a2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/web", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/platform/browser-shell-port.ts b/apps/web/src/platform/browser-shell-port.ts index 6577590..792679e 100644 --- a/apps/web/src/platform/browser-shell-port.ts +++ b/apps/web/src/platform/browser-shell-port.ts @@ -305,7 +305,7 @@ export function createBrowserShellPort( }, client: { name: "T4 Code", - version: "0.1.24", + version: "0.1.25", build: mobilePlatform ?? "browser", platform: mobilePlatform ?? (platform === "darwin" ? "darwin" : "linux"), }, diff --git a/compat/omp-app-matrix.json b/compat/omp-app-matrix.json index 76104b8..c6504ba 100644 --- a/compat/omp-app-matrix.json +++ b/compat/omp-app-matrix.json @@ -181,6 +181,6 @@ }, "desktop": { "package": "@t4-code/protocol", - "version": "0.1.24" + "version": "0.1.25" } } diff --git a/docs/CURRENT_RELEASE_NOTES.md b/docs/CURRENT_RELEASE_NOTES.md index 0cd7eb6..ea8a9d4 100644 --- a/docs/CURRENT_RELEASE_NOTES.md +++ b/docs/CURRENT_RELEASE_NOTES.md @@ -1,8 +1,8 @@ -## Signed and notarized on macOS +## Automatic backend installation on signed Macs -T4 Code v0.1.24 is the first macOS release signed with the project's pinned Developer ID identity and notarized by Apple. The protected release job reopens both the DMG and ZIP, checks their certificate and Team ID, hardened runtime, secure timestamp, stapled notarization ticket, and Gatekeeper result, and stops publication if any of them drift. +T4 Code v0.1.25 fixes first-launch backend installation in signed Mac builds. Apple signing adds a cryptographic signature to the bundled OMP executable, which changes its file hash after the original public download was verified. The app now accepts either the exact original download or an executable signed with the project's exact Developer ID certificate, then copies and rechecks the actual signed bytes atomically. -Local development can still produce an explicitly unsigned package without release credentials. Signing secrets are available only to the protected release job and are never bundled into the app. +The protected release job still verifies the original OMP download's pinned size and SHA-256 hash before packaging. It then reopens the DMG and ZIP and checks their certificate and Team ID, hardened runtime, secure timestamp, stapled notarization ticket, and Gatekeeper result before publication. Signing secrets are never bundled into the app. ## One inbox for sessions that need attention @@ -24,7 +24,7 @@ Session-linked browser previews now open in a dedicated workspace. The client pr ## Runtime provenance -T4 Code v0.1.24 vendors app-wire 0.6.1 from integration commit [e3e15c03](https://github.com/lyc-aon/oh-my-pi/commit/e3e15c03ae95ebbda5f26495cd21213cc53518b1), source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`. The client contract remains `omp-app/1`. +T4 Code v0.1.25 vendors app-wire 0.6.1 from integration commit [e3e15c03](https://github.com/lyc-aon/oh-my-pi/commit/e3e15c03ae95ebbda5f26495cd21213cc53518b1), source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`. The client contract remains `omp-app/1`. The verified OMP 17.0.5 runtime is built from commit [772e5e41](https://github.com/lyc-aon/oh-my-pi/commit/772e5e41eb1537177349247add96a851721c5bfa) and tagged [t4code-17.0.5-appserver-5](https://github.com/lyc-aon/oh-my-pi/tree/t4code-17.0.5-appserver-5). It provides the appserver used by the desktop and remote workflows, including faster startup, cross-session attention and transcript search, and the complete negotiated browser-preview command surface. Unsupported optional capabilities remain hidden when the host does not advertise them. diff --git a/e2e/site-mobile-docs.spec.ts b/e2e/site-mobile-docs.spec.ts index 438dc13..cccb9a1 100644 --- a/e2e/site-mobile-docs.spec.ts +++ b/e2e/site-mobile-docs.spec.ts @@ -130,7 +130,7 @@ test("offers the Android APK without hiding desktop downloads", async ({ page }) await expect(androidDownload).toBeVisible(); await expect(androidDownload).toHaveAttribute( "href", - "https://github.com/LycaonLLC/t4-code/releases/download/v0.1.24/T4-Code-0.1.24-android.apk", + "https://github.com/LycaonLLC/t4-code/releases/download/v0.1.25/T4-Code-0.1.25-android.apk", ); await expect(page.getByRole("link", { name: "Download for Linux" }).first()).toBeVisible(); await expect(page.getByRole("link", { name: "macOS build" }).first()).toBeVisible(); diff --git a/package.json b/package.json index e11aab7..14ecb6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/root", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "scripts": { diff --git a/packages/client/package.json b/packages/client/package.json index 0812897..06d3a11 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/client", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/packages/client/src/omp-client-frames.ts b/packages/client/src/omp-client-frames.ts index 81a0eaa..d21ceaa 100644 --- a/packages/client/src/omp-client-frames.ts +++ b/packages/client/src/omp-client-frames.ts @@ -84,7 +84,7 @@ export function sendClientHello( } catch { fatal(); return; } const encoded = encodeOutgoingMessage(provider, { kind: "hello", - client: options.client ?? { name: "t4-code", version: "0.1.24", build: "client", platform: "electron" }, + client: options.client ?? { name: "t4-code", version: "0.1.25", build: "client", platform: "electron" }, requestedFeatures: [...(options.requestedFeatures ?? ["resume"])], savedCursors, ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }), diff --git a/packages/fixture-server/package.json b/packages/fixture-server/package.json index 635fcb8..207c932 100644 --- a/packages/fixture-server/package.json +++ b/packages/fixture-server/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/fixture-server", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/packages/protocol/package.json b/packages/protocol/package.json index aa1a432..eb964a5 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/protocol", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/packages/protocol/test/fixtures/platform-boundaries.ts b/packages/protocol/test/fixtures/platform-boundaries.ts index ec50d7f..5665344 100644 --- a/packages/protocol/test/fixtures/platform-boundaries.ts +++ b/packages/protocol/test/fixtures/platform-boundaries.ts @@ -25,7 +25,7 @@ export const androidUpdateFixtures = Object.freeze({ }, { currentVersion: "0.1.22", - latestVersion: "0.1.24", + latestVersion: "0.1.25", checkedAt: 1_721_234_567_890, phase: "available", revision: 7, @@ -33,7 +33,7 @@ export const androidUpdateFixtures = Object.freeze({ }, { currentVersion: "0.1.22", - latestVersion: "0.1.24", + latestVersion: "0.1.25", phase: "installer", revision: 8, message: "Installer opened.\nReview Android's prompt.", diff --git a/packages/remote/package.json b/packages/remote/package.json index 161817e..9f2b0b6 100644 --- a/packages/remote/package.json +++ b/packages/remote/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/remote", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/packages/service-manager/package.json b/packages/service-manager/package.json index 5aa5ec8..2dbbe02 100644 --- a/packages/service-manager/package.json +++ b/packages/service-manager/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/service-manager", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 276b3ae..b8eab63 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@t4-code/ui", - "version": "0.1.24", + "version": "0.1.25", "private": true, "type": "module", "exports": { diff --git a/scripts/check-release-consistency.test.mjs b/scripts/check-release-consistency.test.mjs index dfb4b6a..2100397 100644 --- a/scripts/check-release-consistency.test.mjs +++ b/scripts/check-release-consistency.test.mjs @@ -89,7 +89,7 @@ test("keeps verified and published runtime records aligned after promotion", () test("rejects a tag that differs from the package version", () => { assert.ok( collectReleaseConsistencyErrors(files, "v9.9.9").some((error) => - error.includes("release tag v9.9.9 does not match v0.1.24"), + error.includes("release tag v9.9.9 does not match v0.1.25"), ), ); }); @@ -118,7 +118,7 @@ test("tagged releases reject published provenance drift", () => { for (const [field, mutate] of appWireCases) { const drifted = changedRuntime("publishedAppWire", mutate); assert.ok( - collectReleaseConsistencyErrors(drifted, "v0.1.24").some((error) => + collectReleaseConsistencyErrors(drifted, "v0.1.25").some((error) => error.includes( `published app-wire ${field} must match current app-wire for tagged releases`, ), @@ -161,7 +161,7 @@ test("tagged releases reject published provenance drift", () => { for (const [field, mutate] of runtimeCases) { const drifted = changedRuntime("publishedRuntime", mutate); assert.ok( - collectReleaseConsistencyErrors(drifted, "v0.1.24").some((error) => + collectReleaseConsistencyErrors(drifted, "v0.1.25").some((error) => error.includes( `published runtime ${field} must match current verified runtime for tagged releases`, ), @@ -173,7 +173,7 @@ test("tagged releases reject published provenance drift", () => { runtime.artifactSha256 = "0".repeat(64); }); assert.ok( - collectReleaseConsistencyErrors(extended, "v0.1.24").some((error) => + collectReleaseConsistencyErrors(extended, "v0.1.25").some((error) => error.includes( "published runtime must exactly match current verified runtime for tagged releases", ), @@ -183,15 +183,15 @@ test("tagged releases reject published provenance drift", () => { test("rejects workspace, site, README, and runtime version drift", () => { const cases = [ - ["apps/web/package.json", (text) => text.replace('"version": "0.1.24"', '"version": "0.1.3"')], + ["apps/web/package.json", (text) => text.replace('"version": "0.1.25"', '"version": "0.1.3"')], [ "apps/site/src/release.ts", - (text) => text.replace('RELEASE_TAG = "v0.1.24"', 'RELEASE_TAG = "v0.1.3"'), + (text) => text.replace('RELEASE_TAG = "v0.1.25"', 'RELEASE_TAG = "v0.1.3"'), ], - ["README.md", (text) => text.replace("Download v0.1.24", "Download v0.1.3")], + ["README.md", (text) => text.replace("Download v0.1.25", "Download v0.1.3")], [ "apps/desktop/src/target-manager.ts", - (text) => text.replace('version: "0.1.24"', 'version: "0.1.3"'), + (text) => text.replace('version: "0.1.25"', 'version: "0.1.3"'), ], [ "apps/site/src/docs/content.ts", @@ -461,7 +461,7 @@ test("rejects stale README release URLs while allowing historical prose", () => const staleLink = changed("README.md", (text) => `${text}\n[Old release](${oldReleaseUrl})\n`); assert.ok( collectReleaseConsistencyErrors(staleLink).some((error) => - error.includes("release URL for v0.1.3; expected v0.1.24"), + error.includes("release URL for v0.1.3; expected v0.1.25"), ), ); assert.deepEqual(collectReleaseConsistencyErrors(files), []);