Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Hardened runtime and notarization #367

Merged
merged 18 commits into from
Nov 28, 2024
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Desktop App
name: Desktop App CI

on:
push:
Expand All @@ -25,10 +25,6 @@ jobs:
run: npm ci
working-directory: ui/desktop

- name: Package application
run: npm run package
working-directory: ui/desktop

- name: Run E2E tests
run: npm run test-e2e
working-directory: ui/desktop
111 changes: 111 additions & 0 deletions .github/workflows/desktop-app-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Desktop App Release

on:
push:
branches:
- v1.0

jobs:
build-and-bundle:
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true

- name: Cache Cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-

- name: Cache Cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/index
key: ${{ runner.os }}-cargo-index
restore-keys: |
${{ runner.os }}-cargo-index

- name: Cache Cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-

# Install UV and download tokenizer files
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh

- name: Run download_tokenizer_files.py
run: uv run download_tokenizer_files.py

# Build Rust Binary
- name: Build Release Binary
run: cargo build --release

- name: copy binary
run: cp target/release/goosed ui/desktop/src/bin/goosed




# Desktop App Steps
- name: Add MacOS certs for signing and notarization
run: ./add-macos-cert.sh
working-directory: ui/desktop
env:
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: 'lts/*'

- name: Install dependencies
run: npm ci
working-directory: ui/desktop

- name: Make default Goose App
run: npm run bundle:default
working-directory: ui/desktop
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}

- name: Upload default
uses: actions/upload-artifact@v3
with:
name: Goose.zip
path: ui/desktop/out/Goose-darwin-arm64/Goose.zip

- name: Make preconfigured Goose App
run: npm run bundle:preconfigured
working-directory: ui/desktop
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GOOSE_BUNDLE_HOST: ${{ secrets.GOOSE_BUNDLE_HOST }}
GOOSE_BUNDLE_MODEL: ${{ secrets.GOOSE_BUNDLE_MODEL }}
GOOSE_BUNDLE_TYPE: ${{ secrets.GOOSE_BUNDLE_TYPE }}

- name: Upload preconfigured
uses: actions/upload-artifact@v3
with:
name: Goose-preconfigured.app
path: ui/desktop/out/Goose-darwin-arm64/Goose.zip
29 changes: 21 additions & 8 deletions ui/desktop/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
# Goose App

Mac app for Goose.
Mac (and maybe windows?) app for Goose.

```
git clone git@github.com:block/goose.git
cd goose/ui/desktop
npm install
export GOOSE_PROVIDER__API_KEY=... # OpenAI API Key
npm start
```

This will run `goosed` from src/bin (currently just copied into place from goose core) listening automatically.
# Building notes

Testing the rust server from source:
This is an electron forge app, using vite and react.js. `gooosed` runs as multi process binaries on each window/tab similar to chrome.

See `test.sh` for curl on how to use goose daemon - which is from rust version:
see `package.json`:

* rust streaming server version of goose at time of writing: https://github.com/block/goose/pull/237
`npm run bundle:default` will give you a Goose.app/zip which is signed/notarized but only if you setup the env vars as per `forge.config.ts` (you can empty out the section on osxSign if you don't want to sign it) - this will have all defaults.

`cargo run -p goose-server`
`npm run bundle:preconfigured` will make a Goose.app/zip signed and notarized, but use the following:

`./test.sh` (in another shell)
```python
f" process.env.GOOSE_PROVIDER__TYPE = '{os.getenv("GOOSE_BUNDLE_TYPE")}';",
f" process.env.GOOSE_PROVIDER__HOST = '{os.getenv("GOOSE_BUNDLE_HOST")}';",
f" process.env.GOOSE_PROVIDER__MODEL = '{os.getenv("GOOSE_BUNDLE_MODEL")}';"
```

This allows you to set for example GOOSE_PROVIDER__TYPE to be "databricks" by default if you want (so when people start Goose.app - they will get that out of the box). There is no way to set an api key in that bundling as that would be a terrible idea, so only use providers that can do oauth (like databricks can), otherwise stick to default goose.


# Runninng with goosed server from source

Set `VITE_START_EMBEDDED_SERVER=yes` to no in `.env.
Run `cargo run -p goose-server` from parent dir.
`npm run start` will then run against this.
You can try server directly with `./test.sh`
23 changes: 23 additions & 0 deletions ui/desktop/add-macos-cert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env sh

KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12

# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_OSX_APPLICATION | base64 --decode > $CERTIFICATE_P12

#create a keychain
security create-keychain -p actions $KEY_CHAIN

# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN

# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN

security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign;

security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN

# remove certs
rm -fr *.p12
26 changes: 8 additions & 18 deletions ui/desktop/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
import re
from pathlib import Path
from typing import Dict, Union
import os

def replace_env_macro(provider_type: str, host: str, model: str) -> bool:
def replace_env_macro() -> bool:


"""
Replace content between environment macro markers with formatted environment variables.

Expand All @@ -24,9 +27,9 @@ def replace_env_macro(provider_type: str, host: str, model: str) -> bool:

# Format the environment variables
formatted_vars = [
f" process.env.GOOSE_PROVIDER__TYPE = '{provider_type}';",
f" process.env.GOOSE_PROVIDER__HOST = '{host}';",
f" process.env.GOOSE_PROVIDER__MODEL = '{model}';"
f" process.env.GOOSE_PROVIDER__TYPE = '{os.getenv("GOOSE_BUNDLE_TYPE")}';",
f" process.env.GOOSE_PROVIDER__HOST = '{os.getenv("GOOSE_BUNDLE_HOST")}';",
f" process.env.GOOSE_PROVIDER__MODEL = '{os.getenv("GOOSE_BUNDLE_MODEL")}';"
]

replacement_content = "\n".join(formatted_vars)
Expand Down Expand Up @@ -59,20 +62,7 @@ def replace_env_macro(provider_type: str, host: str, model: str) -> bool:

# Example usage
if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser(description='Update environment variables in main.ts')
parser.add_argument('--type', required=True, help='Provider type (e.g., databricks)')
parser.add_argument('--host', required=True, help='Host URL')
parser.add_argument('--model', required=True, help='Model name')

args = parser.parse_args()

success = replace_env_macro(
provider_type=args.type,
host=args.host,
model=args.model
)
success = replace_env_macro()

if not success:
print("Failed to update environment variables")
Expand Down
7 changes: 7 additions & 0 deletions ui/desktop/forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ module.exports = {
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
'gatekeeper-assess': false,
hardenedRuntime: true,
identity: 'Developer ID Application: Michael Neale (W2L75AE9HQ)',
},
osxNotarize: {
appleId: process.env['APPLE_ID'],
appleIdPassword: process.env['APPLE_ID_PASSWORD'],
teamId: process.env['APPLE_TEAM_ID']
},
},
rebuildConfig: {},
Expand Down
7 changes: 3 additions & 4 deletions ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"bundle:preconfigured": "./bundle.py && npm run make && echo 'resetting main' && git checkout src/main.ts && cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip",
"bundle:default": "npm run make && cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip",
"debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app",
"test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi",
"sign-macos": "cd ./out/Goose-darwin-arm64 && codesign --deep --force --verify --sign \"Developer ID Application: Michael Neale (W2L75AE9HQ)\" Goose.app && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip",
"sign-verify": " cd ./out/Goose-darwin-arm64 && codesign --verify --deep --strict Goose.app && codesign -d --entitlements :- Goose.app"
"test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi"
},
"devDependencies": {
"@electron-forge/cli": "^7.5.0",
Expand Down
Loading