Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
169 changes: 169 additions & 0 deletions .agents/skills/apple-notarization-setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
name: apple-notarization-setup
description: Use when setting up or verifying Apple Developer ID signing credentials, Apple app-specific passwords, kinko secret storage, or local macOS notarization readiness for chilla without recording credential values.
---

# Apple Notarization Setup

Use this for chilla macOS signing/notarization setup before local deploys or release builds. Keep all credential values out of logs, skill files, commits, and final responses.

## Credential Safety

- Never print, paste, commit, or summarize actual Apple passwords, app-specific passwords, certificate passwords, private keys, `.p12` contents, or kinko secret values.
- It is acceptable to mention secret key names such as `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, and `APPLE_SIGNING_IDENTITY`.
- When a private login, passkey, or 2FA step is needed, use Computer Use to navigate to the prompt, then ask the user to enter it directly in the browser or system dialog.
- Use `kinko exec --env ...` for commands that need secrets. Do not use commands that echo exported secret values.

## Required Local Inputs

The local notarization path expects:

- A valid Developer ID Application certificate imported into the macOS login keychain.
- `APPLE_SIGNING_IDENTITY` stored in kinko.
- `APPLE_ID` stored in kinko.
- `APPLE_TEAM_ID` stored in kinko.
- `APPLE_PASSWORD` stored in kinko as an Apple app-specific password.

Check presence only:

```bash
kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc '
for key in APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
if [ -n "${!key:-}" ]; then echo "$key=present"; else echo "$key=missing"; fi
done
'
```

Check the local certificate:

```bash
security find-identity -v -p codesigning
```

Expect a valid `Developer ID Application` identity matching `APPLE_SIGNING_IDENTITY`.

## App-Specific Password Flow

Use Computer Use for browser navigation when requested.

1. Open `https://account.apple.com/account/manage`.
2. Ask the user to complete Apple Account login, passkey, and 2FA directly in Safari.
3. Open `Sign-In and Security`.
4. Open `App-Specific Passwords`.
5. Generate a password with a label such as `Chilla`.
6. Store it in kinko as `APPLE_PASSWORD`.
7. Do not include the generated password in chat, commits, or files.

Use kinko storage:

```bash
kinko set-key APPLE_PASSWORD --value '<app-specific-password>'
```

After storage, avoid showing the value again. If the browser dialog is still open, close it after confirming the password is stored.

## Local Build And Notarization

Prefer the existing macOS release skill and tasks for actual packaging. The common local command is:

```bash
kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- task bundle-macos-dmg
```

This builds:

- `target/release/bundle/macos/chilla.app`
- `target/release/bundle/dmg/*.dmg` after notarization and DMG packaging complete

Cargo commands must use `CARGO_TERM_QUIET=true` when invoked directly.

## Nix Environment Notarytool Workaround

In this repository's Nix shell, `xcrun` may point at the Nix Apple SDK and fail with:

```text
tool 'notarytool' not found
```

Do not replace the whole build environment with the system Xcode `DEVELOPER_DIR`; that can break Nix SDK linking. Instead, create a temporary `notarytool` wrapper and keep the Nix build environment intact:

```bash
tmp_notary_dir=/tmp/chilla-notarytool-wrapper
mkdir -p "$tmp_notary_dir"
printf '%s\n' \
'#!/usr/bin/env bash' \
'exec /Applications/Xcode.app/Contents/Developer/usr/bin/notarytool "$@"' \
> "$tmp_notary_dir/notarytool"
chmod 700 "$tmp_notary_dir/notarytool"

kinko exec --env APPLE_SIGNING_IDENTITY,APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc '
export PATH="/tmp/chilla-notarytool-wrapper:$PATH"
task bundle-macos-dmg
'
```

If `/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool` is unavailable, check:

```bash
/usr/bin/mdfind 'kMDItemFSName == "notarytool"'
```

## Notarization Status

When Tauri submits notarization, record only the submission ID and status. To check status:

```bash
kinko exec --env APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc '
/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool info <submission-id> \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
'
```

Look for:

```text
status: Accepted
```

Recent submissions:

```bash
kinko exec --env APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_ID -- bash -lc '
/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
'
```

If a submission stays `In Progress`, do not claim deployment is complete. Report that Apple has not returned a decision yet.

## Validation After Acceptance

After notarization is accepted and artifacts exist:

```bash
codesign --verify --deep --strict --verbose=2 target/release/bundle/macos/chilla.app
xcrun stapler validate target/release/bundle/macos/chilla.app
xcrun stapler validate target/release/bundle/dmg/*.dmg
spctl --assess --type execute --verbose=4 target/release/bundle/macos/chilla.app
spctl --assess --type open --context context:primary-signature --verbose=4 target/release/bundle/dmg/*.dmg
```

