Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8cae54d
feat(mac): add env-var-aware __main__.py entry point
jaylfc Apr 28, 2026
aa39eb3
feat(mac): scaffold AppleContainerBackend skeleton
jaylfc Apr 28, 2026
5b9b680
refactor(mac): annotate AppleContainerBackend stubs to match ABC
jaylfc Apr 28, 2026
6484c03
feat(mac): apple backend list_containers with prefix filter
jaylfc Apr 28, 2026
7ab22f6
refactor(mac): polish list_containers — docstring, import order, bad-…
jaylfc Apr 28, 2026
6e9ffcc
feat(mac): apple backend create_container
jaylfc Apr 28, 2026
1e5cfd2
fix(mac): create_container survives set_root_quota stub/failure
jaylfc Apr 28, 2026
6fb0216
feat(mac): apple backend exec/push
jaylfc Apr 28, 2026
27de065
feat(mac): apple backend lifecycle methods
jaylfc Apr 28, 2026
71f2597
feat(mac): apple backend logs + rename
jaylfc Apr 28, 2026
6564db7
fix(mac): get_container_logs surfaces error code
jaylfc Apr 28, 2026
061dd59
feat(mac): apple backend snapshot methods
jaylfc Apr 28, 2026
d4539d3
test(mac): broaden snapshot failure + filter coverage
jaylfc Apr 28, 2026
ff38ef5
feat(mac): apple backend no-op methods (proxy/env/quota)
jaylfc Apr 28, 2026
469707b
refactor(mac): drop stub-era guards now that Apple backend is complete
jaylfc Apr 28, 2026
c26d119
feat(mac): detect_runtime apple branch
jaylfc Apr 28, 2026
f8631bc
test(mac): isolate detect_runtime tests + add apple>lxc on darwin
jaylfc Apr 28, 2026
d58e6c7
feat(mac): wire AppleContainerBackend in create_app
jaylfc Apr 28, 2026
92e715a
feat(mac): scaffold mac/ build pipeline tree
jaylfc Apr 28, 2026
f99d3f2
feat(mac): add build_python.sh
jaylfc Apr 28, 2026
5239b3c
feat(mac): add build_frontend.sh
jaylfc Apr 28, 2026
97f24ce
feat(mac): add fetch_container_cli.sh
jaylfc Apr 28, 2026
8a112c8
feat(mac): add bump_version.sh
jaylfc Apr 28, 2026
c5b4788
feat(mac): add assemble_bundle.sh + Info.plist.in
jaylfc Apr 28, 2026
8638419
feat(mac): add sign.sh (ad-hoc + Dev ID-aware)
jaylfc Apr 28, 2026
023c57a
feat(mac): add package_dmg.sh
jaylfc Apr 28, 2026
8245abe
feat(mac): add sparkle_sign.sh
jaylfc Apr 28, 2026
14b0edb
feat(mac): add notarize.sh stub
jaylfc Apr 28, 2026
2a563b2
feat(mac): add build.sh orchestrator
jaylfc Apr 28, 2026
a1d800b
feat(mac): scaffold Swift package
jaylfc Apr 28, 2026
defa6fb
feat(mac): ServerProcess spawn + graceful shutdown
jaylfc Apr 28, 2026
1589e4c
feat(mac): MenuBar status-item menu builder
jaylfc Apr 28, 2026
8b01deb
feat(mac): WindowController fullscreen/phone modes
jaylfc Apr 28, 2026
01ff8d3
feat(mac): KeyboardMonitor for fullscreen kiosk mode
jaylfc Apr 28, 2026
7fb9a29
feat(mac): SparkleBridge wrapper (no-op until Task 32)
jaylfc Apr 28, 2026
f5601d1
feat(mac): wire AppDelegate, status item, server lifecycle
jaylfc Apr 28, 2026
3eac0e1
feat(mac): asset catalog + icon placeholder script
jaylfc Apr 28, 2026
3da83f0
docs(mac): RELEASE_TESTING.md manual checklist
jaylfc Apr 28, 2026
39f8ce7
chore(mac): pin requirements lock + checksums; bump Python to 3.12.13…
jaylfc Apr 28, 2026
fb12eb8
chore(mac): defer Sparkle binaryTarget to build-time wiring
jaylfc Apr 28, 2026
b5e5bd4
chore(mac): make local build pipeline viable without release credentials
jaylfc Apr 28, 2026
e0f8730
fix(mac): stop launcher cleanly on SIGTERM and seed user data dir
jaylfc Apr 28, 2026
81afe0c
fix(mac): address CodeRabbit Major review on PR #269
jaylfc Apr 28, 2026
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ __pycache__/
*.egg-info/
dist/
build/
!mac/build/
mac/launcher/.build/
*.db
.superpowers/
.worktrees/
Expand Down
17 changes: 17 additions & 0 deletions mac/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# taOS Mac App build

