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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ jobs:
- name: Context pruning suite
run: pnpm test:runtime

npm-launcher:
name: npm launcher
runs-on: ubuntu-latest
timeout-minutes: 10
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

protocol:
name: Protocol suite
runs-on: ubuntu-latest
Expand Down
54 changes: 53 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:
- "apps/headless/Sources/HeadlessProtocol/ProductVersion.swift"
- "apps/headless/VERSION"
- "apps/headless/VersionSupport/**"
- "packages/headless-npm/**"
- "pnpm-lock.yaml"
workflow_dispatch:
inputs:
dry_run:
Expand Down Expand Up @@ -59,6 +61,11 @@ jobs:
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
echo "publish=true" >> "$GITHUB_OUTPUT"
else
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$DRY_RUN" != "true" ]]; then
Expand All @@ -70,6 +77,22 @@ jobs:
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
Expand Down Expand Up @@ -314,7 +337,7 @@ jobs:
./.github/scripts/container-smoke.sh "$CONTAINER_IMAGE@$DIGEST" "${{ needs.version.outputs.version }}"

publish:
needs: [version, macos, linux-amd64, linux-arm64, container]
needs: [version, npm-launcher, macos, linux-amd64, linux-arm64, container]
if: >-
github.event_name == 'push' &&
startsWith(github.ref, 'refs/tags/v') &&
Expand Down Expand Up @@ -354,10 +377,39 @@ jobs:
- **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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Cutting that release is tracked in
- Tagged macOS releases now ship a universal Apple Silicon/Intel app through a
checksum-pinned Homebrew cask, with Developer ID signing, hardened runtime,
notarization, stapling, and Gatekeeper validation enforced by release CI.
- A checksum-verifying `@lockintime/headless` npm launcher now provides the
CLI and MCP adapter to JavaScript-centric agent harnesses through `npx`.
- Tagged releases now publish a smoke-tested, non-root amd64/arm64 production
image to GHCR with SemVer and commit-SHA tags, provenance, and an SBOM.
- A checksum-verifying Linux bootstrap installer now selects the correct
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ installer that checks Chromium and FFmpeg before copying the binaries. The
Docker image is the supported self-contained Linux runtime and uses Debian's
Chromium binary at `/usr/lib/chromium/chromium`.

### npm / npx

JavaScript-based agent harnesses can run the verified launcher without a
global install:

```sh
npx @lockintime/headless help
npx -p @lockintime/headless headless-mcp
```

The launcher selects the matching macOS or Linux release, verifies it against
the release `SHA256SUMS`, validates its archive and embedded product version,
and caches it privately for subsequent commands. Its download origin is fixed
to this repository.

Tagged releases publish a non-root amd64/arm64 image with Chromium and FFmpeg:

```sh
Expand Down
23 changes: 20 additions & 3 deletions apps/headless/docs/P3.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ binaries. The GHCR production image contains both architectures, runs as uid
10001, exposes no ports, and must be anonymously pullable by digest before the
GitHub release is created.

## npm launcher

`@lockintime/headless` provides `headless` and `headless-mcp` binary shims for
JavaScript-centric tools. The package contains no product binary. On first use
it selects the macOS or Linux release asset matching the package version and
machine architecture, downloads only from GitHub's release hosts, verifies the
asset against the exact `SHA256SUMS` entry, rejects unsafe archive shapes, and
checks the embedded `headless --version` before atomically populating a private
per-user cache. Concurrent invocations share a bounded installation lock.

The launcher does not support Windows natively and points Windows callers to
the GHCR image. `HEADLESS_NPM_CACHE` may select an absolute cache directory;
the release origin deliberately has no environment override.

## Release operator secrets

Tagged macOS releases require these GitHub Actions secrets:
Expand All @@ -50,6 +64,8 @@ Tagged macOS releases require these GitHub Actions secrets:
- `APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD`: PKCS#12 password.
- `APPLE_NOTARY_KEY_BASE64`: base64 App Store Connect API private key.
- `APPLE_NOTARY_KEY_ID` and `APPLE_NOTARY_ISSUER_ID`: notary API identifiers.
- `NPM_TOKEN`: automation token allowed to publish the public
`@lockintime/headless` package.

The Developer ID certificate and notary key must belong to the same Apple
Developer team responsible for `com.headless.app`. Secrets are written only to
Expand All @@ -61,6 +77,7 @@ uploaded as artifacts.
Before a tag is published, the release graph requires protocol/security tests,
real WKWebView and Chromium E2E, universal architecture checks, strict signing
checks, notarization and stapling, package checksums, native and multi-platform
container smoke tests, and Homebrew cask input validation. The tap independently
verifies the published `SHA256SUMS` entry and runs Homebrew style validation
before committing a cask. No check uses `continue-on-error`.
container smoke tests, npm launcher download/checksum/cache tests, npm package
contents, and Homebrew cask input validation. The tap independently verifies
the published `SHA256SUMS` entry and runs Homebrew style validation before
committing a cask. No check uses `continue-on-error`.
7 changes: 4 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ Honest snapshot, so newcomers know what is real:
- ~~No CI on pull requests or `main`~~ — PR CI landed (`.github/workflows/ci.yml`,
backlog §D1/§D3). Correctness fixes in §A are still outstanding, and the
macOS E2E is nightly/label-gated rather than a per-PR gate.
- The npm wrapper and distro-native Linux packages remain unimplemented.
Homebrew, notarized ZIP, Linux bootstrap, checksums, and GHCR automation are
implemented but are not user-visible until the next tag.
- The verified npm launcher, Homebrew cask, notarized universal macOS ZIP,
Linux bootstrap, release checksums, and GHCR publication paths are
implemented but remain unreleased. Distro-native Linux packages remain
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
Expand Down
6 changes: 5 additions & 1 deletion docs/roadmap/improvements-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,11 @@ Owner-decided scope: package managers, no hosted service.
sums; releases don't.~~ **Done:** the publish job requires all three named
regular package files, generates `SHA256SUMS` atomically, verifies it, and
attaches the manifest to the release. Cosign remains optional future work.
- **E5.** ([#43](https://github.com/LockInTime/headless/issues/43)) npm wrapper package (binary download shim) for `npx` reach.
- **E5.** [x] ([#43](https://github.com/LockInTime/headless/issues/43)) ~~npm wrapper package (binary download shim) for `npx` reach.~~
**Done:** `@lockintime/headless` exposes the CLI and MCP adapter, downloads
only its matching official release, verifies the exact `SHA256SUMS` entry,
rejects unsafe archives, validates the embedded version, and reuses a
private per-user cache. CI locks the installer and packed npm contents.
- **E6.** [x] ([#44](https://github.com/LockInTime/headless/issues/44)) Version unification + `headless --version` + CHANGELOG + release
automation (architecture §12). `package.json` says 0.0.0, tags say 1.0.x,
default `HEADLESS_VERSION` is 1.0.0.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dev": "pnpm --filter @headless/web dev",
"start": "pnpm --filter @headless/web start",
"test": "pnpm --filter @headless/app test",
"test:npm": "pnpm --filter @lockintime/headless test",
"test:runtime": "pnpm --filter @headless/app test:runtime",
"test:e2e:mac": "pnpm --filter @headless/app test:e2e:mac",
"test:e2e:linux": "pnpm --filter @headless/app test:e2e:linux"
Expand Down
17 changes: 17 additions & 0 deletions packages/headless-npm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# `@lockintime/headless`

Verified npm launcher for the [Headless agent browser](https://github.com/LockInTime/headless).

```sh
npx @lockintime/headless help
npx -p @lockintime/headless headless-mcp
```

The launcher downloads the release matching its own package version from the
official GitHub repository, verifies the exact asset against `SHA256SUMS`,
validates the archive shape and embedded product version, and caches it in a
private per-user directory. It supports macOS 13+ on Apple Silicon and Intel,
plus Linux x86_64 and arm64. Windows users should use the published GHCR image.

Set `HEADLESS_NPM_CACHE` to an absolute directory to move the verified cache.
The release download origin is fixed and cannot be overridden.
5 changes: 5 additions & 0 deletions packages/headless-npm/bin/headless-mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { launch } from "../lib/launcher.mjs";

await launch("headless-mcp", process.argv.slice(2));
5 changes: 5 additions & 0 deletions packages/headless-npm/bin/headless.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { launch } from "../lib/launcher.mjs";

await launch("headless", process.argv.slice(2));
Loading
Loading