If `xcrun stapler` cannot find tools due to the Nix SDK, call the Xcode tool directly when appropriate:

```bash
/Applications/Xcode.app/Contents/Developer/usr/bin/stapler validate target/release/bundle/macos/chilla.app
```

## Completion Criteria

Local Apple setup is complete when:

- kinko has all required Apple secret keys present.
- `security find-identity` reports a valid Developer ID Application identity.
- `task bundle-macos-dmg` or the wrapper-based equivalent signs the app.
- Notarization reaches `Accepted`.
- Stapler and Gatekeeper validation pass for the app and DMG.
4 changes: 4 additions & 0 deletions .agents/skills/apple-notarization-setup/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Apple Notarization Setup"
short_description: "Set up Apple signing and notarization prerequisites for chilla"
default_prompt: "Set up or verify Apple Developer ID signing, app-specific password storage, and local notarization readiness for chilla without exposing credential values."
19 changes: 18 additions & 1 deletion flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 31 additions & 10 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/release-24.11";
nixpkgs-webkit.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
fenix = {
Expand All @@ -20,6 +21,7 @@
{
self,
nixpkgs,
nixpkgs-webkit,
nixpkgs-unstable,
flake-utils,
fenix,
Expand All @@ -32,6 +34,7 @@
let
overlays = [ fenix.overlays.default ];
pkgs = import nixpkgs { inherit system overlays; };
pkgs-webkit = import nixpkgs-webkit { inherit system; };
pkgs-unstable = import nixpkgs-unstable { inherit system; };
lib = pkgs.lib;
bun = pkgs-unstable.bun;
Expand Down Expand Up @@ -59,7 +62,9 @@
);
};