Build pipeline for the macOS `.app` bundle (Apple Silicon, macOS 26+).

See `docs/superpowers/specs/2026-04-28-taos-mac-app-c1-design.md` for the full design.

## Quick start

./mac/build/build.sh --version 0.1.0 --output dist/

Produces `dist/taOS.app`, `dist/taOS-0.1.0.dmg`, and `dist/taOS-0.1.0.dmg.sig`.

## Layout

- `launcher/` — Swift Package for the native launcher
- `build/` — shell pipeline (orchestrator + step scripts)
- `appcast/` — Sparkle appcast.xml + EdDSA public key
9 changes: 9 additions & 0 deletions mac/appcast/appcast.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>taOS Updates</title>
<link>https://taos.app/appcast.xml</link>
<description>Sparkle feed for taOS</description>
<language>en</language>
</channel>
</rss>
61 changes: 61 additions & 0 deletions mac/build/RELEASE_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# taOS Mac App — Release Testing Checklist

Run on a fresh macOS 26 install (or a wiped data dir) before tagging a release.

## 1. Fresh install

- [ ] Open `taOS-X.Y.Z.dmg` → drag to Applications.
- [ ] Right-click `/Applications/taOS.app` → Open → confirm "open" in Gatekeeper dialog.
- [ ] Status-bar `taOS` icon appears.
- [ ] First-run wizard opens in **phone-shape mode**.
- [ ] Setup completes; Dock icon disappears after closing the window.

## 2. Container creation

- [ ] Create a project, add an agent.
- [ ] Open Terminal: `container ls` shows the new container.
- [ ] Send chat → response received.
- [ ] Trace store entry shows `host.containers.internal:4000` reachable.

## 3. Window-mode toggle

- [ ] `Cmd+Shift+M` switches phone ↔ fullscreen.
- [ ] In fullscreen, `Cmd+Q` is intercepted (no quit).
- [ ] In fullscreen, `Ctrl+→` moves to the next Space.
- [ ] In fullscreen, three-finger swipe moves Spaces.

## 4. Quit + relaunch

- [ ] Close window with the red traffic light.
- [ ] Dock icon hides; status-bar icon stays.
- [ ] `~/Library/Logs/taOS/server.log` keeps writing.
- [ ] "Quit taOS" from status bar → graceful shutdown logged → process gone.
- [ ] Relaunch → restores last window mode + last project.

## 5. Update path

- [ ] Stage a fake v0.1.1 DMG.
- [ ] Serve a local appcast: `cd staging && python -m http.server 8000`.
- [ ] In test build, set `SUFeedURL` to `http://127.0.0.1:8000/appcast.xml`.
- [ ] Sparkle prompts → EdDSA verifies → relaunches at v0.1.1.
- [ ] Re-run with a tampered DMG → install refused; `~/Library/Logs/taOS/sparkle.log` has a verification-failed entry; app stays on v0.1.0.

## 6. Failure isolation

- [ ] Kill FastAPI from Activity Monitor → status-bar icon goes yellow → "Restart Server" works.
- [ ] Stop the container daemon → existing containers listed but new creates fail with the documented note → restart daemon → recovers.
- [ ] Disconnect network → Sparkle silently skips next scheduled check.

## 7. Uninstall

- [ ] Trash `/Applications/taOS.app`.
- [ ] `~/Library/Application Support/taOS/` untouched.
- [ ] Reinstall the same DMG → no setup wizard, picks up where it left off.
- [ ] `brew uninstall --cask --zap taos` (when Cask exists) wipes the four `~/Library/...` dirs.

## Sign-off

Tester: ______________ Date: ______________
Version: ______________ All boxes checked: yes / no

Failures: file as GitHub issues with `mac-release` label and block release until resolved.
92 changes: 92 additions & 0 deletions mac/build/assemble_bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Build taOS.app/Contents/ from staging dirs.
#
# Args: --version <X.Y.Z> --staging <DIR> --launcher-binary <PATH> --output <DIR>
set -euo pipefail

