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
39 changes: 39 additions & 0 deletions .claude/rules/vm-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ self-contained; no user-global skill or rule is required to run it.
| CPU / memory profiling (`lyra benchmark`, `sample`) | VM |
| Screen resolution change (approximation via Dynamic Resolution) | VM — see note below |
| `lyra healthcheck` / API smoke | VM |
| Code signing / Info.plist binding (TCC bundle identity) | VM (`codesign -dvv`, `otool -P`) — see scenario below |
| Display hot-plug (external monitor attach / detach) | ScreenProvider fixture + final manual smoke |
| NSScreen topology change (`NSApplicationDidChangeScreenParameters`) | ScreenProvider fixture |
| Visual overlay pixel verification | Host debug-build lane (`dev-verification.md`) |
Expand Down Expand Up @@ -100,6 +101,7 @@ $SCRIPT shutdown $VM # graceful guest shutdown

| Variable | Default | Purpose |
|---|---|---|
| `LYRA_VM_SSH_HOST` | (unset) | Guest IP override — **required for Apple Virtualization backend**, where `utmctl ip-address` is unsupported. Find it via the guest's `/var/db/dhcpd_leases` or `ifconfig`. |
| `LYRA_VM_SSH_USER` | `admin` | Guest login name |
| `LYRA_VM_SSH_KEY` | `~/.ssh/vm_rsa` | SSH private key path |
| `LYRA_VM_SSH_PORT` | `22` | Guest SSH port |
Expand Down Expand Up @@ -153,6 +155,32 @@ $SCRIPT restore $VM
$SCRIPT shutdown $VM
```

### Code signing / Info.plist binding (TCC bundle identity)

When a change embeds an `Info.plist` (Mach-O `__TEXT,__info_plist` section) so
TCC can key permission grants by **bundle identity** rather than executable
path (#23), verify the binding inside the guest — a clean macOS install proves
the result without the host's accumulated signing state.

`swift build -c release` embeds the section but its ad-hoc signature leaves it
**unbound** (`Info.plist=not bound`, `Identifier=<binary-name>`). Only an
explicit `codesign --force --sign -` (what `make install` and CI packaging run)
binds it — codesign then derives `Identifier` from the embedded
`CFBundleIdentifier`.

```sh
$SCRIPT run-lyra $VM # pushes the release binary
BIN=/tmp/lyra-vm-test/lyra
$SCRIPT exec $VM -- "otool -P $BIN" # section present? CFBundleIdentifier?
$SCRIPT exec $VM -- "codesign -dvv $BIN 2>&1 | grep -E 'Identifier|Info.plist'" # BEFORE: not bound
$SCRIPT exec $VM -- "codesign --force --sign - $BIN && codesign -dvv $BIN 2>&1 | grep -E 'Identifier|Info.plist'" # AFTER: entries=N
```

Expected transition: `Identifier=lyra` / `Info.plist=not bound` →
`Identifier=com.generald.lyra` / `Info.plist entries=4`. Re-signing changes the
cdhash, so **restart the daemon** with the bound binary and re-`capture` to
prove it still executes and renders (no-regression).

---

## Agent rules
Expand All @@ -168,3 +196,14 @@ $SCRIPT shutdown $VM
replace this requirement.
- **Restore always runs.** The `restore` subcommand must run even if an
intermediate step fails. Use `trap` in any script that calls `run-lyra`.
- **`run-lyra` "daemon crashed at startup" can be a false negative.** The
harness checks `kill -0 $pid` shortly after launch, but the daemon's
first-launch `swift-frontend -interpret` of the MediaRemote helper takes
1–2 s; a slow guest can trip the check while the process is in fact alive.
Before trusting the `die`, confirm with `$SCRIPT exec $VM -- "pgrep -x lyra"`
and the daemon log — if the PID is alive, proceed.
- **Never run two `run-lyra` concurrently.** Both build under the same
`.build` (SwiftPM serializes with a lock) and both stage into the guest's
`/tmp/lyra-drop`; the second `scp` hits `Permission denied` on the
half-written bundle. Let one finish, or `sudo rm -rf /tmp/lyra-drop` on the
guest before retrying.
6 changes: 6 additions & 0 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ jobs:
STAGING="lyra-${VERSION}-macos-arm64"
mkdir -p "$STAGING"
cp "$BUILD_DIR/lyra" "$STAGING/"
# Bind the embedded __info_plist (CFBundleIdentifier) into the ad-hoc
# signature so TCC keys permission grants by bundle identity (#23).
codesign --force --sign - "$STAGING/lyra"
find "$BUILD_DIR" -name '*.bundle' -exec cp -R {} "$STAGING/" \;
tar czf "${STAGING}.tar.gz" "$STAGING"
echo "ARCHIVE=${STAGING}.tar.gz" >> "$GITHUB_ENV"
Expand All @@ -89,6 +92,9 @@ jobs:
STAGING="lyra-${VERSION}-macos-arm64"
mkdir -p "$STAGING"
cp "$BUILD_DIR/lyra" "$STAGING/"
# Bind the embedded __info_plist (CFBundleIdentifier) into the ad-hoc
# signature so TCC keys permission grants by bundle identity (#23).
codesign --force --sign - "$STAGING/lyra"
find "$BUILD_DIR" -name '*.bundle' -exec cp -R {} "$STAGING/" \;
tar czf "${STAGING}.tar.gz" "$STAGING"
gh release upload "$TAG" "${STAGING}.tar.gz" --clobber
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ install: build
install -d $(PREFIX)/bin
install $(BUILD_DIR)/$(BINARY) $(PREFIX)/bin/$(BINARY)
find $(BUILD_DIR) -name '*.bundle' -exec cp -R {} $(PREFIX)/bin/ \;
# Bind the embedded __TEXT,__info_plist (CFBundleIdentifier) into the
# signature. `swift build -c release` leaves it unbound; TCC keys permission
# grants by bundle identity, so the binary needs a re-sign to expose it (#23).
codesign --force --sign - $(PREFIX)/bin/$(BINARY)

uninstall:
rm -f $(PREFIX)/bin/$(BINARY)
Expand Down
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ let package = Package(
"AsyncRunnableCommand",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Dependencies", package: "swift-dependencies"),
],
// Info.plist is embedded into the Mach-O __TEXT,__info_plist section
// (see linkerSettings) rather than copied as a bundle resource, so it
// is excluded from SwiftPM's resource processing here.
exclude: ["Info.plist"],
// Embed Info.plist so the binary carries a stable CFBundleIdentifier.
// TCC keys permission grants (e.g. system-audio capture for the planned
// spectrum analyzer, #23) by bundle identity; without an embedded plist
// the grant is keyed to the executable path and resets on every reinstall.
// The path is relative to the package root, where the linker is invoked.
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-sectcreate",
"-Xlinker", "__TEXT",
"-Xlinker", "__info_plist",
"-Xlinker", "Sources/CLI/Info.plist",
])
]
),

Expand Down
14 changes: 14 additions & 0 deletions Sources/CLI/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.generald.lyra</string>
<key>CFBundleName</key>
<string>lyra</string>
<key>CFBundleExecutable</key>
<string>lyra</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Sources/VersionHandler/Resources/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.15.0
2.15.1
Loading