linuxGuiLibraries = with pkgs; [
linuxGuiPkgs = if pkgs.stdenv.isLinux then pkgs-webkit else pkgs;

linuxGuiLibraries = with linuxGuiPkgs; [
atk
cairo
gdk-pixbuf
Expand All @@ -75,10 +80,10 @@
# - gstreamer itself for coreelements like typefind/fakesink
# - base/good/bad/ugly/libav for container/codec support
# - pipewire so autoaudiosink can resolve a compatible pipewiresink
linuxGStreamerCorePackage = pkgs.gst_all_1.gstreamer.out;
linuxGStreamerCorePackage = linuxGuiPkgs.gst_all_1.gstreamer.out;

linuxGStreamerPluginPackages =
(with pkgs.gst_all_1; [
(with linuxGuiPkgs.gst_all_1; [
gst-plugins-base
gst-plugins-good
gst-plugins-bad
Expand All @@ -87,37 +92,38 @@
])
++ [
linuxGStreamerCorePackage
pkgs.pipewire
linuxGuiPkgs.pipewire
];

linuxGStreamerPluginPath = lib.makeSearchPath "lib/gstreamer-1.0" linuxGStreamerPluginPackages;
linuxGStreamerPluginScanner =
"${linuxGStreamerCorePackage}/libexec/gstreamer-1.0/gst-plugin-scanner";

linuxWebkitMediaLibraries = with pkgs; [
linuxWebkitMediaLibraries = with linuxGuiPkgs; [
ffmpeg
libpulseaudio
alsa-lib
pipewire
];
gitRuntimePath = lib.makeBinPath [ pkgs.git ];

linuxRuntimeLibraryPath =
if pkgs.stdenv.isLinux then
lib.makeLibraryPath (
linuxGuiLibraries
++ (with pkgs.gst_all_1; [
++ (with linuxGuiPkgs.gst_all_1; [
linuxGStreamerCorePackage
gst-plugins-base
])
++ linuxWebkitMediaLibraries
)
else
lib.makeLibraryPath linuxGuiLibraries;
linuxGioModulePath = lib.makeSearchPath "lib/gio/modules" (with pkgs; [
linuxGioModulePath = lib.makeSearchPath "lib/gio/modules" (with linuxGuiPkgs; [
dconf.lib
glib-networking
]);
linuxXdgDataDirs = lib.concatStringsSep ":" (with pkgs; [
linuxXdgDataDirs = lib.concatStringsSep ":" (with linuxGuiPkgs; [
"${gsettings-desktop-schemas}/share/gsettings-schemas/${gsettings-desktop-schemas.name}"
"${gtk3}/share/gsettings-schemas/${gtk3.name}"
"${shared-mime-info}/share"
Expand Down Expand Up @@ -188,7 +194,7 @@
src = tauriBuildSource;
cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml";
buildInputs = commonBuildInputs;
nativeBuildInputs = with pkgs; [ pkg-config ];
nativeBuildInputs = with pkgs; [ git pkg-config ];
};

chilla = craneLib.buildPackage {
Expand All @@ -199,6 +205,7 @@
cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml";
buildInputs = commonBuildInputs;
nativeBuildInputs = with pkgs; [
git
makeWrapper
pkg-config
];
Expand All @@ -217,6 +224,7 @@

postFixup = lib.optionalString pkgs.stdenv.isLinux ''
wrapProgram $out/bin/chilla \
--prefix PATH : "${gitRuntimePath}" \
--prefix LD_LIBRARY_PATH : "${linuxRuntimeLibraryPath}" \
--set GIO_EXTRA_MODULES "${linuxGioModulePath}" \
--set XDG_DATA_DIRS "${linuxXdgDataDirs}" \
Expand Down Expand Up @@ -250,6 +258,7 @@
openssl
pkg-config
taplo
git
gh
go-task
]
Expand All @@ -269,7 +278,19 @@
inherit cargoArtifacts;
cargoExtraArgs = "--manifest-path src-tauri/Cargo.toml";
buildInputs = commonBuildInputs;
nativeBuildInputs = with pkgs; [ pkg-config ];
nativeBuildInputs = with pkgs; [ git pkg-config ];
preBuild = ''
# Tauri caches build-script outputs with absolute OUT_DIR paths.
# When crane reuses cargoArtifacts across derivations, those paths
# point at the previous sandbox and break permission generation.
rm -rf target/release/build/tauri-*
rm -rf target/release/build/tauri-build-*
rm -rf target/release/build/tauri-plugin-*
rm -rf target/release/build/tauri-runtime-*
rm -rf target/release/build/tauri-runtime-wry-*
rm -rf target/release/build/tauri-utils-*
rm -rf target/release/build/chilla-*
'';
cargoClippyExtraArgs = "--all-targets -- -D warnings";
};

Expand Down
4 changes: 4 additions & 0 deletions scripts/run-tauri-e2e-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ fi

export CARGO_TERM_QUIET=true
export GDK_BACKEND="${GDK_BACKEND:-x11}"
export EGL_PLATFORM="${EGL_PLATFORM:-x11}"
export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"
export GSK_RENDERER="${GSK_RENDERER:-cairo}"
export LIBGL_ALWAYS_SOFTWARE="${LIBGL_ALWAYS_SOFTWARE:-1}"
export LIBGL_KOPPER_DISABLE="${LIBGL_KOPPER_DISABLE:-true}"
export MESA_LOADER_DRIVER_OVERRIDE="${MESA_LOADER_DRIVER_OVERRIDE:-llvmpipe}"
export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-1}"
export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"
export CHILLA_TAURI_E2E_REPO_ROOT="$repo_root"
Expand Down
13 changes: 11 additions & 2 deletions tests/tauri/tauri-smoke.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ const FIXTURE_MP4_NAME = "file_example_MP4_480_1_5MG.mp4";
const SHUTDOWN_TIMEOUT_MS = 5_000;
const HEADLESS_RENDER_ENV_EXPORTS = [
'export GDK_BACKEND="${GDK_BACKEND:-x11}"',
'export EGL_PLATFORM="${EGL_PLATFORM:-x11}"',
'export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"',
'export GSK_RENDERER="${GSK_RENDERER:-cairo}"',
'export LIBGL_ALWAYS_SOFTWARE="${LIBGL_ALWAYS_SOFTWARE:-1}"',
'export LIBGL_KOPPER_DISABLE="${LIBGL_KOPPER_DISABLE:-true}"',
'export MESA_LOADER_DRIVER_OVERRIDE="${MESA_LOADER_DRIVER_OVERRIDE:-llvmpipe}"',
'export NO_AT_BRIDGE="${NO_AT_BRIDGE:-1}"',
'export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-1}"',
'export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"',
Expand Down Expand Up @@ -273,9 +277,14 @@ async function verifyWorkspaceLoads(
until.elementLocated(By.css(".file-browser__path")),
STARTUP_TIMEOUT_MS,
);
const pathText = await pathElement.getText();
let pathText = "";
await waitUntil(currentDriver, async () => {
pathText = await pathElement.getText();

return pathText.includes(expectedWorkspaceRoot);
});

if (!pathText.includes(expectedWorkspaceRoot)) {
if (pathText.length === 0 || !pathText.includes(expectedWorkspaceRoot)) {
throw new Error(
`Expected workspace path to include ${expectedWorkspaceRoot}, got ${JSON.stringify(pathText)}`,
);
Expand Down
Loading