VERSION=""
STAGING=""
LAUNCHER_BINARY=""
OUTPUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
--staging) STAGING="$2"; shift 2 ;;
--launcher-binary) LAUNCHER_BINARY="$2"; shift 2 ;;
--output) OUTPUT="$2"; shift 2 ;;
*) echo "assemble_bundle.sh: unknown arg $1" >&2; exit 2 ;;
esac
done

[[ -n "$VERSION" && -n "$STAGING" && -n "$LAUNCHER_BINARY" && -n "$OUTPUT" ]] \
|| { echo "all args required" >&2; exit 2; }

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
APP="$OUTPUT/taOS.app"
CONTENTS="$APP/Contents"

rm -rf "$APP"
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" "$CONTENTS/Frameworks"

# Info.plist
ED_KEY_FILE="$REPO_ROOT/mac/appcast/ed_public.pem"
if [[ -f "$ED_KEY_FILE" ]]; then
SU_PUBLIC_ED_KEY="$(grep -v '^-----' "$ED_KEY_FILE" | tr -d '\n')"
else
echo "[assemble_bundle] no ed_public.pem — Sparkle will be disabled in this build"
SU_PUBLIC_ED_KEY=""
fi
sed -e "s|\${VERSION}|$VERSION|g" \
-e "s|\${SU_PUBLIC_ED_KEY}|$SU_PUBLIC_ED_KEY|g" \
"$REPO_ROOT/mac/launcher/Sources/taOSLauncher/Resources/Info.plist.in" \
> "$CONTENTS/Info.plist"

echo -n "APPL????" > "$CONTENTS/PkgInfo"

# Launcher binary
cp "$LAUNCHER_BINARY" "$CONTENTS/MacOS/taOS"
chmod +x "$CONTENTS/MacOS/taOS"

# Python distribution
cp -R "$STAGING/python" "$CONTENTS/Resources/python"

# taOS source tree
mkdir -p "$CONTENTS/Resources/taos"
cp -R "$REPO_ROOT/tinyagentos" "$CONTENTS/Resources/taos/tinyagentos"
find "$CONTENTS/Resources/taos" -type d -name __pycache__ -exec rm -rf {} +
find "$CONTENTS/Resources/taos" -type f -name "*.pyc" -delete
cp "$REPO_ROOT/pyproject.toml" "$CONTENTS/Resources/taos/pyproject.toml"

# Bundle the data/ skeleton (config example, seed agents, templates) so the
# launcher can copy it into ~/Library/Application Support/taOS on first run.
if [[ -d "$REPO_ROOT/data" ]]; then
cp -R "$REPO_ROOT/data" "$CONTENTS/Resources/taos/data"
fi
# Bundle the app-catalog so backend auto-registration can find service manifests.
if [[ -d "$REPO_ROOT/app-catalog" ]]; then
cp -R "$REPO_ROOT/app-catalog" "$CONTENTS/Resources/taos/app-catalog"
fi

# Frontend
cp -R "$STAGING/frontend" "$CONTENTS/Resources/frontend"

# Apple container CLI + libexec plugins (image/network/runtime)
mkdir -p "$CONTENTS/Resources/bin"
cp "$STAGING/bin/container" "$CONTENTS/Resources/bin/container"
chmod +x "$CONTENTS/Resources/bin/container"
if [[ -d "$STAGING/libexec/container" ]]; then
mkdir -p "$CONTENTS/Resources/libexec"
cp -R "$STAGING/libexec/container" "$CONTENTS/Resources/libexec/container"
fi

# Sparkle.framework — fetched/extracted by build.sh prior
if [[ -d "$STAGING/Sparkle.framework" ]]; then
cp -R "$STAGING/Sparkle.framework" "$CONTENTS/Frameworks/Sparkle.framework"
fi

# AppIcon
if [[ -f "$STAGING/AppIcon.icns" ]]; then
cp "$STAGING/AppIcon.icns" "$CONTENTS/Resources/AppIcon.icns"
fi

echo "[assemble_bundle] done: $APP"
89 changes: 89 additions & 0 deletions mac/build/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Orchestrate the full build: launcher, python, frontend, container CLI,
# bundle, sign, DMG, sparkle-sign, notarize.
#
# Args:
# --version <X.Y.Z>
# --python-version <PYVER>
# --container-cli-version <CLIVER>
# --output <DIR>
set -euo pipefail

VERSION=""
PYTHON_VER="3.12.13"
CLI_VER="0.12.0"
OUTPUT="dist"
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
--python-version) PYTHON_VER="$2"; shift 2 ;;
--container-cli-version) CLI_VER="$2"; shift 2 ;;
--output) OUTPUT="$2"; shift 2 ;;
*) echo "build.sh: unknown arg $1" >&2; exit 2 ;;
esac
done

