Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -55,23 +56,23 @@ 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.

### 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.

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/can1357/oh-my-pi>.
- 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.
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
80 changes: 72 additions & 8 deletions apps/desktop/src/bundled-runtime.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -29,27 +40,78 @@ function decodeManifest(value: unknown): BundledRuntimeManifest {
return record as BundledRuntimeManifest;
}

async function matches(path: string, manifest: BundledRuntimeManifest): Promise<boolean> {
interface RuntimeIntegrity {
readonly size: number;
readonly sha256: string;
}

async function inspectIntegrity(path: string): Promise<RuntimeIntegrity> {
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<boolean> {
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<void> {
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");
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=") ||
certificateSha256 !== SIGNED_RUNTIME_CERTIFICATE_SHA256
) throw new Error("signed bundled OMP runtime identity is invalid");
} finally {
await rm(certificateDirectory, { recursive: true, force: true });
}
}

export async function installBundledOmpRuntime(options: {
readonly resourcesPath: string;
readonly applicationSupportPath: string;
readonly verifySignedRuntime?: (path: string) => Promise<void>;
}): Promise<string> {
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");
let sourceIntegrity: RuntimeIntegrity;
try {
sourceIntegrity = await inspectIntegrity(source);
if (
Comment thread
wolfiesch marked this conversation as resolved.
sourceIntegrity.size !== manifest.size ||
sourceIntegrity.sha256 !== manifest.sha256
) {
await (options.verifySignedRuntime ?? verifySignedDeveloperIdRuntime)(source);
}
} catch {
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)) {
if (await matches(destination, sourceIntegrity)) {
await chmod(destination, 0o755);
return destination;
}
Expand All @@ -58,7 +120,9 @@ export async function installBundledOmpRuntime(options: {
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(() => {});
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/target-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 35 additions & 1 deletion apps/desktop/test/bundled-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,41 @@ 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) => { verified.push(path); },
});

expect(verified).toEqual([join(runtimeRoot, "omp")]);
expect(await readFile(installed)).toEqual(signedBytes);
});
});
2 changes: 1 addition & 1 deletion apps/mobile/capacitor.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t4-code/mobile",
"version": "0.1.24",
"version": "0.1.25",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/site/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t4-code/site",
"version": "0.1.24",
"version": "0.1.25",
"private": true,
"type": "module",
"scripts": {
Expand Down
14 changes: 7 additions & 7 deletions apps/site/src/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -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[] {
Expand Down
Loading
Loading