feat(security): add host origin allowlist on start #73
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| pull_request: | |
| paths: | |
| - ".github/workflows/release.yml" | |
| - ".github/scripts/**" | |
| - "apps/headless/Dockerfile.linux" | |
| - "apps/headless/build.sh" | |
| - "apps/headless/build-linux.sh" | |
| - "apps/headless/install-linux.sh" | |
| - "apps/headless/install.sh" | |
| - "apps/headless/Tests/linux-installer.sh" | |
| - "apps/headless/Tests/macos-distribution.sh" | |
| - "apps/headless/headless.entitlements" | |
| - "apps/headless/Package.swift" | |
| - "apps/headless/Sources/HeadlessProtocol/ProductVersion.swift" | |
| - "apps/headless/VERSION" | |
| - "apps/headless/VersionSupport/**" | |
| - "packages/headless-npm/**" | |
| - "pnpm-lock.yaml" | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: Build and verify release packages without publishing | |
| required: true | |
| default: true | |
| type: boolean | |
| permissions: | |
| contents: read | |
| env: | |
| CONTAINER_IMAGE: ghcr.io/lockintime/headless | |
| jobs: | |
| version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| publish: ${{ steps.version.outputs.publish }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - id: version | |
| shell: bash | |
| env: | |
| APPLE_DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_BASE64 }} | |
| APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }} | |
| APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} | |
| APPLE_NOTARY_KEY_BASE64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} | |
| APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| DRY_RUN: ${{ inputs.dry_run }} | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then | |
| version="${GITHUB_REF_NAME#v}" | |
| if ! printf '%s\n' "$version" | grep -Eq -f apps/headless/VersionSupport/semver-pattern.txt; then | |
| echo "Release tag must be a semantic version prefixed with v: $GITHUB_REF_NAME" >&2 | |
| exit 64 | |
| fi | |
| expected="$(tr -d '[:space:]' < apps/headless/VERSION)" | |
| if [[ "$version" != "$expected" ]]; then | |
| echo "Release tag $version does not match apps/headless/VERSION $expected" >&2 | |
| exit 64 | |
| fi | |
| npm_version="$(node -p "require('./packages/headless-npm/package.json').version")" | |
| if [[ "$version" != "$npm_version" ]]; then | |
| echo "Release tag $version does not match npm launcher $npm_version" >&2 | |
| exit 64 | |
| fi | |
| for required in \ | |
| APPLE_DEVELOPER_ID_CERTIFICATE_BASE64 \ | |
| APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD \ | |
| APPLE_NOTARY_KEY_BASE64 \ | |
| APPLE_NOTARY_KEY_ID \ | |
| APPLE_NOTARY_ISSUER_ID \ | |
| NPM_TOKEN; do | |
| if [[ -z "${!required:-}" ]]; then | |
| echo "Required release secret is missing: $required" >&2 | |
| exit 64 | |
| fi | |
| done | |
| echo "publish=true" >> "$GITHUB_OUTPUT" | |
| else | |
| if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$DRY_RUN" != "true" ]]; then | |
| echo "manual release runs must use dry_run=true; publish by pushing a v* tag" >&2 | |
| exit 64 | |
| fi | |
| version="0.0.${GITHUB_RUN_NUMBER}" | |
| echo "publish=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| npm-launcher: | |
| needs: version | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile --filter @lockintime/headless | |
| - run: pnpm --filter @lockintime/headless test | |
| - name: Verify published package contents | |
| working-directory: packages/headless-npm | |
| run: npm pack --dry-run | |
| macos: | |
| needs: version | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install FFmpeg | |
| run: brew install ffmpeg | |
| - name: Import Developer ID certificate | |
| if: needs.version.outputs.publish == 'true' | |
| id: signing | |
| shell: bash | |
| env: | |
| CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_BASE64 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| [[ -n "$CERTIFICATE_BASE64" && -n "$CERTIFICATE_PASSWORD" ]] || { | |
| echo "Apple Developer ID certificate secrets are required for tagged releases" >&2 | |
| exit 64 | |
| } | |
| keychain="$RUNNER_TEMP/headless-signing.keychain-db" | |
| certificate="$RUNNER_TEMP/headless-developer-id.p12" | |
| keychain_password="$(uuidgen)$(uuidgen)" | |
| printf '%s' "$CERTIFICATE_BASE64" | base64 -D > "$certificate" | |
| chmod 0600 "$certificate" | |
| security create-keychain -p "$keychain_password" "$keychain" | |
| security set-keychain-settings -lut 21600 "$keychain" | |
| security unlock-keychain -p "$keychain_password" "$keychain" | |
| security import "$certificate" -k "$keychain" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" | |
| security list-keychains -d user -s "$keychain" | |
| identities="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p')" | |
| [[ "$(printf '%s\n' "$identities" | sed '/^$/d' | wc -l | tr -d ' ')" == 1 ]] || { | |
| echo "The certificate archive must contain exactly one Developer ID Application identity" >&2 | |
| exit 64 | |
| } | |
| echo "identity=$identities" >> "$GITHUB_OUTPUT" | |
| - name: Build | |
| env: | |
| CODESIGN_IDENTITY: ${{ steps.signing.outputs.identity }} | |
| HEADLESS_ARCHS: arm64 x86_64 | |
| HEADLESS_RELEASE_BUILD: ${{ needs.version.outputs.publish == 'true' && '1' || '0' }} | |
| HEADLESS_VERSION: ${{ needs.version.outputs.version }} | |
| run: ./apps/headless/build.sh | |
| - name: Unit tests | |
| env: | |
| HEADLESS_VERSION: ${{ needs.version.outputs.version }} | |
| run: ./apps/headless/test.sh | |
| - name: E2E | |
| run: zsh ./apps/headless/Tests/macos-e2e.sh | |
| - name: Validate distribution bundle | |
| shell: bash | |
| env: | |
| PUBLISH: ${{ needs.version.outputs.publish }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| mode=adhoc | |
| if [[ "$PUBLISH" == true ]]; then mode=developer-id; fi | |
| ./apps/headless/Tests/macos-distribution.sh \ | |
| apps/headless/Headless.app "$VERSION" "$mode" "arm64 x86_64" | |
| - name: Prepare notarization key | |
| if: needs.version.outputs.publish == 'true' | |
| shell: bash | |
| env: | |
| NOTARY_KEY_BASE64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} | |
| NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| [[ -n "$NOTARY_KEY_BASE64" && -n "$NOTARY_KEY_ID" && -n "$NOTARY_ISSUER_ID" ]] || { | |
| echo "Apple notary API secrets are required for tagged releases" >&2 | |
| exit 64 | |
| } | |
| printf '%s' "$NOTARY_KEY_BASE64" | base64 -D > "$RUNNER_TEMP/AuthKey.p8" | |
| chmod 0600 "$RUNNER_TEMP/AuthKey.p8" | |
| - name: Package | |
| env: | |
| APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} | |
| APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} | |
| APPLE_NOTARY_KEY_PATH: ${{ runner.temp }}/AuthKey.p8 | |
| PUBLISH: ${{ needs.version.outputs.publish }} | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$PUBLISH" == true ]]; then | |
| ./.github/scripts/macos-package.sh \ | |
| apps/headless/Headless.app "$VERSION" apps/headless --notarize | |
| else | |
| ./.github/scripts/macos-package.sh \ | |
| apps/headless/Headless.app "$VERSION" apps/headless | |
| fi | |
| - name: Clean signing material | |
| if: always() && needs.version.outputs.publish == 'true' | |
| shell: bash | |
| env: | |
| KEYCHAIN: ${{ runner.temp }}/headless-signing.keychain-db | |
| run: | | |
| set -euo pipefail | |
| if [[ -f "$KEYCHAIN" ]]; then security delete-keychain "$KEYCHAIN" || true; fi | |
| rm -f "$RUNNER_TEMP/headless-developer-id.p12" "$RUNNER_TEMP/AuthKey.p8" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: macos | |
| path: apps/headless/Headless-${{ needs.version.outputs.version }}-macos.zip | |
| linux-amd64: | |
| needs: version | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Build | |
| env: | |
| HEADLESS_LINUX_PLATFORM: linux/amd64 | |
| HEADLESS_VERSION: ${{ needs.version.outputs.version }} | |
| run: ./apps/headless/build-linux.sh | |
| - name: E2E | |
| run: ./apps/headless/Tests/linux-docker.sh | |
| - name: Package | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| set -eu | |
| cp apps/headless/build/headless-linux-amd64.tar.gz \ | |
| "apps/headless/build/headless-${VERSION}-linux-amd64.tar.gz" | |
| archive="apps/headless/build/headless-${VERSION}-linux-amd64.tar.gz" | |
| test -s "$archive" | |
| tar -tzf "$archive" > archive-contents.txt | |
| for expected in headless headless-host headless-mcp headless-credential-broker install-linux.sh Headless_HeadlessProtocol.resources/AgentRuntime.js; do | |
| grep -qx "$expected" archive-contents.txt | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-amd64 | |
| path: apps/headless/build/headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz | |
| linux-arm64: | |
| needs: version | |
| runs-on: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Build | |
| env: | |
| HEADLESS_LINUX_PLATFORM: linux/arm64 | |
| HEADLESS_VERSION: ${{ needs.version.outputs.version }} | |
| run: ./apps/headless/build-linux.sh | |
| - name: E2E | |
| run: ./apps/headless/Tests/linux-docker.sh | |
| - name: Package | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| run: | | |
| set -eu | |
| cp apps/headless/build/headless-linux-arm64.tar.gz \ | |
| "apps/headless/build/headless-${VERSION}-linux-arm64.tar.gz" | |
| archive="apps/headless/build/headless-${VERSION}-linux-arm64.tar.gz" | |
| test -s "$archive" | |
| tar -tzf "$archive" > archive-contents.txt | |
| for expected in headless headless-host headless-mcp headless-credential-broker install-linux.sh Headless_HeadlessProtocol.resources/AgentRuntime.js; do | |
| grep -qx "$expected" archive-contents.txt | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-arm64 | |
| path: apps/headless/build/headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz | |
| container: | |
| needs: version | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: docker/setup-buildx-action@v4 | |
| - name: Build native production image for verification | |
| run: >- | |
| docker build | |
| --build-arg HEADLESS_VERSION=${{ needs.version.outputs.version }} | |
| --target production | |
| --tag headless-release-smoke:${{ github.run_id }} | |
| --file apps/headless/Dockerfile.linux | |
| apps/headless | |
| - name: Smoke test native production image | |
| run: >- | |
| ./.github/scripts/container-smoke.sh | |
| headless-release-smoke:${{ github.run_id }} | |
| ${{ needs.version.outputs.version }} | |
| - name: Generate container metadata | |
| id: metadata | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.CONTAINER_IMAGE }} | |
| tags: | | |
| type=ref,event=tag | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha,format=long | |
| type=raw,value=latest | |
| labels: | | |
| org.opencontainers.image.title=Headless | |
| org.opencontainers.image.description=Persistent safety-enforced browser control for AI agents | |
| org.opencontainers.image.source=https://github.com/LockInTime/headless | |
| org.opencontainers.image.licenses=MIT | |
| - name: Log in to GHCR | |
| if: needs.version.outputs.publish == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build multi-platform production image | |
| id: publish | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: apps/headless | |
| file: apps/headless/Dockerfile.linux | |
| target: production | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: HEADLESS_VERSION=${{ needs.version.outputs.version }} | |
| labels: ${{ steps.metadata.outputs.labels }} | |
| tags: ${{ steps.metadata.outputs.tags }} | |
| push: ${{ needs.version.outputs.publish == 'true' }} | |
| provenance: mode=max | |
| sbom: true | |
| - name: Verify published manifest and native image | |
| if: needs.version.outputs.publish == 'true' | |
| env: | |
| DIGEST: ${{ steps.publish.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| docker buildx imagetools inspect "$CONTAINER_IMAGE@$DIGEST" | |
| docker logout ghcr.io | |
| for attempt in {1..12}; do | |
| if docker pull "$CONTAINER_IMAGE@$DIGEST"; then break; fi | |
| if [[ "$attempt" == 12 ]]; then | |
| echo "Published container is not anonymously readable" >&2 | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| ./.github/scripts/container-smoke.sh "$CONTAINER_IMAGE@$DIGEST" "${{ needs.version.outputs.version }}" | |
| publish: | |
| needs: [version, npm-launcher, macos, linux-amd64, linux-arm64, container] | |
| if: >- | |
| github.event_name == 'push' && | |
| startsWith(github.ref, 'refs/tags/v') && | |
| needs.version.outputs.publish == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: Add Linux bootstrap installer | |
| run: cp apps/headless/install.sh dist/install.sh | |
| - name: Generate and verify checksums | |
| run: ./.github/scripts/release-checksums.sh "${{ needs.version.outputs.version }}" dist | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: Headless ${{ needs.version.outputs.version }} | |
| generate_release_notes: true | |
| body: | | |
| ## Downloads | |
| | File | Platform | | |
| | --- | --- | | |
| | `Headless-${{ needs.version.outputs.version }}-macos.zip` | macOS universal, Developer ID signed and notarized | | |
| | `headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz` | Linux x86_64 | | |
| | `headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz` | Linux arm64 | | |
| | `install.sh` | Verified Linux bootstrap installer | | |
| | `${{ env.CONTAINER_IMAGE }}:${{ needs.version.outputs.version }}` | Linux amd64/arm64 container | | |
| ### Notes | |
| - **macOS:** Unzip and run `Headless.app`, or install with `brew install --cask LockInTime/headless/headless` after the tap sync completes. The app is universal, Developer ID signed, notarized, and stapled. CLI: `Headless.app/Contents/Resources/bin/headless`. | |
| - **Linux:** `curl -fsSL https://github.com/LockInTime/headless/releases/latest/download/install.sh | sh`. The installer verifies the selected tarball against `SHA256SUMS`. Chromium and FFmpeg remain system dependencies. | |
| - **Container:** `docker pull ${{ env.CONTAINER_IMAGE }}:${{ needs.version.outputs.version }}`. The image includes Chromium and FFmpeg and runs as uid 10001. | |
| - **npm:** `npx @lockintime/headless help`. The launcher verifies the matching GitHub release asset and provides both `headless` and `headless-mcp` shims. | |
| files: | | |
| dist/Headless-${{ needs.version.outputs.version }}-macos.zip | |
| dist/headless-${{ needs.version.outputs.version }}-linux-amd64.tar.gz | |
| dist/headless-${{ needs.version.outputs.version }}-linux-arm64.tar.gz | |
| dist/SHA256SUMS | |
| dist/install.sh | |
| fail_on_unmatched_files: true | |
| publish-npm: | |
| needs: [version, publish] | |
| if: >- | |
| github.event_name == 'push' && | |
| startsWith(github.ref, 'refs/tags/v') && | |
| needs.version.outputs.publish == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| registry-url: https://registry.npmjs.org | |
| - name: Publish verified launcher | |
| working-directory: packages/headless-npm | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| [[ -n "$NODE_AUTH_TOKEN" ]] || { | |
| echo "NPM_TOKEN is required to publish @lockintime/headless" >&2 | |
| exit 64 | |
| } | |
| npm publish --access public --provenance |