[[ -n "$VERSION" ]] || { echo "--version required" >&2; exit 2; }

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT_DIR="$REPO_ROOT/mac/build"
STAGING="$REPO_ROOT/$OUTPUT/staging"

echo "[build] env validation"
command -v swift >/dev/null || { echo "swift not found" >&2; exit 1; }
command -v create-dmg >/dev/null || { echo "create-dmg not found (brew install create-dmg)" >&2; exit 1; }

mkdir -p "$REPO_ROOT/$OUTPUT"
rm -rf "$STAGING"
mkdir -p "$STAGING"

echo "[build] (1/9) launcher"
cd "$REPO_ROOT/mac/launcher"
swift build -c release --arch arm64
LAUNCHER_BINARY="$REPO_ROOT/mac/launcher/.build/arm64-apple-macosx/release/taOSLauncher"
cd "$REPO_ROOT"

echo "[build] (2/9) python"
"$SCRIPT_DIR/build_python.sh" --version "$PYTHON_VER" --output "$STAGING"

echo "[build] (3/9) frontend"
"$SCRIPT_DIR/build_frontend.sh" --output "$STAGING"

echo "[build] (4/9) container CLI"
"$SCRIPT_DIR/fetch_container_cli.sh" --version "$CLI_VER" --output "$STAGING"

echo "[build] (5/9) assemble bundle"
"$SCRIPT_DIR/assemble_bundle.sh" \
--version "$VERSION" \
--staging "$STAGING" \
--launcher-binary "$LAUNCHER_BINARY" \
--output "$REPO_ROOT/$OUTPUT"

APP="$REPO_ROOT/$OUTPUT/taOS.app"

echo "[build] (6/9) sign"
"$SCRIPT_DIR/sign.sh" --app "$APP"

echo "[build] (7/9) package DMG"
"$SCRIPT_DIR/package_dmg.sh" --app "$APP" --version "$VERSION" --output "$REPO_ROOT/$OUTPUT"
DMG="$REPO_ROOT/$OUTPUT/taOS-$VERSION.dmg"

echo "[build] (8/9) notarize"
"$SCRIPT_DIR/notarize.sh" --dmg "$DMG"

echo "[build] (9/9) sparkle-sign"
SPARKLE_KEY="${SPARKLE_ED_PRIVATE_KEY:-$HOME/.taos/sparkle_ed_private.pem}"
if [[ -f "$SPARKLE_KEY" ]]; then
"$SCRIPT_DIR/sparkle_sign.sh" --dmg "$DMG" --version "$VERSION" --output "$REPO_ROOT/$OUTPUT"
else
echo "[sparkle-sign] skipped — no Sparkle EdDSA private key at $SPARKLE_KEY"
fi

# Clean staging on success
rm -rf "$STAGING"

echo "[build] done"
echo " app: $APP"
echo " dmg: $DMG"
echo " sig: ${DMG}.sig"
echo " appcast snippet: $REPO_ROOT/$OUTPUT/appcast-snippet.xml"
31 changes: 31 additions & 0 deletions mac/build/build_frontend.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Build the Vite frontend and copy output to staging.
#
# Args: --output <STAGING_DIR>
set -euo pipefail

OUTPUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--output) OUTPUT="$2"; shift 2 ;;
*) echo "build_frontend.sh: unknown arg $1" >&2; exit 2 ;;
esac
done

[[ -n "$OUTPUT" ]] || { echo "--output required" >&2; exit 2; }

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"

cd "$REPO_ROOT/desktop"
echo "[build_frontend] npm ci"
npm ci
echo "[build_frontend] npm run build"
npm run build

# Vite is configured (desktop/vite.config.ts) to write to ../static/desktop
DIST="$REPO_ROOT/static/desktop"
[[ -d "$DIST" ]] || { echo "[build_frontend] missing $DIST" >&2; exit 1; }

mkdir -p "$OUTPUT/frontend"
cp -R "$DIST"/. "$OUTPUT/frontend/"
echo "[build_frontend] done: $OUTPUT/frontend"
50 changes: 50 additions & 0 deletions mac/build/build_python.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Build the embedded Python distribution from python-build-standalone.
#
# Args: --version <PYTHON_VER> --output <STAGING_DIR>
# Output: $STAGING_DIR/python/{bin,lib,...}
set -euo pipefail

PYTHON_VER=""
OUTPUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--version) PYTHON_VER="$2"; shift 2 ;;
--output) OUTPUT="$2"; shift 2 ;;
*) echo "build_python.sh: unknown arg $1" >&2; exit 2 ;;
esac
done

