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
60 changes: 60 additions & 0 deletions .github/workflows/update-cask.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Update cask

on:
pull_request:
paths:
- ".github/workflows/update-cask.yml"
- "scripts/update-cask.sh"
schedule:
- cron: "17,47 * * * *"
workflow_dispatch:
inputs:
version:
description: Optional exact Headless release version
required: false
type: string

permissions:
contents: write

concurrency:
group: update-headless-cask
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: Validate updater
run: |
bash -n scripts/update-cask.sh
shellcheck scripts/update-cask.sh

update:
if: github.event_name != 'pull_request'
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Verify release and render cask
env:
VERSION: ${{ inputs.version }}
run: ./scripts/update-cask.sh "$VERSION"
- name: Validate cask style
run: brew style --cask Casks/headless.rb
- name: Commit update
run: |
set -euo pipefail
if git diff --quiet -- Casks/headless.rb; then
echo "Cask is already current"
exit 0
fi
rendered_version="$(sed -n 's/^ version "\([^"]*\)"$/\1/p' Casks/headless.rb)"
test -n "$rendered_version"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Casks/headless.rb
git commit -m "headless $rendered_version"
git push origin HEAD:main
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# homebrew-headless
Homebrew tap for Headless
# Homebrew tap for Headless

Install the latest signed and notarized Headless release:

```sh
brew install --cask LockInTime/headless/headless
```

The cask is generated from the latest public
[`LockInTime/headless`](https://github.com/LockInTime/headless) release. The
update workflow verifies the release checksum, universal architectures,
Developer ID signature, stapled notarization ticket, and Gatekeeper assessment
before committing a new version.
94 changes: 94 additions & 0 deletions scripts/update-cask.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/bash
set -euo pipefail

VERSION="${1:-}"
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
REPOSITORY="LockInTime/headless"
SEMVER_PATTERN='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
TEMP_DIRECTORY="$(mktemp -d "${TMPDIR:-/tmp}/headless-cask-update.XXXXXX")"
trap 'rm -rf "$TEMP_DIRECTORY"' EXIT

curl_release() {
curl --fail --silent --show-error --location \
--proto '=https' --tlsv1.2 \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"$1"
}

if [[ -z "$VERSION" ]]; then
VERSION="$(curl_release "https://api.github.com/repos/$REPOSITORY/releases/latest" | jq -r '.tag_name')"
VERSION="${VERSION#v}"
fi
if ! printf '%s\n' "$VERSION" | grep -Eq "$SEMVER_PATTERN"; then
echo "Homebrew update: invalid semantic version: $VERSION" >&2
exit 64
fi

ASSET="Headless-${VERSION}-macos.zip"
BASE_URL="https://github.com/$REPOSITORY/releases/download/v${VERSION}"
curl_release "$BASE_URL/SHA256SUMS" > "$TEMP_DIRECTORY/SHA256SUMS"
curl_release "$BASE_URL/$ASSET" > "$TEMP_DIRECTORY/$ASSET"

EXPECTED_SHA256="$(awk -v asset="$ASSET" '$2 == asset { print $1 }' "$TEMP_DIRECTORY/SHA256SUMS")"
if [[ "$(printf '%s\n' "$EXPECTED_SHA256" | sed '/^$/d' | wc -l | tr -d ' ')" != 1 ]] \
|| ! printf '%s\n' "$EXPECTED_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then
echo "Homebrew update: release manifest must contain one exact checksum for $ASSET" >&2
exit 65
fi
ACTUAL_SHA256="$(shasum -a 256 "$TEMP_DIRECTORY/$ASSET" | awk '{ print $1 }')"
[[ "$ACTUAL_SHA256" == "$EXPECTED_SHA256" ]] || {
echo "Homebrew update: checksum mismatch for $ASSET" >&2
exit 65
}

ditto -x -k "$TEMP_DIRECTORY/$ASSET" "$TEMP_DIRECTORY/unpacked"
APP="$TEMP_DIRECTORY/unpacked/Headless.app"
[[ -d "$APP" ]] || { echo "Homebrew update: archive does not contain Headless.app" >&2; exit 65; }
codesign --verify --deep --strict "$APP"
CODESIGN_DETAILS="$(codesign --display --verbose=4 "$APP" 2>&1)"
printf '%s\n' "$CODESIGN_DETAILS" | grep -q '^Authority=Developer ID Application:' \
|| { echo "Homebrew update: app is not Developer ID signed" >&2; exit 65; }
printf '%s\n' "$CODESIGN_DETAILS" | grep -q 'flags=.*runtime' \
|| { echo "Homebrew update: hardened runtime is missing" >&2; exit 65; }
for executable in \
"$APP/Contents/MacOS/Headless" \
"$APP/Contents/Resources/bin/headless" \
"$APP/Contents/Resources/bin/headless-mcp"; do
ARCHITECTURES="$(lipo -archs "$executable")"
for architecture in arm64 x86_64; do
printf '%s\n' "$ARCHITECTURES" | grep -Eq "(^| )$architecture( |$)" \
|| { echo "Homebrew update: $executable is missing $architecture" >&2; exit 65; }
done
done
xcrun stapler validate "$APP"
spctl --assess --type execute --verbose=4 "$APP"

mkdir -p "$ROOT/Casks"
TEMP_CASK="$ROOT/Casks/.headless.rb.tmp"
cat > "$TEMP_CASK" <<CASK
cask "headless" do
version "$VERSION"
sha256 "$ACTUAL_SHA256"

url "https://github.com/LockInTime/headless/releases/download/v#{version}/Headless-#{version}-macos.zip",
verified: "github.com/LockInTime/headless/"
name "Headless"
desc "Persistent safety-enforced browser control for AI agents"
homepage "https://github.com/LockInTime/headless"

depends_on macos: ">= :ventura"

app "Headless.app"
binary "#{appdir}/Headless.app/Contents/Resources/bin/headless", target: "headless"
binary "#{appdir}/Headless.app/Contents/Resources/bin/headless-mcp", target: "headless-mcp"

zap trash: [
"~/Library/Application Support/com.headless.app",
"~/Library/Preferences/com.headless.app.plist",
]
end
CASK
mv "$TEMP_CASK" "$ROOT/Casks/headless.rb"

echo "Rendered Headless $VERSION cask after distribution verification"