[[ -n "$PYTHON_VER" ]] || { echo "--version required" >&2; exit 2; }
[[ -n "$OUTPUT" ]] || { echo "--output required" >&2; exit 2; }

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
CHECKSUM_FILE="$REPO_ROOT/mac/build/checksums/python-build-standalone.sha256"

# Latest stable release naming convention from astral-sh/python-build-standalone
TAG="20260414"
URL="https://github.com/astral-sh/python-build-standalone/releases/download/${TAG}/cpython-${PYTHON_VER}+${TAG}-aarch64-apple-darwin-install_only.tar.gz"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
echo "=== build_python.sh version/checksum wiring ==="
rg -n 'PYTHON_VER|TAG|CHECKSUM_FILE|URL=|EXPECTED_SHA|ACTUAL_SHA' mac/build/build_python.sh
echo
echo "=== checksum file contents ==="
nl -ba mac/build/checksums/python-build-standalone.sha256

Repository: jaylfc/tinyagentos

Length of output: 984


🏁 Script executed:

cat mac/build/checksums/python-build-standalone.sha256

Repository: jaylfc/tinyagentos

Length of output: 127


🏁 Script executed:

fd -type f -name "*checksum*" -o -name "*.sha256" mac/

Repository: jaylfc/tinyagentos

Length of output: 233


🏁 Script executed:

sed -n '1,45p' mac/build/build_python.sh

Repository: jaylfc/tinyagentos

Length of output: 1522


🏁 Script executed:

find mac/build/checksums -type f

Repository: jaylfc/tinyagentos

Length of output: 189


🏁 Script executed:

ls -la mac/build/checksums/

Repository: jaylfc/tinyagentos

Length of output: 382


🏁 Script executed:

grep -r "PYTHON_VER\|TAG\|CHECKSUM" mac/build/ --include="*.sh" -B2 -A2

Repository: jaylfc/tinyagentos

Length of output: 7813


Version argument conflicts with static checksum validation.

The script accepts --version <PYTHON_VER> to parameterize the Python distribution, and the download URL correctly incorporates this version (line 26). However, validation against a single static checksum file (line 22) defeats this flexibility—any PYTHON_VER other than the one matching python-build-standalone.sha256 will fail integrity checks, even when the artifact is valid from the upstream repository.

Consider using version-specific checksum file paths:

CHECKSUM_FILE="$REPO_ROOT/mac/build/checksums/python-build-standalone-${PYTHON_VER}+${TAG}.sha256"
[[ -f "$CHECKSUM_FILE" ]] || {
  echo "[build_python] missing checksum file for ${PYTHON_VER}+${TAG}: $CHECKSUM_FILE" >&2
  exit 2
}

Also applies to: fetch_container_cli.sh—same pattern with static checksum file while version is parameterized.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mac/build/build_python.sh` around lines 22 - 26, The script sets
CHECKSUM_FILE to a static path while TAG and PYTHON_VER parameterize the
downloaded artifact, so integrity checks will fail for any version not
represented by that single checksum file; change CHECKSUM_FILE to include the
version/tag (e.g., incorporate ${PYTHON_VER} and ${TAG} into the filename), test
for its existence before use and emit a clear error if missing, and update the
checksum lookup/validation logic accordingly; apply the same pattern to the
analogous variable/validation in fetch_container_cli.sh so its checksum file is
also version-specific.


mkdir -p "$OUTPUT"
TARBALL="$OUTPUT/python-${PYTHON_VER}.tar.gz"

echo "[build_python] downloading $URL"
curl -L --fail -o "$TARBALL" "$URL"

EXPECTED_SHA="$(cat "$CHECKSUM_FILE")"
ACTUAL_SHA="$(shasum -a 256 "$TARBALL" | awk '{print $1}')"
if [[ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then
echo "[build_python] SHA mismatch: expected $EXPECTED_SHA got $ACTUAL_SHA" >&2
exit 1
fi

echo "[build_python] extracting"
mkdir -p "$OUTPUT/python"
tar -xzf "$TARBALL" -C "$OUTPUT/python" --strip-components=1
rm "$TARBALL"

PYBIN="$OUTPUT/python/bin/python3"
"$PYBIN" -m pip install --upgrade pip
"$PYBIN" -m pip install --no-deps -r "$REPO_ROOT/tinyagentos/requirements.lock"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

echo "[build_python] done: $OUTPUT/python/bin/python3"
Loading
Loading