From 1a327cb520d070d4b2e46d74e775d7419fc9822f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 00:43:14 +0000 Subject: [PATCH 1/3] feat: README how-to + 1.0 release pipeline preparation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U1: Rewrite README.md with Mac → Grok Bot Linux featured path - Lead with Linux agent runtime (Grok Bot) as the featured sync target - Demote Mac mini / second Mac below the fold (still supported) - Add numbered CLI primitives for agent-executable setup - Document frozen YAML for Linux sink (live_cdp, blocklist) - Honest status: Linux SQLite write is 0, success is live CDP inject - Archive naming: agentcookie_1.0.0_{platform}_{arch}.tar.gz U2: Update satellite docs - docs/faq.md: Fix MIT license (was Apache 2.0), update Docker paragraph - docs/consumption.md: Clarify Linux verification (live CDP, not sidecar) - docs/architecture.md: Add policy override note for trusted boxes - docs/threat-model.md: v1.0 version bump, policy override documentation - examples/sink.yaml: Rewrite for live_cdp attach (not cdp.managed) - CHANGELOG.md: Add [1.0.0] - 2026-08-13 section U3: Update skill files - skill/SKILL.md: Remove v0.3 planned, add Linux featured path - skill/prompts/install-on-both-machines.md: Two-machine Linux playbook U4: Update release notes template - .github/RELEASE_NOTES_TEMPLATE.md: Replace closed-beta with 1.0 content U5: Update packaging - .goreleaser.yaml: Add Linux builds (amd64, arm64 with cross-compiler) - .github/workflows/release.yml: Split darwin/linux jobs, go-version-file - scripts/release-tarball.sh: Use underscore naming scheme - scripts/install-beta.sh: Update pattern to *darwin_arm64.tar.gz Verification: - go build ./... ✓ - go vet ./... ✓ - go test ./... ✓ (all 26 packages pass) - goreleaser check ✓ - No forbidden phrases in key files ✓ - No changes to denylisted files ✓ Does NOT cut the v1.0.0 tag or publish a GitHub Release (U6 is human-gated). Co-authored-by: Matt Van Horn --- .github/RELEASE_NOTES_TEMPLATE.md | 80 ++-- .github/workflows/release.yml | 163 ++++--- .goreleaser.yaml | 34 +- CHANGELOG.md | 35 ++ README.md | 439 ++++++++---------- docs/architecture.md | 6 +- docs/consumption.md | 67 ++- docs/faq.md | 22 +- ...-13-1720-feat-readme-howto-release-plan.md | 106 +++++ docs/threat-model.md | 10 +- examples/sink.yaml | 74 +-- scripts/install-beta.sh | 4 +- scripts/release-tarball.sh | 24 +- skill/SKILL.md | 194 +++++--- skill/prompts/install-on-both-machines.md | 61 ++- 15 files changed, 848 insertions(+), 471 deletions(-) create mode 100644 docs/plans/2026-08-13-1720-feat-readme-howto-release-plan.md diff --git a/.github/RELEASE_NOTES_TEMPLATE.md b/.github/RELEASE_NOTES_TEMPLATE.md index a2fe748..d9dea90 100644 --- a/.github/RELEASE_NOTES_TEMPLATE.md +++ b/.github/RELEASE_NOTES_TEMPLATE.md @@ -1,48 +1,62 @@ -# agentcookie {{VERSION}} +# agentcookie v1.0.0 -Closed-beta release. Invitation only. +Continuous Mac to Linux cookie sync over Tailscale. Your agent runtime wakes up logged in. -## Install +## Highlights -Download `agentcookie-{{VERSION}}-darwin-arm64.tar.gz` from the assets below, then: +- **Mac to Linux sync**: Your Mac's Chrome sessions flow to a Linux agent runtime (Grok Bot, cloud VM, homelab server) via live CDP injection over Tailscale +- **Live CDP injection**: Cookies go directly into Chrome's in-memory store via `Storage.setCookies` - no SQLite write, no Keychain, no libsecret +- **Tailscale-only transport**: AES-256-GCM sealed envelopes over your tailnet's WireGuard channel +- **Security-by-default**: Linux sinks with missing policy ship nothing; explicit `policy: blocklist` required for sync-all -``` -tar -xzf agentcookie-{{VERSION}}-darwin-arm64.tar.gz -cd agentcookie-{{VERSION}}-darwin-arm64 -./install-beta.sh --as source # on your MacBook -# or -./install-beta.sh --as sink # on your second Mac +## Install + +Download from the assets below and verify against `checksums.txt`: + +| Platform | Archive | +|----------|---------| +| macOS arm64 | `agentcookie_1.0.0_darwin_arm64.tar.gz` | +| Linux amd64 | `agentcookie_1.0.0_linux_amd64.tar.gz` | +| Linux arm64 | `agentcookie_1.0.0_linux_arm64.tar.gz` | + +```bash +# On Mac +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_darwin_arm64.tar.gz +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/checksums.txt +shasum -a 256 -c checksums.txt --ignore-missing +tar -xzf agentcookie_1.0.0_darwin_arm64.tar.gz +sudo mv agentcookie /usr/local/bin/ + +# On Linux +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_linux_amd64.tar.gz +sha256sum -c checksums.txt --ignore-missing +tar -xzf agentcookie_1.0.0_linux_amd64.tar.gz +sudo mv agentcookie /usr/local/bin/ ``` -See `quickstart-beta.md` inside the tarball for the ten-minute walkthrough. +Or build from source: `go install github.com/mvanhorn/agentcookie/cmd/agentcookie@v1.0.0` -## Verifying the binary +See the [README](https://github.com/mvanhorn/agentcookie/blob/main/README.md) for the full how-to. -``` -codesign --verify --strict --verbose=2 agentcookie -# expected: valid on disk / satisfies its Designated Requirement +## Honest limits -codesign -d -r- agentcookie -# expected: identifier "agentcookie" ... certificate leaf[subject.OU] = NM8VT393AR -``` +- **Linux SQLite write is 0**: Expected. Success is the `live_cdp: injected N cookies into M context(s)` line. +- **Omitted policy ships nothing**: On Linux, missing `blocklist.yaml` or omitted `policy:` means allowlist-empty. Write `policy: blocklist` with `domains: []` for sync-all on a trusted box. +- **Google/DBSC cookies**: Need local sign-in on the sink. Copied cookies expire in minutes. +- **CDP port is loopback-only**: Same-user processes can attach to `127.0.0.1:9223` and read injected cookies. This is the same-user trust boundary. +- **Cookie values never logged**: Cookie values do not appear in logs or doctor output. -`spctl -a` is the wrong assessment tool for this CLI binary - it -expects an app bundle and reports "rejected: not an app" even when -the binary is correctly signed and notarized. Use the `codesign` -commands above instead. The notarization ticket is verified by -Apple's notary service on first launch. +## Security -## What's in this release - -{{CHANGELOG_BODY}} +- Tailscale 100.x bind required on Linux sinks; refuses to start on 0.0.0.0 +- AES-256-GCM sealed envelopes with pairing-derived 32-byte keys +- Per-IP rate limiting on the pair endpoint (5 attempts before 429) +- Persistent replay defense via sequence tracking -## Known limits (closed beta) +## Support -- macOS only on both ends (Linux and Windows sinks are on the roadmap). -- Plaintext sidecar at rest is the default. Sealed sidecar infrastructure is wired up but off until U12 PP CLI migration ships in cli-printing-press. -- No live key rotation. To rotate, re-run `agentcookie wizard install` on both sides. -- eBay sessions are fingerprint-bound at the server side; expect `ebay-pp-cli` to fail authentication regardless of sync state. +Run `agentcookie doctor --json` on both machines and include the output when reporting issues. -## Reporting issues +## What's in this release -DM the person who invited you. Include the output of `agentcookie doctor --json`. +{{CHANGELOG_BODY}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d81c445..d23846c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,39 +9,76 @@ permissions: contents: write jobs: - goreleaser: + # Build Linux binaries on Ubuntu (CGO_ENABLED=1 requires native gcc) + build-linux: + runs-on: ubuntu-latest + if: ${{ vars.RELEASE_CI_ENABLED == 'true' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + # Install cross-compiler for arm64 + - name: install arm64 cross-compiler + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + # Build Linux amd64 + - name: build linux amd64 + run: | + CGO_ENABLED=1 go build -ldflags "-s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version=${{ github.ref_name }}" -o dist/linux-amd64/agentcookie ./cmd/agentcookie + env: + GOOS: linux + GOARCH: amd64 + + # Build Linux arm64 + - name: build linux arm64 + run: | + CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -ldflags "-s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version=${{ github.ref_name }}" -o dist/linux-arm64/agentcookie ./cmd/agentcookie + env: + GOOS: linux + GOARCH: arm64 + + # Create tarballs with the frozen naming scheme + - name: create linux tarballs + run: | + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" # Strip leading v + + mkdir -p release + + # Linux amd64 + tar -czvf "release/agentcookie_${VERSION}_linux_amd64.tar.gz" -C dist/linux-amd64 agentcookie + + # Linux arm64 + tar -czvf "release/agentcookie_${VERSION}_linux_arm64.tar.gz" -C dist/linux-arm64 agentcookie + + - name: upload linux artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-binaries + path: release/*.tar.gz + + # Build darwin binaries on macOS (codesigning requires macOS) + build-darwin: runs-on: macos-latest - # Skip the entire job when the signing secrets aren't configured. - # Releases are cut locally by the maintainer until CERTIFICATE_OSX_APPLICATION - # and AC_NOTARY_PASSWORD are populated; without this guard `make release` - # would fail on every tag and pollute the CI history. if: ${{ vars.RELEASE_CI_ENABLED == 'true' }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version-file: go.mod cache: true - # Import the Developer ID Application certificate into a temporary - # keychain so codesign can find it. The .p12 bundle and its password - # are repo secrets. The action creates an ephemeral keychain, unlocks - # it, and adds it to the search list for the rest of the job. - # - # To enable CI-driven releases, the maintainer must add the following - # GitHub Actions secrets and set the `RELEASE_CI_ENABLED` repo variable - # to `true` (the job-level `if:` above gates on the variable): - # - CERTIFICATE_OSX_APPLICATION : base64-encoded .p12 of the - # Developer ID Application cert + private key - # - CERTIFICATE_OSX_APPLICATION_PASSWORD : password for the .p12 - # - AC_NOTARY_PASSWORD : app-specific password for Apple notarytool - # The matching cert is Common Name - # "Developer ID Application: Matthew Charles Van Horn (NM8VT393AR)" - # See docs/runbook-v0.12-codesign.md for the export / renewal flow. - # Until the variable is flipped, releases are cut locally with - # `make release` + scripts/release-tarball.sh + `gh release create`. - name: import Developer ID cert if: ${{ secrets.CERTIFICATE_OSX_APPLICATION != '' }} uses: apple-actions/import-codesign-certs@v3 @@ -49,20 +86,9 @@ jobs: p12-file-base64: ${{ secrets.CERTIFICATE_OSX_APPLICATION }} p12-password: ${{ secrets.CERTIFICATE_OSX_APPLICATION_PASSWORD }} - # Sanity: print which codesigning identities are visible to this - # runner. On a properly configured release this lists the Developer - # ID Application identity NM8VT393AR. If the secret is unset, the - # list will be empty and goreleaser's signing post-hook will fail - # fast with the runbook pointer. - name: list codesigning identities run: security find-identity -v -p codesigning - # Store the notarytool credentials in the runner's login keychain. - # The maintainer generates an app-specific password at - # appleid.apple.com and saves it as the AC_NOTARY_PASSWORD secret. - # See docs/runbook-v0.12-codesign.md "Notarization" for the - # one-time setup steps. Without this step, scripts/notarize.sh - # below would fail with a "credentials not found" error. - name: setup notarytool credentials if: ${{ secrets.AC_NOTARY_PASSWORD != '' }} run: | @@ -71,38 +97,57 @@ jobs: --team-id NM8VT393AR \ --password "${{ secrets.AC_NOTARY_PASSWORD }}" - # Build, sign, notarize the agentcookie binary. After this step - # bin/agentcookie is fully launchable on any Mac without - # Gatekeeper interactive approval. - name: make release run: make release - # Bundle the notarized binary + install-beta.sh + quickstart into - # a single tarball friends can extract and run. - name: build release tarball run: scripts/release-tarball.sh "${{ github.ref_name }}" - # GoReleaser also runs to publish the standard cross-platform - # archives and the GitHub release entry with notes. It uses the - # signed bin/agentcookie produced above. - - name: goreleaser - uses: goreleaser/goreleaser-action@v6 + - name: upload darwin artifacts + uses: actions/upload-artifact@v4 with: - distribution: goreleaser - version: "~> v2" - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AGENTCOOKIE_SIGN_IDENTITY: "Developer ID Application: Matthew Charles Van Horn (NM8VT393AR)" - - # Attach the closed-beta bundle tarball to the GitHub release - # that goreleaser just created. The bundle is the install-script- - # consumable shape; goreleaser's own archives are the - # cross-platform binary archives. - - name: attach beta bundle to release + name: darwin-binaries + path: dist/*.tar.gz + + # Publish release after both builds complete + publish: + runs-on: ubuntu-latest + needs: [build-linux, build-darwin] + if: ${{ vars.RELEASE_CI_ENABLED == 'true' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: download linux artifacts + uses: actions/download-artifact@v4 + with: + name: linux-binaries + path: dist/ + + - name: download darwin artifacts + uses: actions/download-artifact@v4 + with: + name: darwin-binaries + path: dist/ + + - name: list artifacts + run: ls -la dist/ + + # Generate checksums for all archives + - name: generate checksums + run: | + cd dist + sha256sum *.tar.gz > checksums.txt + cat checksums.txt + + # Create the GitHub release with all assets + - name: create release run: | - gh release upload "${{ github.ref_name }}" \ - dist/agentcookie-*-darwin-arm64.tar.gz \ - --clobber + gh release create "${{ github.ref_name }}" \ + dist/*.tar.gz \ + dist/checksums.txt \ + --title "agentcookie ${{ github.ref_name }}" \ + --generate-notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2d10c60..de22112 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,7 +7,8 @@ before: - go mod tidy builds: - - id: agentcookie + # Darwin build: runs on macos-latest, codesigned + - id: agentcookie-darwin main: ./cmd/agentcookie binary: agentcookie env: @@ -19,15 +20,38 @@ builds: - amd64 ldflags: - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} - # Sign each darwin binary in place before it is archived. Uses - # scripts/sign.sh so the local + release pipelines share one - # codesign invocation. AGENTCOOKIE_SIGN_IDENTITY is set by the - # release workflow (and defaults inside scripts/sign.sh if unset). hooks: post: - cmd: scripts/sign.sh {{ .Path }} output: true + # Linux amd64 build: runs on ubuntu-latest with default gcc + - id: agentcookie-linux-amd64 + main: ./cmd/agentcookie + binary: agentcookie + env: + - CGO_ENABLED=1 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} + + # Linux arm64 build: runs on ubuntu-latest with cross-compiler + - id: agentcookie-linux-arm64 + main: ./cmd/agentcookie + binary: agentcookie + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + goos: + - linux + goarch: + - arm64 + ldflags: + - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} + archives: - id: agentcookie name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2112dec..61eb453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [1.0.0] - 2026-08-13 + +### Featured: Mac to Linux continuous sync + +The 1.0 release ships Mac to Linux cookie sync as the featured path. Your Mac's Chrome sessions flow to a Linux agent runtime (Grok Bot, cloud VM, homelab server) via live CDP injection over Tailscale. + +**What's new:** + +- Linux binaries: `agentcookie_1.0.0_linux_amd64.tar.gz` and `agentcookie_1.0.0_linux_arm64.tar.gz` +- Live CDP injection on Linux: cookies go into Chrome's in-memory store via `Storage.setCookies` +- No Chrome SQLite write on Linux (expected; success is the live CDP inject line) +- Tailscale-only transport with AES-256-GCM sealed envelopes + +**Install:** + +Download from [GitHub Releases](https://github.com/mvanhorn/agentcookie/releases/tag/v1.0.0). Verify against `checksums.txt`. See the README for the full how-to. + +**Security defaults:** + +- Linux sinks with missing `blocklist.yaml` or omitted `policy:` ship nothing (allowlist-empty). This is security-by-default. +- For a single-operator trusted box, write `blocklist.yaml` with `policy: blocklist` and `domains: []` to enable sync-all. +- The 1.0 release does NOT change this default in code. A later release may flip the default, which would be a breaking change. + +**Honest limits:** + +- Linux `wrote 0 cookies` is expected (success is live CDP inject) +- Google/DBSC cookies need local sign-in on the sink; copied cookies expire in minutes +- CDP port is loopback-only; same-user processes can attach to the debug port +- Cookie values never appear in logs + +**Breaking changes from beta:** + +- Archive naming changed to underscores: `agentcookie_1.0.0_darwin_arm64.tar.gz` (was `agentcookie-VERSION-darwin-arm64.tar.gz`) +- `install-beta.sh` pattern updated to match new archive names + ## [Unreleased] ### Cookie allowlist policy diff --git a/README.md b/README.md index 53357ff..7f1a04b 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,232 @@ # agentcookie -Your agent runs on a Mac that isn't your daily driver. It needs to act as you on every site you're already logged into and against every API you've already authenticated. agentcookie keeps your second Mac's session state (Chrome cookies, per-CLI bearer tokens, API keys, and the auth blobs your tools persist next to them) in sync with your first Mac's, continuously, encrypted over your Tailscale tailnet, with zero per-site auth ceremony. +Your agent runs on a Linux box (a Grok Bot VM, a cloud agent runtime, a homelab server) and needs to act as you on every site you're already logged into. agentcookie keeps that box's Chrome session in sync with your Mac's, continuously, encrypted over your Tailscale tailnet, with zero per-site auth ceremony. -OpenClaw, Hermes, or any other agent runtime you point at the second Mac wakes up authenticated, on the web and in the terminal. +The Linux Chrome wakes up logged in. browserUse, Puppeteer, Playwright, or any Chromium automation that connects to Chrome's debug port sees your session already there. ## What it looks like -You browse normally on your first Mac. agentcookie watches Chrome's Cookies file (and a parallel per-CLI secrets bus) and ships the diff to your second Mac the moment anything changes. On the second Mac, an agent does its work: +You browse normally on your Mac. agentcookie watches Chrome's Cookies file and ships the diff to your Linux sink the moment anything changes. On the Linux box, an agent does its work: ``` -$ ssh second-mac 'instacart-pp-cli carts' +$ ssh grok-bot 'python3 -c " +from browser_use import BrowserUse +with BrowserUse(cdp_url=\"http://127.0.0.1:9223\") as b: + print(b.page.goto(\"https://github.com/settings/profile\").title()) +"' +Profile settings + +$ ssh grok-bot 'instacart-pp-cli carts' Costco slug=costco cart=757109404 items=5 Safeway slug=safeway cart=3190 items=1 - -$ ssh second-mac 'ebay-pp-cli auctions "watch" --has-bids --ending-within 1h' -12 active auctions: - $352 23 bids 1m Apple Watch Ultra 2 49mm Titanium ... - $115 26 bids 1m Gucci 5500M Steel Quartz ... - ... - -$ ssh second-mac 'table-reservation-goat-pp-cli goat "omakase" --location seattle' -{ "results": [ { "name": "Omakase Dinner Series", "network": "tock", ... } ] } ``` -No `auth login`. No Keychain prompt. No paste-the-cookie ritual. No re-entering API keys you already configured on your laptop. The agent's sessions were already there when the request hit. - -The same is true for browser-driving agents and for any unmodified cookie tool. On a universal sink (the default), agentcookie writes your real Default Chrome profile and opens its Safe Storage key with a single login-password entry at install, so a tool that has never heard of agentcookie, yt-dlp, gallery-dl, a Polymarket CLI, a browser-driving agent, reads your synced, logged-in session with no per-tool setup. Prefer not to touch Chrome at all? Read the plaintext cookies sidecar at `~/.agentcookie/cookies-plain.db`, and the per-CLI secrets under `~/.agentcookie/secrets//secrets.env`. +No `auth login`. No paste-the-cookie ritual. The agent's session was already there when the request hit. ## What this fixes -Logging in twice. Once on your laptop, once again on the Mac your agent lives on. Per site, per CLI, per API key. Forever. +Logging in twice. Once on your Mac, once again on the Linux box where your agent runs. Per site, forever. -Tools that ship cookies between machines today assume a human is going to click "merge" or unlock a vault or open the destination browser. They were built for switching accounts between two laptops the same person uses. They weren't built for "the agent on the headless Mac mini needs my session in 30 seconds and there's nobody home." +Tools that ship cookies between machines today assume a human is going to click "merge" or unlock a vault or open the destination browser. They were built for switching accounts between two laptops the same person uses. They weren't built for "the agent on the Grok Bot VM needs my session in 30 seconds and there's nobody home." -agentcookie is the second pattern. One-way, continuous, unattended replication from the machine you live in to the machine your agents act from. Pairing-derived per-peer keys, cookie policy filters on both sides, AES-256-GCM over the Tailscale tailnet's WireGuard channel. The hard parts (macOS Keychain protections, Chrome's App-Bound Encryption, per-CLI auth conventions) are handled. +agentcookie is the second pattern. One-way, continuous, unattended replication from the Mac you live in to the Linux box your agents act from. Pairing-derived per-peer keys, cookie policy filters on both sides, AES-256-GCM over the Tailscale tailnet's WireGuard channel. The hard parts (macOS Keychain protections, Chrome's App-Bound Encryption on the source, live CDP injection on the sink) are handled. ## How it works ``` -laptop second Mac -====== ========== - -Chrome cookies change secrets bus change -(fsnotify on Cookies) (fsnotify on ~/.agentcookie/ - | secrets//secrets.env, - | or autodiscovered via - | agentcookie.toml manifests) - | | - +--------------+-----------------+ - | - v -agentcookie source --watch (decrypt Chrome with Keychain key, - filter against cookie policy, fold in - secrets bus payload) - | - v -+----- HTTPS over Tailscale (AES-256-GCM, replay-defended) -----+ - | - v - agentcookie sink (LaunchAgent) - | - | cookie delivery surfaces: - v - 1. Chrome's Cookies SQLite (re-encrypted for sink Keychain) - 2. Plaintext sidecar at ~/.agentcookie/cookies-plain.db - (env var: AGENTCOOKIE_PLAIN_COOKIES) - 3. Per-CLI adapter fan-out: - instacart -> session.json - airbnb -> config.toml + cookies.json - ebay -> config.toml + cookies.json - pagliacci -> config.toml + cookies.json - table-reservation-goat -> session.json - 4. cmux WebKit browser (opt-in) via - cmux rpc browser.cookies.set - - plus the secrets bus mirror: - ~/.agentcookie/secrets//secrets.env (mode 0600, - optional sealed twin under the v0.12 master key) +Mac (source) Linux (sink) +============ ============ + +Chrome cookies change +(fsnotify on Cookies) + | + v +agentcookie source --watch + - read SQLite (RO) + - decrypt w/ Keychain key + - filter by cookie policy + - wrap in envelope + - seal w/ peer key + | + +-- HTTPS over Tailscale (AES-256-GCM) ----------> agentcookie sink + - listen 100.x:9999/sync + - decrypt seal + - filter by policy + - CDP attach to Chrome + - Storage.setCookies per + browser context + +No Keychain on Linux. No Chrome SQLite rewrite. Just live CDP injection. ``` -Multiple cookie surfaces because different agents read cookies differently. Universal delivery (surface 1, the real Default profile plus the one-password Safe Storage open) is the default and is what makes any unmodified cookie tool work; the sidecar (surface 2) and per-CLI adapters (surface 3) are the agentcookie-aware paths that also work in degraded mode, when no login password is available to open the key. The sink runs surfaces 1 through 3 after every sync, so the agent picks what fits. +The sink injects cookies directly into Chrome's in-memory store via the Chrome DevTools Protocol. Chrome on the Linux box must be started with `--remote-debugging-port=9223` (or another port you configure). The inject happens on every sync and on every new browser context, so an agent that launches a fresh tab inherits the session immediately. -Surface 4 is the opt-in cmux surface. cmux ([cmux.com](https://cmux.com)) ships its own embedded browser on Apple WebKit with a cookie jar separate from Chrome's, so none of the other surfaces reach it. Enable it and the sink injects the synced session into cmux's browser after every sync (`cmux rpc browser.cookies.set`), so an agent driving cmux's browser pane wakes up authenticated. Injected cookies persist at cmux's profile level, so one injection carries to the agent's later panes. See [cmux delivery](#cmux-delivery-opt-in). +## Install -Bearer tokens, API keys, and other per-CLI auth blobs ride the same encrypted push and land at `~/.agentcookie/secrets//secrets.env` on the sink. CLIs read them via environment variables, the in-process `pkg/agentcookiesecret` Go library, or a project's own `agentcookie.toml` manifest (see the adoption standard below). +### Download release binaries -New cookie adapters are roughly 50 lines of Go and a `Register()` call; the runbook walks through it. New secrets bus consumers usually require no agentcookie-side change at all: drop an `agentcookie.toml` next to your CLI and `agentcookie discover` finds it. +From the [GitHub Releases](https://github.com/mvanhorn/agentcookie/releases/tag/v1.0.0) page, download the archive for your platform: -## cmux delivery (opt-in) +| Platform | Archive | +|----------|---------| +| macOS arm64 | `agentcookie_1.0.0_darwin_arm64.tar.gz` | +| Linux amd64 | `agentcookie_1.0.0_linux_amd64.tar.gz` | +| Linux arm64 | `agentcookie_1.0.0_linux_arm64.tar.gz` | -cmux's browser is Apple WebKit, with a cookie jar separate from Chrome's, so it needs its own surface. Enable it in `sink.yaml`: +Verify against `checksums.txt`: -```yaml -cmux: - enabled: true - # cmux_path: /custom/path/to/cmux # optional; default resolves the app bundle, then PATH - # domain_filter: # optional; SQLite-LIKE host_key patterns. empty = all synced cookies - # - "%github.com" - # - "%openai.com" +```bash +# On Mac +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_darwin_arm64.tar.gz +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/checksums.txt +shasum -a 256 -c checksums.txt --ignore-missing + +tar -xzf agentcookie_1.0.0_darwin_arm64.tar.gz +sudo mv agentcookie /usr/local/bin/ + +# On Linux +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_linux_amd64.tar.gz +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/checksums.txt +sha256sum -c checksums.txt --ignore-missing + +tar -xzf agentcookie_1.0.0_linux_amd64.tar.gz +sudo mv agentcookie /usr/local/bin/ ``` -One required cmux-side step: cmux's RPC socket defaults to `socketControlMode: "cmuxOnly"`, which only accepts processes started inside cmux. The agentcookie sink is a LaunchAgent, not a cmux child, so with the default it is rejected and no cookies land. Open the socket to the sink: +Or build from source after the tag: -```jsonc -// ~/.config/cmux/cmux.json -{ - "automation": { - "socketControlMode": "allowAll" // or "password" (then set automation.socketPassword) - } -} +```bash +go install github.com/mvanhorn/agentcookie/cmd/agentcookie@v1.0.0 ``` -Then fully restart cmux (Quit and reopen). The mode is read only at app launch; `cmux reload-config` does not apply it. Verify with `cmux capabilities | grep access_mode` (it should no longer say `cmuxOnly`), or just run `agentcookie doctor`, which reports the cmux delivery surface and prints this exact remediation when the gate is still closed. +### Prereqs -Caveats: the surface delivers cookies only, so sites whose session also lives in localStorage/IndexedDB or is device-bound (DBSC, e.g. Google/Workspace) may still need a one-time sign-in inside the cmux pane; WebKit's ITP can also drop some cross-site cookies. The surface is best-effort and non-fatal: if cmux is not running or still gated, the sync and the other three surfaces are unaffected. +- Tailscale running on both machines +- Chrome installed on both machines +- On Linux: Chrome started with `--remote-debugging-port=9223` -### Local loop (one machine, no sink) +### Mac source setup -The sink surface above is for the two-machine model. If you just want *this* Mac's Chrome logins to flow into *this* Mac's cmux browser, use the local loop instead. No second machine, no Tailscale, no pairing. +```bash +# 1. Run the source wizard (interactive) +agentcookie wizard install --as source --peer + +# The wizard prints a pairing code and URL. Keep this terminal open. +# Example output: +# Pairing code: ABCD-EFGH-IJKL +# Pair URL: http://your-mac.tailnet:9998/pair +# Waiting for sink to pair... +``` -**On by default when cmux is installed.** `agentcookie wizard install` detects cmux and turns the loop on automatically: it sets cmux's `socketControlMode` to `allowAll`, installs a launch agent that runs `cmux-sync --watch` over your full cookie set, and tells you to **restart cmux once** to activate (the mode is read only at app launch). After that single restart it stays in sync hands-free. Opt out with `agentcookie wizard install --no-cmux`. Turn it on/off later with `agentcookie cmux-sync enable` / `disable`. `agentcookie doctor` reports the loop's liveness (enabled / needs-restart / live). +### Linux sink setup (featured: Grok Bot / trusted single-operator box) -For manual or one-shot use: +Do NOT run `wizard install --as sink` on Linux. The wizard omits the policy file, which means allowlist-empty (ship nothing). Instead, write the YAML files directly: ```bash -# one-shot: read Chrome now, inject into cmux -agentcookie cmux-sync --once +# 2. Create the config directory +mkdir -p ~/.config/agentcookie -# continuous: re-inject whenever Chrome cookies change (fsnotify) -agentcookie cmux-sync --watch +# 3. Write sink.yaml +cat > ~/.config/agentcookie/sink.yaml << 'EOF' +listen: + addr: 100.x.y.z:9999 # Your Linux box's Tailscale IP -# narrow to specific sites -agentcookie cmux-sync --watch --domain "%github.com" --domain "%amazon%" +peer: + hostname: your-mac.tailnet # Mac's Tailscale hostname + +live_cdp: + enabled: true + endpoint: http://127.0.0.1:9223 # Chrome's debug port + +skip_chrome_sqlite: true +EOF + +# 4. Write blocklist.yaml for sync-all on a trusted box +cat > ~/.config/agentcookie/blocklist.yaml << 'EOF' +version: 1 +policy: blocklist +domains: [] +EOF + +# 5. Pair with the Mac source +agentcookie pair --as sink \ + --peer your-mac.tailnet \ + --code ABCD-EFGH-IJKL \ + --pair-url http://your-mac.tailnet:9998/pair ``` -It reuses `source.yaml`'s Chrome path and cookie policy (so your block or allow rules still apply) and the same decrypt + DBSC filtering as `source`. Configure defaults under a `cmux:` block in `source.yaml` (same shape as the sink block above); flags override. +Replace: +- `100.x.y.z` with your Linux box's Tailscale IP (`tailscale ip -4`) +- `your-mac.tailnet` with your Mac's Tailscale hostname (`tailscale status` on either machine) +- The pairing code and URL with the values printed by the Mac source wizard -Run model and the `cmuxOnly` gate: +### Start Chrome with CDP enabled -- **From inside cmux (recommended):** run `cmux-sync` in a cmux terminal. The process is a cmux child, so it passes the default `cmuxOnly` gate with no cmux change at all. -- **Unattended (launchd):** a LaunchAgent is not a cmux child, so it needs `socketControlMode` set to `allowAll`/`password` and a cmux restart (same as the sink, above). `agentcookie doctor` reports the local loop's state and prints the fix. +```bash +# On Linux, start Chrome with the debug port +google-chrome --remote-debugging-port=9223 & -Keychain note: run the **installed, signed `agentcookie`** binary. Reading Chrome's Safe Storage key is a one-time Keychain grant for that signed binary (set up at `wizard install`), so it does not prompt. Running via `go run` or an unsigned/rebuilt binary will pop the macOS Keychain password prompt on every run, because the grant is scoped per binary. +# Or headless +google-chrome --remote-debugging-port=9223 --headless=new & +``` -## Agent browsers (browser-use, agent-browser) +If Chrome runs on a different port (e.g., 9228), update `live_cdp.endpoint` in sink.yaml. -cmux is WebKit. The Chromium agent browsers used for automation and HAR sniffing -- **browser-use** and vercel-labs **agent-browser** -- get their own loop: `agent-sync`. It launches a dedicated Chrome on a loopback debug port, reads this Mac's Chrome cookies (same decrypt + cookie policy + DBSC pipeline as `source`), and injects them -- as plaintext, over CDP -- into every browser context that Chrome opens, including the context a connector creates for itself. browser-use / agent-browser connect via `--cdp-url` and wake up logged into your sites. +### Start the sink ```bash -agentcookie agent-sync # launch + sync, hold until Ctrl-C -agentcookie agent-sync --headed # show the owned browser window -agentcookie agent-sync --domain "%github.com" # limit to matching hosts +agentcookie sink ``` -It prints the connect commands for the running browser: +For a persistent daemon, copy the systemd user unit printed by `agentcookie wizard install --as sink` on macOS (or write your own). Do not auto-install it; review and place it yourself: ```bash -browser-use --cdp-url http://127.0.0.1:9400 open https://github.com -agent-browser --cdp 9400 +mkdir -p ~/.config/systemd/user/ +# Paste the unit content +systemctl --user daemon-reload +systemctl --user enable --now agentcookie-sink.service ``` -Why this works where copying cookies does not: this is **live injection into a running browser**, not a cold on-disk profile or a Playwright `storage_state` file. Cookies go straight into Chrome's in-memory store via CDP, so Chrome 127+ App-Bound Encryption -- which makes cold-profile cookies undecryptable on load -- never applies, and httpOnly + persistent session cookies (the real auth cookies, which Playwright's `addCookies` rejects) carry fine. The owned Chrome uses its own `--user-data-dir`, so the debug port is honored without the `chrome://inspect` toggle (Chrome 136+ only blocks the port on the *default* profile) and your everyday Chrome is never touched. Verified end to end: browser-use connected to `agent-sync` reads logged-in on github.com, including the login-gated `/settings/profile`. +### Verify -It re-injects whenever your Chrome cookies change (fsnotify, same loop `cmux-sync` uses) and injects each new context as it appears, so a site you log into in your real Chrome becomes logged-in in the agent browser without a restart. +```bash +# On Mac +agentcookie doctor +agentcookie status --json -Limits: **device-bound (DBSC) cookies cannot transfer** to another browser -- Google/Workspace account cookies are the broad adopter -- so those sites may still read logged-out; everything else (the large majority) works. Sites whose auth lives in localStorage/IndexedDB rather than cookies are not yet carried (cookies-first; localStorage injection is a planned follow-up). Keychain note above applies (run the signed binary). +# On Linux +agentcookie doctor +agentcookie status --json +``` -## Install +On Linux, `doctor` reports expected FAILs for macOS-specific checks (codesign, Chrome.app path, launchctl). Look for: -Prereqs: Tailscale running on both Macs, Chrome installed, Go 1.22+ (or a pre-built release). +- `live_cdp: endpoint reachable` - must be OK +- `tailnet: bind address` - must be OK +- Status output with `LastWriteMode` containing `livecdp` +- `live_cdp: injected N cookies into M context(s)` in sink output -``` -# On both machines: -go install github.com/mvanhorn/agentcookie/cmd/agentcookie@latest +The message `wrote 0 cookies` for Chrome SQLite is expected on Linux. Success is the live CDP inject line. -# On the first Mac (source): -agentcookie wizard install --as source --peer +## Cookie policy -# It prints a pairing code. On the second Mac (sink), paste: -agentcookie wizard install --as sink --peer \ - --code --pair-url http://:9998/pair -``` +### Linux defaults to allowlist-empty (ship nothing) -The sink wizard installs a LaunchAgent, opens Chrome Safe Storage for universal delivery with one login-password entry over SSH (no GUI click), and registers the five built-in adapters that fire after every sync. If no password is available (a fully non-interactive install with no `AGENTCOOKIE_LOGIN_PASSWORD`), it lands in degraded mode (sidecar + adapters) and prints the one-line `agentcookie wizard set-keychain-access` upgrade command. After install, all sync work runs unattended. +On Linux, a missing `blocklist.yaml` or omitted `policy:` field means the sink accepts no cookies. This is security-by-default for untrusted sinks. -See [docs/quickstart.md](docs/quickstart.md) for the long-form walkthrough and [docs/quickstart-beta.md](docs/quickstart-beta.md) for the headless flow if you're installing the second Mac over SSH. +For a single-operator trusted box (like your own Grok Bot VM), the featured setup writes: -## Verify it's working - -``` -agentcookie doctor # both sides' health -agentcookie wizard verify-adapters # per-adapter results from the last sync -agentcookie wizard verify-adapters --json # same, structured for SSH agents +```yaml +version: 1 +policy: blocklist +domains: [] ``` -Turn cookie sync off or back on for a site without editing YAML: +This syncs all cookies. The 1.0 release does NOT change this default in code. A later release may flip the default, which would be a breaking change. -~~~bash -agentcookie accounts off x.com # add x.com + subdomains to blocklist.yaml -agentcookie accounts on x.com # remove those blocklist entries -agentcookie accounts list # show disabled domains and custom patterns -~~~ +### For multi-user or less-trusted sinks -By default `blocklist.yaml` is an opt-out filter: omitted `policy`, a missing -file, or an empty domains list preserves sync-all behavior. For a high-trust -agent runtime where you only want specific browser sessions to leave the source -machine, make the policy explicit: +Use allowlist mode to sync only specific domains: ```yaml version: 1 @@ -213,140 +234,72 @@ policy: allowlist domains: - pattern: "github.com" - pattern: "%.github.com" + - pattern: "%.openai.com" ``` -Allowlist mode runs on both source and sink. Only matching `host_key` patterns -sync; an empty allowlist syncs no cookie hosts. `agentcookie status` and -`agentcookie doctor` report the active cookie policy, and `accounts on/off` -remain blocklist-only helpers. +## macOS sink (second Mac / Mac mini) -Healthy output: +macOS sinks are still supported. The wizard works: +```bash +# On the second Mac +agentcookie wizard install --as sink \ + --peer \ + --code \ + --pair-url http://:9998/pair ``` -ADAPTER STATUS PUSHED DETAIL -------- ------ ------ ------ -instacart-pp-cli ok 33 -airbnb-pp-cli ok 25 -ebay-pp-cli ok 51 -pagliacci-pp-cli skip no matching cookies -table-reservation-goat-pp-cli ok 36 - -last run: 4s ago -``` - -## Status - -The source is macOS only (it reads Chrome via the macOS Keychain-backed decrypt path). The sink runs on macOS or Linux: - -- **macOS sink**: full two-machine continuous sync via Tailscale `/sync`. Writes Chrome SQLite + plaintext sidecar + per-CLI adapters. LaunchAgent for unattended sync. -- **Linux sink** (new in v0.14): continuous sync via Tailscale `/sync`, with live CDP injection into a running Chrome. This is the Grok Bot / agent-runtime path: the Linux box wakes up logged into source-allowlisted sites without a second login. No Chrome SQLite rewrite, no Keychain, no libsecret — just live CDP injection into Chrome started with `--remote-debugging-port`. The Linux sink MUST bind a Tailscale 100.x address; it will not start without a tailnet. Security: missing policy defaults to allowlist-empty (ship nothing) on Linux, so the operator must explicitly configure which domains sync. - -### Linux sink configuration - -The Linux sink injects cookies via CDP into an already-running Chrome. Key configuration points: -1. **live_cdp.endpoint**: Set in `sink.yaml` to the Chrome debug port. Default is `http://127.0.0.1:9223`. If your Chrome runs on a different port (e.g., 9228), configure it: +The macOS sink writes to Chrome's encrypted SQLite, the plaintext sidecar, and per-CLI adapter session files. It can also run CDP injection into a managed Chrome subprocess. See [docs/quickstart.md](docs/quickstart.md) for the full macOS-to-macOS walkthrough. - ```yaml - live_cdp: - enabled: true - endpoint: http://127.0.0.1:9228 - ``` - -2. **Cookie policy**: Linux defaults to allowlist-empty (ship nothing) when no `blocklist.yaml` exists. You MUST explicitly allow domains: - - ```yaml - version: 1 - policy: allowlist - domains: - - pattern: "github.com" - - pattern: "%.github.com" - - pattern: "%.amazon.com" - ``` - -3. **Ok-line and status**: The sink reports `wrote 0 cookies` for Chrome SQLite on Linux — this is expected. The real injection happens via live CDP. Look for `live_cdp: injected N cookies into M context(s)` in the response and `agentcookie status` output. `agentcookie doctor` checks the live CDP endpoint and probes common debug ports if the configured one is unreachable. - -4. **Daemon setup**: Linux has no LaunchAgents. The wizard prints a systemd user unit you can install: - - ```bash - # Copy the unit file to ~/.config/systemd/user/ - systemctl --user daemon-reload - systemctl --user enable --now agentcookie-sink.service - ``` - - Or run the sink manually / via supervisor: - - ```bash - agentcookie sink - ``` - -5. **Chrome launch**: Start Chrome with the debug port before the sink runs: - - ```bash - google-chrome --remote-debugging-port=9223 - ``` +## What about Chrome's device-bound cookies (DBSC)? -Working: +Chrome's Device Bound Session Credentials (DBSC) tie a session to one machine's secure hardware so a stolen cookie cannot be replayed elsewhere. For a site that has adopted DBSC, a copied cookie works on the sink only until its short-lived window (minutes) lapses. -- Continuous laptop to second-Mac sync via fsnotify on Chrome's Cookies file, debounced, cookie-policy filtered, AES-256-GCM over Tailscale. -- Three cookie delivery surfaces on the sink (Chrome SQLite, plaintext sidecar, per-CLI adapter session files). -- Works with Printing Press CLIs like Stripe, Linear, Notion, Granola, Slack, Kalshi, ElevenLabs, Mercury, and dozens more: anything with a bearer token or API key reads the secrets bus, anything that reads cookies reads the plaintext sidecar. Five PP CLIs (instacart, airbnb, ebay, pagliacci, table-reservation-goat with OpenTable + Tock) additionally get a bespoke zero-config cookie adapter. -- Per-CLI secrets bus: bearer tokens, API keys, and `KEY=VALUE` auth blobs ride the same encrypted push and land at `~/.agentcookie/secrets//secrets.env` (mode 0600) with an optional sealed twin. -- `agentcookie secret list / get / set / rm / revoke / import-from / env` for managing the bus, and `pkg/agentcookiesecret` as an in-process Go reader library. -- v2 adoption standard: drop an `agentcookie.toml` in your repo and `agentcookie discover` auto-detects it. Three integration tiers (explicit-manifest, pp-cli-derived auto-synthesized from `.printing-press.json`, and legacy v1 directories) coexist. -- Tailnet-only listeners on both ends; pair endpoint rate-limited with a 64-bit code. -- Persistent replay defense; per-peer pairing-derived keys. -- Universal cookie delivery: one macOS login-password entry at install (no GUI click) opens the sink's Chrome Safe Storage key to any cookie reader via a partition list (`apple-tool:,apple:,teamid:`), so unmodified cookie tools (yt-dlp, gallery-dl, browser-driving agents, the Printing Press CLIs) read the real synced Default Chrome profile. Verified live on macOS 15.x. -- Apple Developer ID signed binaries; the sink daemon reads Chrome Safe Storage via the `teamid:` partition (no per-binary trust list, no recreate of the key value). -- Headless second-Mac install over SSH: one login-password entry, no GUI SecurityAgent click. A box with no password available installs in degraded mode (sidecar + adapters still work) and prints the one-line upgrade command. -- `agentcookie doctor` runs health checks including cookie delivery (universal vs degraded, with duplicate-keychain-item race detection), binary signature + install, daemon binary path match, Tailscale, config, keystore, listener bind, sink/source state, sealing posture, adapter coverage, CDP injector health, live CDP endpoint (Linux), secrets-bus + secret coverage, and DBSC-suspect cookies. -- 520+ unit tests across 26 packages. +As of August 2026, the one broad adopter is Google's own account and Workspace cookies. The vast majority of sites, and every Printing Press CLI agentcookie feeds, do not use DBSC and sync as before. -Not yet: +For Google sessions: sign the sink's Chrome into the same Google account once. It establishes its own device-bound session locally, no cookie copy required. -- Python reader library at `clients/python/agentcookie_secret` (planned; the Go reader ships today). -- Signature verification on adoption manifests (`signed_by` field reserved; v2.1). -- `[secrets.command]` and `[secrets.keychain]` source kinds (reserved; v2.1). -- `agentcookie pair --rotate` for live key rotation. Today: re-run `wizard install` on both sides. -- One first-Mac, many second-Macs fan-out. -- At-rest sealing of the sidecar + adapter session files is wired in but off by default; turns on via `wizard set-keychain-access --enable-sealing` once consumer-side support lands. +The secrets bus (bearer tokens, API keys, OAuth refresh tokens) is untouched by DBSC and replicates normally. -## What about Chrome's device-bound cookies (DBSC)? +## Status -Chrome's Device Bound Session Credentials (DBSC) tie a session to one machine's secure hardware so a stolen cookie cannot be replayed elsewhere. That is exactly the "move a cookie to another machine" shape agentcookie is built on, so it is worth being precise about what DBSC does and does not change here. +### Working today -DBSC is opt-in per site. A cookie becomes device-bound only when the site's own backend asks for it; nothing binds automatically, and a site binds only the specific session cookies it nominates. As of May 2026 the one broad adopter is Google's own account and Workspace cookies, and that protection went generally available on Chrome for Windows first. macOS support began rolling out gradually in the next Chrome release. The vast majority of sites agentcookie syncs, and every Printing Press CLI it feeds, do not use DBSC, so their cookies replicate to the second Mac and keep working exactly as before. +- Mac to Linux continuous sync via Tailscale `/sync` +- Mac to Mac continuous sync (second Mac, Mac mini) +- Live CDP injection on Linux (cookies go into Chrome's in-memory store) +- Three cookie delivery surfaces on macOS sink (Chrome SQLite, plaintext sidecar, per-CLI adapters) +- Per-CLI secrets bus for bearer tokens and API keys +- 520+ unit tests across 26 packages -For a site that has adopted DBSC, a copied cookie works on the second Mac only until its short-lived window (minutes) lapses, because the second Mac cannot sign the refresh challenge that the source Mac's Secure Enclave holds. agentcookie does not try to defeat that. Instead the source flags cookies that look device-bound and, by default, ships them with a warning you can see in `agentcookie doctor`. Pass `--skip-dbsc-suspect` (or set `AGENTCOOKIE_SKIP_DBSC_SUSPECT=1`) to drop them instead of shipping cookies that will not survive on the sink. +### Honest limits -Two things blunt the impact: +- Linux sink writes 0 cookies to Chrome SQLite (expected; success is live CDP inject) +- Omitted cookie policy on Linux ships nothing (explicit `policy: blocklist` required for sync-all) +- CDP port is loopback-only; same-user processes can attach and read injected cookies +- Sidecar at `~/.agentcookie/cookies-plain.db` is plaintext at rest (not a success metric; verify with live CDP) +- Google/DBSC cookies need local sign-in on the sink; copied cookies expire in minutes +- No live key rotation yet; re-run wizard on both sides to rotate +- Cookie values never appear in logs; do not use `cookies --json` as a verify step -- The secrets bus is untouched. DBSC is a cookie protocol. Bearer tokens, API keys, and OAuth refresh tokens that ride the bus to `~/.agentcookie/secrets//secrets.env` are outside its scope and replicate normally. -- For Google sessions specifically, copying cookies was never the right tool. Sign the second Mac's Chrome into the same Google account once and it establishes its own device-bound session there, no copy required. The agent on the sink reads that local session. +### Not yet -In short: DBSC narrows one corner of the web (today, mostly Google) and agentcookie is honest about it, while the bulk of what it syncs, non-DBSC site cookies and the entire secrets bus, is unaffected. See [docs/threat-model.md](docs/threat-model.md) for the full treatment. +- One source to many sinks fan-out +- Python reader library for the secrets bus +- Signature verification on adoption manifests ## Documentation | Doc | Use | |---|---| -| [Quickstart](docs/quickstart.md) | install on a laptop + second-Mac pair | | [Architecture](docs/architecture.md) | module layout, sync lifecycle, security boundaries | | [Protocol v1](docs/protocol.md) | wire format spec for future client implementations | | [Threat model](docs/threat-model.md) | what agentcookie does and does not protect against | | [FAQ](docs/faq.md) | common questions | -| [Headless quickstart](docs/quickstart-beta.md) | SSH-only install on a headless second Mac | -| [v0.13 one-password keychain runbook](docs/runbook-v0.13-one-password-keychain.md) | universal delivery: the one-password Safe Storage partition open, the duplicate-item race + converge, and the unsigned-CGO boundary | -| [v0.10 keychain runbook](docs/runbook-v0.10-keychain-access.md) | legacy sink Keychain ACL setup (superseded by v0.13 for the grant path) | -| [agent-sync runbook](docs/runbook-agent-sync.md) | log browser-use / agent-browser into your sites via live CDP injection; why cold profiles / storage_state fail | -| [v0.11 adapter runbook](docs/runbook-v0.11-adapter-cookie-push.md) | adapter mechanism + how to write your own | -| [v0.12 security runbook](docs/runbook-v0.12-security-hardening.md) | sealed master key, tailnet-only listeners, rate-limited pairing | -| [v0.12 codesign runbook](docs/runbook-v0.12-codesign.md) | Developer ID signing, notarization, CI secrets, renewal | -| [Secrets bus v1 spec](docs/spec-agentcookie-secrets-bus-v1.md) | wire format and on-disk layout for non-cookie auth | -| [Secrets bus v2 adoption spec](docs/spec-agentcookie-secrets-bus-v2-adoption.md) | `agentcookie.toml` manifest format and discovery rules | -| [Secrets bus adoption runbook](docs/runbook-secrets-bus-adoption.md) | migrating a CLI from imperative `secret import-from` to manifest-driven sync | -| [gh shim worked example](docs/runbook-secrets-bus-gh-example.md) | 50-line bash shim consuming the bus from a non-PP CLI | -| [Install skill](skill/SKILL.md) | Generic `SKILL.md` installer prompt for Claude Code, Codex, Cursor, OpenClaw, Hermes, or any shell-capable agent | +| [Consumption](docs/consumption.md) | how tools read synced cookies and secrets on the sink | +| [agent-sync runbook](docs/runbook-agent-sync.md) | browserUse / agent-browser via live CDP injection | +| [Secrets bus v2 adoption spec](docs/spec-agentcookie-secrets-bus-v2-adoption.md) | `agentcookie.toml` manifest format | +| [Install skill](skill/SKILL.md) | agent-executable installer prompt | ## License diff --git a/docs/architecture.md b/docs/architecture.md index 068bec6..5637142 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,7 +50,11 @@ No Keychain, no Chrome SQLite rewrite, no libsecret. Just live CDP injection. Tailscale required: sink MUST bind a 100.x address (refuses to start without tailnet). -Security: missing policy = allowlist-empty (ship nothing) on Linux. + +Security default (v1.0): missing policy = allowlist-empty (ship nothing) on Linux. +For a single-operator trusted box, the featured setup writes blocklist.yaml with +policy: blocklist and domains: [] to enable sync-all. This is an EXPLICIT operator +choice, not the code default. The code default remains security-by-default. ``` ## Module layout diff --git a/docs/consumption.md b/docs/consumption.md index 87c8731..5fa6ff8 100644 --- a/docs/consumption.md +++ b/docs/consumption.md @@ -98,7 +98,7 @@ emits; the alias above is the operator-set bridge until then. Per-app push adapters (`internal/sinkpush`) remain as a legacy fallback; the read commands above are the supported, generic consumption path. -## Linux consumption (v0.14+) +## Linux consumption (v1.0+) On Linux, agentcookie runs as a continuous `/sync` sink over Tailscale, just like macOS. The key difference: it injects cookies directly into a running @@ -109,42 +109,75 @@ Chrome via CDP instead of writing Chrome's SQLite. 1. **Join the tailnet**: The Linux sink MUST have a Tailscale 100.x address. Verify with `tailscale status`. The sink will refuse to start without it. -2. **Pair with the source Mac**: +2. **Write sink.yaml and blocklist.yaml**: Do NOT run `wizard install --as sink` + on Linux (it omits the policy file, which means allowlist-empty / ship nothing). + Write the YAML files directly as shown in the README. + +3. **Pair with the source Mac**: ```bash # On Mac (source): - agentcookie pair --as source + agentcookie wizard install --as source --peer + # The wizard prints a pairing code and URL # On Linux (sink): agentcookie pair --as sink --peer \ --pair-url http://:9998/pair --code ``` -3. **Start Chrome with CDP enabled**: +4. **Start Chrome with CDP enabled**: ```bash google-chrome --remote-debugging-port=9223 & ``` -4. **Start the sink** (binds to your tailnet IP): +5. **Start the sink** (binds to your tailnet IP): ```bash agentcookie sink ``` -5. **Cookies sync continuously**. Whenever the source Mac's Chrome cookies +6. **Cookies sync continuously**. Whenever the source Mac's Chrome cookies change, they're pushed to the Linux sink and injected via CDP. Any page Chrome loads (including agent-driven pages via Playwright, Puppeteer, or browser-use) sees the logged-in session. +### Verifying success on Linux + +The sidecar (`~/.agentcookie/cookies-plain.db`) and `agentcookie cookies --domain` +DO work on Linux but are NOT the success metric. The sidecar is plaintext at rest. + +Success is the live CDP inject. Verify with: + +```bash +agentcookie status --json +# Look for LastWriteMode containing "livecdp" + +agentcookie doctor +# Look for "live_cdp: endpoint reachable" OK +# Look for "live_cdp: injected N cookies into M context(s)" in sync output +``` + +The message `wrote 0 cookies` for Chrome SQLite is expected on Linux. + +Do NOT use `agentcookie cookies --json` as a verify step - it reads the plaintext +sidecar, which is not the success path. + ### Linux cookie policy Missing `blocklist.yaml` or an omitted `policy` field defaults to **allowlist- -empty** on Linux (ship nothing). This is security-by-default: an untrusted -Linux sink must explicitly opt domains into sync. +empty** on Linux (ship nothing). This is security-by-default. -Configure which domains sync: +For a single-operator trusted box (like your own Grok Bot VM), write: ```yaml # ~/.config/agentcookie/blocklist.yaml version: 1 +policy: blocklist +domains: [] +``` + +This syncs all cookies. For multi-user or less-trusted sinks, use allowlist mode: + +```yaml +version: 1 policy: allowlist domains: - pattern: "github.com" @@ -166,13 +199,13 @@ agentcookie sink # Grok Bot's browser now syncs continuously with your Mac's sessions ``` -### What's NOT available on Linux +### Secrets bus on Linux + +Per-CLI secrets sync to `~/.agentcookie/secrets//` and work on Linux the +same as macOS. Use `agentcookie secret env ` to emit shell-assignable lines. -- Plaintext sidecar (`~/.agentcookie/cookies-plain.db`) — not written -- Per-CLI push adapters — not triggered (no sidecar to read) -- `agentcookie cookies --domain` — reads the sidecar, which doesn't exist -- File-based import — not the supported path; use Tailscale `/sync` +### Per-CLI adapters on Linux -The Linux sink is designed for agent runtimes where the only consumer is a -browser driven by the agent. For CLIs that need the sidecar or adapters, use -a macOS sink. +Per-CLI push adapters run on Linux when the sidecar is present. However, the +primary consumption path on Linux is the browser via CDP. CLIs that need +specific session files can read the sidecar or secrets bus directly. diff --git a/docs/faq.md b/docs/faq.md index dbd1bdb..2b2c2e2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -16,13 +16,21 @@ Firefox and Safari have different cookie stores entirely; supporting them is rea ## What about Linux sinks? -**Fully supported via Tailscale `/sync` (v0.14+).** The Linux receive path is the same continuous sync as macOS-to-macOS, but skips Chrome SQLite entirely (no Keychain, no libsecret) and instead injects cookies into a running Chrome via CDP. +**Fully supported via Tailscale `/sync` (v1.0+).** The Linux receive path is the same continuous sync as macOS-to-macOS, but skips Chrome SQLite entirely (no Keychain, no libsecret) and instead injects cookies into a running Chrome via CDP. -The sink receives the AES-GCM-sealed envelope over Tailscale, decrypts with the pairing-derived key, filters by the sink's allowlist, and injects via CDP's `Storage.setCookies` into every browser context. The target Chrome must be running with `--remote-debugging-port` (default 9223). This is the Grok Bot / agent-runtime path: the Linux box wakes up logged into source-allowlisted sites without a second login. +The sink receives the AES-GCM-sealed envelope over Tailscale, decrypts with the pairing-derived key, filters by the sink's policy, and injects via CDP's `Storage.setCookies` into every browser context. The target Chrome must be running with `--remote-debugging-port` (default 9223). This is the Grok Bot / agent-runtime path: the Linux box wakes up logged into your sites without a second login. -**Tailscale required**: The Linux sink MUST bind a Tailscale 100.x address. It will refuse to start without a working tailnet connection. Run `tailscale status` to verify connectivity. Plaintext cookie JSON file transfers are NOT the supported Linux path — the tailnet (pairing + AES-GCM envelope + 100.x bind) is the trust boundary. +**Tailscale required**: The Linux sink MUST bind a Tailscale 100.x address. It will refuse to start without a working tailnet connection. Run `tailscale status` to verify connectivity. Plaintext cookie JSON file transfers are NOT the supported Linux path - the tailnet (pairing + AES-GCM envelope + 100.x bind) is the trust boundary. -**Security**: Missing policy on Linux defaults to allowlist-empty (ship nothing). You must explicitly configure which domains sync via `blocklist.yaml` with `policy: allowlist` and the domains you want. +**Security**: Missing policy on Linux defaults to allowlist-empty (ship nothing). For a single-operator trusted box (like your own Grok Bot VM), write `blocklist.yaml` with `policy: blocklist` and `domains: []` to sync all cookies: + +```yaml +version: 1 +policy: blocklist +domains: [] +``` + +The 1.0 release does NOT change this default in code. A later release may flip the default, which would be a breaking change. ## Will syncing cookies log me out of sites on the source machine? @@ -79,11 +87,13 @@ After pairing once, delete the `security.shared_secret` field from both YAMLs. T ## Can I run this in a Docker container on a cloud VM? -The sink can run anywhere Chrome stable runs (when CDP is enabled and Chrome is reachable) OR anywhere you can mount the destination's Chrome Cookies SQLite (when only the SQLite path is used). On Linux that means the v0.2 Linux sink work needs to land first. On macOS-in-the-cloud (e.g. MacStadium), it works today as long as you can SSH in and the tailnet reaches the host. +Yes. The sink can run anywhere Chrome stable runs and the tailnet reaches. On Linux, the sink injects cookies via CDP into a running Chrome - no SQLite write, no Keychain. Start Chrome with `--remote-debugging-port=9223` and configure `live_cdp.endpoint` in sink.yaml. The sink must bind a Tailscale 100.x address; run `tailscale status` to verify connectivity. + +On macOS-in-the-cloud (e.g. MacStadium), the full macOS sink path works as long as you can SSH in and the tailnet reaches the host. ## Is this open source? -Apache 2.0. PRs welcome. See the repo at https://github.com/mvanhorn/agentcookie. +MIT. PRs welcome. See the repo at https://github.com/mvanhorn/agentcookie. ## How do I report a security issue? diff --git a/docs/plans/2026-08-13-1720-feat-readme-howto-release-plan.md b/docs/plans/2026-08-13-1720-feat-readme-howto-release-plan.md new file mode 100644 index 0000000..35310ff --- /dev/null +++ b/docs/plans/2026-08-13-1720-feat-readme-howto-release-plan.md @@ -0,0 +1,106 @@ +--- +title: "feat: README how-to + 1.0 release pipeline" +status: active +type: feat +created: 2026-08-13 +--- + +# feat: README how-to + 1.0 release pipeline + +## Goal + +Ship a README how-to that makes Mac to Grok Bot Linux cookie sync the featured path, lockstep satellite docs/skill/release notes, and a linux+darwin binary pipeline so a later human-approved `v1.0.0` can attach the right archives. Celebrate live CDP over Tailscale. + +Stop when U1-U5 are on a PR against main. Do not merge. Do not change sink inject, policy defaults, or pairing crypto. + +## Authority + +This prompt is the execution brief. Investigate the repo and reach your own conclusions. Session-settled product decisions below are structure pins (do not "improve" them). A real defect inside a settled approach should still be surfaced, not silently worked around. + +## Session-settled (do not reverse) + +- Featured path is Grok Bot Linux sync, not Mac-mini-first. +- Formal release target is GitHub Release `v1.0.0` with binaries (this PR prepares it; does not cut it). +- Transport stays Tailscale `/sync` only. No export/import file path in the happy path. +- Happy-path cookie set is sync-all via explicit YAML override, not Amazon-only. +- Live CDP attach on Linux. Do not write Chrome cookie SQLite as success. +- Do not change `PolicyModeForSink` or any sink inject code. + +## Hard denylist (must not change) + +`internal/config/allowlist.go`, `internal/protocol/allowlist.go`, `internal/cli/sink.go`, `internal/livecdp/*`. +No cookie values in any file. No CDP on the tailnet. Do not start a second Chrome / agent-sync `:9400`. Do not flip `RELEASE_CI_ENABLED`. No Homebrew. No `web/` marketing lockstep. + +## Current binary facts you must document (do not "fix" in code) + +1. Linux omitted `policy:` is allowlist-empty (ships nothing). Featured how-to writes `~/.config/agentcookie/blocklist.yaml` with `version: 1`, `policy: blocklist`, `domains: []` for a single-operator Grok Bot / trusted box. Other Linux sinks keep the default. 1.0 notes must NOT claim "Linux defaults to sync-all". A later PolicyModeForSink flip is a breaking follow-up. +2. Wizard on Linux is forbidden in the featured path. `wizard install --as sink` omits `policy:` (ships nothing) and can write `cdp.enabled: true`, which makes `applyLinuxSinkDefaults` skip live CDP and can spawn a second Chrome that fights Grok Bot. Document the YAML, not "run wizard and done". +3. Frozen Linux YAML: + - `sink.yaml`: `listen.addr` on 100.x:9999, `peer.hostname` = Mac Tailscale name, `live_cdp.enabled: true`, `live_cdp.endpoint` default `http://127.0.0.1:9223`, `cdp.enabled: false` (or omit `cdp.enabled`; never leave wizard `cdp.enabled: true`), `skip_chrome_sqlite: true`. Policy does NOT live in sink.yaml (KnownFields rejects it). + - `blocklist.yaml`: `version: 1`, `policy: blocklist`, `domains: []`. +4. Attach to the already-running box Chrome. Never `cdp.managed` / LaunchOwnedChrome / `:9400`. +5. Doctor can print `sync-all` while `/sync` drops everything. Verify with ok-line `live_cdp: injected N cookies into M context(s)` and `LastWriteMode` containing `livecdp`, not the policy label. Linux `wrote 0 cookies` is expected. Sidecar is not success. +6. Default CDP port 9223; doctor also probes 9222/9224/9228/9229/9400. How-to must say what to do when Chrome is on 9228. +7. Pairing: Mac `wizard install --as source --peer `; user relays the pairing code (10-minute, not a cookie); Linux `pair` with `--code` and `--pair-url`. Cookie values must never appear. +8. Keep the sink alive: copy the wizard-printed systemd user unit (do not auto-install). Fresh browserUse only works while the sink is still polling. + +## Units (do all of these) + +### U1 README.md + +Re-lead with Mac to Grok Bot Linux forever-sync. Demote second-Mac / Mac mini below the fold (still supported, not first). +Reading order from `docs/plans/2026-05-22-001-feat-marketing-readme-rewrite-plan.md`: outcome, proof, why, how, install, honest Status, docs table, MIT. +Featured how-to is agent-executable: numbered CLI primitives, `doctor --json` / `status --json`. +Honest Status: Linux SQLite write is 0; look for live_cdp inject; omitted policy ships nothing; Google/DBSC local sign-in; CDP loopback only; same-user processes can attach to the debug port; sidecar `~/.agentcookie/cookies-plain.db` is plaintext and is not success; never `cookies --json`. Linux doctor table: expected FAIL/WARN (codesign, Chrome.app, launchctl) vs ignore vs must-fix (Live CDP endpoint, tailnet bind). +Install: GitHub Release binaries first; verify against `checksums.txt`; `go install ...@v1.0.0` after the tag. No Homebrew. Do not point at `docs/quickstart.md` as the 1.0 how-to (stale). +Freeze download names: `agentcookie_1.0.0_linux_amd64.tar.gz`, `agentcookie_1.0.0_linux_arm64.tar.gz`, plus one darwin-arm64 tarball matching the frozen scheme. + +### U2 satellites + +Files: docs/faq.md, docs/consumption.md, docs/architecture.md, docs/threat-model.md, examples/sink.yaml, CHANGELOG.md + +- FAQ Linux Q matches featured YAML. Fix Apache 2.0 vs MIT. Fix Docker paragraph that still says Linux sink has not landed. +- consumption.md: keep Grok Bot example; sidecar/adapters/cookies DO run on Linux; sidecar is plaintext at rest; never `cookies --json` as verify; success is live inject. +- architecture.md + threat-model.md: keep allowlist-empty as code default / 1.0 security-by-default; featured Grok Bot path is the explicit `policy: blocklist` override. +- examples/sink.yaml: commented `live_cdp` attach at 9223, not `cdp.managed` LaunchChrome. Must not be copy-pasteable into a second Chrome. This file ships in the GoReleaser archive. +- CHANGELOG: add `## [1.0.0] - 2026-08-13` for the hero + Linux binaries. Do not trust `[Unreleased]` as the 1.0 body. + +### U3 skill + +Files: skill/SKILL.md, skill/prompts/install-on-both-machines.md + +Remove "Linux sink support (planned for v0.3)" and Mac-mini-SSH-as-only-sink assumptions. +Two-agent playbook: Mac source wizard; Grok Bot does NOT run `wizard install --as sink`; writes featured YAML and pairs with flags + user-relayed code. +Verify with doctor/status JSON + LastWriteMode / live CDP counts. Never cookies --json. + +### U4 notes + +Files: .github/RELEASE_NOTES_TEMPLATE.md, CHANGELOG.md + +Replace closed-beta / macOS-only template. Name real assets, Tailscale-only, live CDP attach, linux sqlite-0 expected, doctor as support, DBSC/Google local sign-in, cookie values never logged. +Draft the handwritten 1.0 header (highlights, install, breaking/honest limits, security). Do not paste the full PR list into the template (GitHub-native notes from v0.17.1 are appended at cut time). +No claim that Linux defaults to sync-all. + +### U5 packaging + +Files: .goreleaser.yaml, .github/workflows/release.yml, scripts/release-tarball.sh, scripts/install-beta.sh + +Linux archives need CGO_ENABLED=1 (go-sqlite3 has no build tags). Build on Ubuntu: amd64 with default gcc, arm64 with CC=aarch64-linux-gnu-gcc. Do NOT add linux to the current darwin GoReleaser job (CGO + sign.sh on macos-latest will fail). Isolate any linux GoReleaser id so macos-latest never builds it. Keep darwin Path A + codesign unchanged. +Fix release.yml to `go-version-file: go.mod`. Do not flip RELEASE_CI_ENABLED. +Change release-tarball.sh to emit `agentcookie_1.0.0_darwin_arm64.tar.gz` (underscores, no leading v) and update install-beta.sh to `--pattern "*darwin_arm64.tar.gz"` so there is one scheme. +One publisher. No post-publish --clobber. checksums.txt covers every attached archive. + +## Verification (required) + +- `go build ./...`, `go vet ./...`, `go test ./...` +- `goreleaser check` after U5 +- Grep-clean across README, docs/, skill/, .github/RELEASE_NOTES_TEMPLATE.md, examples/ for: "macOS only on both ends", "Linux ... on the roadmap", "v0.2 Linux sink work needs to land", "Closed-beta", "invitation only", "Linux sink support (planned for v0.3)" +- No cookie values in those files +- Relative README links resolve +- Do not change the denylisted sink files + +## Done + +PR against main with U1-U5. Title/body should say this prepares v1.0.0 and does not cut the tag. Report the PR URL, changed files, grep results, and test/goreleaser results. + +Hypothesis only (verify, discard if wrong): examples/sink.yaml currently has cdp.enabled/managed that would skip live CDP if copied; U2 must rewrite that, not only add a comment. diff --git a/docs/threat-model.md b/docs/threat-model.md index 1fe0884..346b0bf 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -56,21 +56,21 @@ agentcookie trusts: - v0.12 (this release) closes every Critical and High finding from the v0.11 threat survey except S5 (plaintext sidecar at rest), which stays open in the default install because turning sealing on requires the PP CLI consumer-side (U12) to ship in cli-printing-press first. Operators who only run agentcookie-controlled binaries on the sink can pass `wizard set-keychain-access --enable-sealing` to opt in; the on-disk sidecar and adapter session files become sealed and S5 closes for them. - v0.13 (planned) will migrate the paired key keystore at `~/.config/agentcookie/keys/.json` into the macOS Keychain, closing the last on-disk plaintext credential. -- v0.14 adds Linux sink support via Tailscale `/sync` with live CDP injection. Key security differences on Linux: - - **Tailscale required**: The Linux sink MUST bind a Tailscale 100.x address. It refuses to start without a working tailnet connection. This is the trust boundary — pairing-derived keys, AES-GCM-sealed envelopes, and 100.x bind policy. Plaintext cookie JSON file transfers are not a supported path. +- v1.0 adds Linux sink support via Tailscale `/sync` with live CDP injection. Key security differences on Linux: + - **Tailscale required**: The Linux sink MUST bind a Tailscale 100.x address. It refuses to start without a working tailnet connection. This is the trust boundary - pairing-derived keys, AES-GCM-sealed envelopes, and 100.x bind policy. Plaintext cookie JSON file transfers are not a supported path. - **No Keychain**: Linux has no macOS Keychain; the sink never touches Chrome's encryption layer. - - **Allowlist-empty default**: Missing `blocklist.yaml` or missing `policy` field defaults to an empty allowlist (ship nothing) on Linux, not the macOS blocklist default (sync-all). This is security-by-default for untrusted sinks. + - **Allowlist-empty default**: Missing `blocklist.yaml` or missing `policy` field defaults to an empty allowlist (ship nothing) on Linux, not the macOS blocklist default (sync-all). This is security-by-default for untrusted sinks. For a single-operator trusted box (like your own Grok Bot VM), the featured setup writes `blocklist.yaml` with `policy: blocklist` and `domains: []` to enable sync-all - this is an explicit operator choice, not the code default. - **CDP-only injection**: Cookies go straight into Chrome's in-memory store via CDP; there's no on-disk Chrome SQLite write on Linux. - **Threat surface**: Same-user processes can connect to Chrome's CDP port while it's open. The CDP port is loopback-only (`127.0.0.1:9223`), never on the tailnet. Treat a Linux agent runtime as a same-user trust boundary. ## Linux sink specifics -The Linux receive path (v0.14+) operates under a stricter trust model than the macOS sink: +The Linux receive path (v1.0+) operates under a stricter trust model than the macOS sink: - **Tailscale-only transport**: The Linux sink receives cookies via the same `/sync` endpoint as macOS sinks, over the Tailscale tailnet. The sink MUST bind a 100.x tailnet IP; a missing tailnet is a hard fail, not a fallback. The trust boundary is: pairing-derived 32-byte keys, AES-256-GCM-sealed envelopes, replay defense via persistent sequence tracking, and 100.x-only bind policy. - **Source stays macOS**: Linux never decrypts Chrome Safe Storage or reads the macOS Keychain. Cookie values arrive encrypted in the sync envelope and are decrypted by the Linux sink using the pairing-derived key. - **No Chrome SQLite rewrite**: agentcookie does not call libsecret or write Chrome's Cookies file on Linux. All injection is live CDP. -- **Cookie policy**: Missing policy defaults to allowlist-empty. The operator must explicitly opt domains into sync via `blocklist.yaml` with `policy: allowlist`. +- **Cookie policy**: Missing policy defaults to allowlist-empty. For a single-operator trusted box, the featured setup writes `blocklist.yaml` with `policy: blocklist` and `domains: []` to enable sync-all. For multi-user or less-trusted sinks, use `policy: allowlist` with specific domains. - **CDP surface**: The sink attaches to an already-running Chrome's CDP endpoint (default `http://127.0.0.1:9223`). Chrome must have been started with `--remote-debugging-port=9223`. The CDP port is loopback-only; it is never exposed on the tailnet. While the endpoint is open locally, any same-user process can connect to it. - **Replay defense**: Same as macOS — persistent sequence tracking in `~/.agentcookie/sequence.json`. - **Doctor/wizard**: On Linux, `agentcookie doctor` does not check Keychain, LaunchAgents, or codesign — those are macOS-specific. It checks CDP connectivity and tailnet bind instead. diff --git a/examples/sink.yaml b/examples/sink.yaml index 29f5b06..be4e36d 100644 --- a/examples/sink.yaml +++ b/examples/sink.yaml @@ -1,35 +1,53 @@ -# Example sink.yaml. Copy to ~/.config/agentcookie/sink.yaml on the machine -# where your AI agents run (your Mac mini, cloud VM, etc.). +# Example sink.yaml for Linux sinks (Grok Bot, cloud VM, etc.) +# Copy to ~/.config/agentcookie/sink.yaml + +# IMPORTANT: Also create blocklist.yaml for the cookie policy. +# On Linux, omitted policy = allowlist-empty (ship nothing). +# See examples/blocklist.yaml for the sync-all configuration. listen: - # Address the sink's /sync HTTP listener binds to. Must be your - # tailnet 100.x address, or "127.0.0.1" for local-dev testing. - # v0.12 hardening (S1): the sink refuses to start on 0.0.0.0 or - # any non-tailnet routable address. `agentcookie wizard install - # --as sink` writes the resolved 100.x address automatically. + # Address the sink's /sync HTTP listener binds to. MUST be your + # Tailscale 100.x address. The sink refuses to start on 0.0.0.0 or + # any non-tailnet routable address. + # + # Find your Tailscale IP: tailscale ip -4 addr: 100.x.y.z:9999 -cdp: - # The default v0.2 install path. The sink launches its own Chrome subprocess - # with --remote-debugging-port=AUTO and an isolated --user-data-dir. Cookies - # land via Chrome DevTools Protocol Storage.setCookies, so the sink never - # calls macOS Keychain. No interactive prompts. No screen-sharing needed. - enabled: true - managed: true - # profile_dir defaults to ~/.agentcookie/chrome-profile when omitted. - # chrome_binary defaults to /Applications/Google Chrome.app/Contents/MacOS/Google Chrome. +peer: + # Hostname of the source machine (your Mac). After pairing, this + # matches the filename under ~/.config/agentcookie/keys/. + # + # Find your Mac's hostname: tailscale status + hostname: your-mac.tailnet.ts.net -# chrome: -# # Only set db_path when running in legacy SQLite mode (managed: false AND -# # cdp.enabled: false). The managed CDP path does not touch SQLite. -# db_path: ~/Library/Application Support/Google/Chrome/Default/Cookies +# Live CDP injection: attach to an ALREADY-RUNNING Chrome and inject +# cookies via Chrome DevTools Protocol. This is the featured Linux path. +# +# Start Chrome with: google-chrome --remote-debugging-port=9223 +# +# Do NOT set cdp.enabled or cdp.managed - those launch a SECOND Chrome +# subprocess which conflicts with browserUse/Puppeteer/Playwright +# connecting to the existing Chrome. +live_cdp: + enabled: true + # Default endpoint. Change if Chrome runs on a different port. + # Common alternatives: 9222, 9224, 9228, 9229 + endpoint: http://127.0.0.1:9223 -peer: - # Hostname of the source machine. After `agentcookie pair --as sink ...` - # finishes, this is the filename under ~/.config/agentcookie/keys/. - hostname: my-laptop.tailnet.ts.net +# Skip Chrome SQLite write. On Linux there's no Keychain and no way +# to write Chrome's encrypted cookie database. All injection is via CDP. +skip_chrome_sqlite: true -# Legacy security.shared_secret still accepted as a fallback for users who -# have not paired yet. After pairing, delete the field below. -# security: -# shared_secret: only-set-this-before-you-pair +# --- macOS-only options (do not use on Linux) --- +# +# cdp: +# # Launches a managed Chrome subprocess with its own profile. +# # Use only on macOS when you want CDP injection into an +# # agentcookie-owned Chrome, not your everyday browser. +# enabled: true +# managed: true +# # profile_dir: ~/.agentcookie/chrome-profile +# +# chrome: +# # Only set db_path for legacy SQLite mode on macOS. +# # db_path: ~/Library/Application Support/Google/Chrome/Default/Cookies diff --git a/scripts/install-beta.sh b/scripts/install-beta.sh index 5e580cb..a48088a 100755 --- a/scripts/install-beta.sh +++ b/scripts/install-beta.sh @@ -146,9 +146,9 @@ if [[ -z "$TARBALL" ]]; then if ! gh auth status >/dev/null 2>&1; then die "gh is not authenticated. Run 'gh auth login' first." fi - step "downloading latest beta release from $REPO" + step "downloading latest release from $REPO" TMP_DL="$(mktemp -d -t agentcookie-beta.XXXXXX)" - gh release download --repo "$REPO" --pattern '*darwin-arm64.tar.gz' --dir "$TMP_DL" --clobber + gh release download --repo "$REPO" --pattern '*darwin_arm64.tar.gz' --dir "$TMP_DL" --clobber TARBALL="$(ls -1 "$TMP_DL"/*.tar.gz | head -n1)" if [[ -z "$TARBALL" || ! -f "$TARBALL" ]]; then die "release tarball not found after download (looked in $TMP_DL)" diff --git a/scripts/release-tarball.sh b/scripts/release-tarball.sh index 01648d4..0d7e40b 100755 --- a/scripts/release-tarball.sh +++ b/scripts/release-tarball.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash # -# release-tarball.sh - Build the closed-beta release tarball that goes -# into a GitHub release. Bundles the notarized agentcookie binary with -# the install-beta.sh script and the closed-beta quickstart guide. The -# install script knows how to consume this exact shape. +# release-tarball.sh - Build the release tarball for darwin-arm64. +# Bundles the notarized agentcookie binary with the install-beta.sh script +# and the quickstart guide. # # Usage: # scripts/release-tarball.sh # -# Where matches the release tag (e.g. v0.12.0-beta.1). The -# script produces: +# Where matches the release tag (e.g. v1.0.0). The script produces: # -# dist/agentcookie--darwin-arm64.tar.gz +# dist/agentcookie__darwin_arm64.tar.gz +# +# Example: v1.0.0 -> dist/agentcookie_1.0.0_darwin_arm64.tar.gz # # Prereqs: # 1. bin/agentcookie exists, signed and notarized (run `make release` @@ -31,6 +31,9 @@ if [[ $# -lt 1 ]]; then fi VERSION="$1" +# Strip leading 'v' from version for archive naming +VERSION_NUM="${VERSION#v}" + REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" @@ -56,12 +59,13 @@ fi ARCH="$(uname -m)" if [[ "$ARCH" == "arm64" ]]; then - TARBALL_ARCH="darwin-arm64" + TARBALL_ARCH="darwin_arm64" else - TARBALL_ARCH="darwin-$ARCH" + TARBALL_ARCH="darwin_$ARCH" fi -OUT_NAME="agentcookie-${VERSION}-${TARBALL_ARCH}" +# Use underscore naming: agentcookie_1.0.0_darwin_arm64 +OUT_NAME="agentcookie_${VERSION_NUM}_${TARBALL_ARCH}" DIST_DIR="dist" mkdir -p "$DIST_DIR" STAGE="$(mktemp -d -t agentcookie-release.XXXXXX)/$OUT_NAME" diff --git a/skill/SKILL.md b/skill/SKILL.md index 7f2a8f2..e54f9f3 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,134 +1,222 @@ --- name: agentcookie-install -description: Install agentcookie on the user's source (laptop) and sink (Mac mini / cloud VM / second Mac) machines and pair them so Chrome cookies sync continuously over their Tailscale tailnet. Use when the user says "install agentcookie", "set up cookie sync", "share my Chrome sessions with my Mac mini", or "make my agent log in as me". -version: 0.2.0 +description: Install agentcookie on a Mac source and a Linux or Mac sink so Chrome cookies sync continuously over Tailscale. Use when the user says "install agentcookie", "set up cookie sync", "share my Chrome sessions with my agent box", or "make my agent log in as me". +version: 1.0.0 --- # agentcookie install You are helping the user install agentcookie on two machines that are both on the same Tailscale tailnet, then pair them, so that the sink's Chrome stays continuously in sync with the source's Chrome. -After install, the user does not touch agentcookie again. A LaunchAgent on each side keeps the daemons running across reboots. The source watches Chrome via fsnotify and pushes every cookie change to the sink within seconds. The sink writes via Chrome DevTools Protocol into a dedicated managed Chrome subprocess. No Keychain prompt fires on the sink. No screen-sharing required. - -The user expects one prompt ("install agentcookie on my laptop and my Mac mini") to be enough. Make that real. +After install, the user does not touch agentcookie again. The source watches Chrome via fsnotify and pushes every cookie change to the sink within seconds. On Linux, the sink injects cookies via CDP into an already-running Chrome. On macOS, a LaunchAgent keeps the daemon running across reboots. ## Inputs you need -1. Which machine is the **source** (the machine the user logs into Chrome on, usually their laptop). -2. Which machine is the **sink** (the machine where AI agents act, usually a Mac mini or cloud VM). -3. Tailscale is up on both, and the user can SSH from source to sink without a password prompt. +1. Which machine is the **source** (the Mac the user logs into Chrome on, usually their laptop). +2. Which machine is the **sink** (where AI agents act - a Linux VM like Grok Bot, or a second Mac). +3. Tailscale is up on both. +4. On Linux: Chrome is running with `--remote-debugging-port=9223` (or another port). If any of these are missing, stop and ask. -## Flow +## Flow: Mac source to Linux sink (featured path) ### Step 0: detect the lay of the land Run on the current machine: ```bash -which agentcookie 2>/dev/null || echo "missing" -/Applications/Tailscale.app/Contents/MacOS/Tailscale status 2>&1 | head -20 -ssh -o ConnectTimeout=5 -o BatchMode=yes 'whoami' 2>&1 +uname -s # Darwin = macOS, Linux = Linux +tailscale status 2>&1 | head -20 ``` -From the Tailscale status output, the current machine is the entry marked "active" or appears at the top. Every other macOS entry is a candidate sink. +From the Tailscale status output, identify which host is the Mac and which is the Linux box. ### Step 1: confirm source vs sink with the user Use the platform's blocking question primitive. Phrase it concretely: -> I see you're on ``. Looks like `` (Tailscale IP `100.x.y.z`) is your other Mac. Should I install agentcookie with `` as the source (your logged-in Chrome) and `` as the sink (where your agents run)? +> I see you're on ``. Looks like `` (Tailscale IP `100.x.y.z`) is your other machine. Should I install agentcookie with `` as the source (your logged-in Chrome) and `` as the sink (where your agents run)? Confirm before proceeding. If wrong, ask which is which. -### Step 2: install on the source +### Step 2: install on the Mac source Install the binary if missing: ```bash -go install github.com/mvanhorn/agentcookie/cmd/agentcookie@latest +# Download from GitHub Releases +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_darwin_arm64.tar.gz +tar -xzf agentcookie_1.0.0_darwin_arm64.tar.gz +sudo mv agentcookie /usr/local/bin/ +``` + +Or build from source: + +```bash +go install github.com/mvanhorn/agentcookie/cmd/agentcookie@v1.0.0 ``` -Run the source-side wizard. It blocks until pairing completes: +Run the source wizard. It blocks until pairing completes: ```bash -agentcookie wizard install --as source --peer --local-name & +agentcookie wizard install --as source --peer & WIZARD_PID=$! ``` Run in the background because we need to poll the pairing info file: ```bash -# Wait up to 10 seconds for the pairing info to appear. -for i in {1..40}; do +# Wait up to 30 seconds for the pairing info to appear. +for i in {1..120}; do if [ -f ~/.agentcookie/pairing.json ]; then break; fi sleep 0.25 done cat ~/.agentcookie/pairing.json ``` -Extract `code` and `pair_url` from the JSON output. These are what the sink needs. +Extract `code` and `pair_url` from the JSON output. These are what the sink needs. The code expires in 10 minutes. + +### Step 3: install on the Linux sink -### Step 3: install on the sink +Do NOT run `wizard install --as sink` on Linux. The wizard omits the policy file (which means allowlist-empty / ship nothing) and can write `cdp.enabled: true` (which launches a second Chrome that fights your existing Chrome). -SSH to the sink and run its wizard. You can pass everything on one line: +Instead, write the config files directly: ```bash -ssh "go install github.com/mvanhorn/agentcookie/cmd/agentcookie@latest && \ - agentcookie wizard install --as sink \ - --peer \ - --code \ - --pair-url \ - --local-name " +# Install the binary +curl -LO https://github.com/mvanhorn/agentcookie/releases/download/v1.0.0/agentcookie_1.0.0_linux_amd64.tar.gz +tar -xzf agentcookie_1.0.0_linux_amd64.tar.gz +sudo mv agentcookie /usr/local/bin/ + +# Create config directory +mkdir -p ~/.config/agentcookie + +# Get the Tailscale IP +TAILSCALE_IP=$(tailscale ip -4) + +# Write sink.yaml +cat > ~/.config/agentcookie/sink.yaml << EOF +listen: + addr: ${TAILSCALE_IP}:9999 + +peer: + hostname: # REPLACE with Mac's Tailscale hostname + +live_cdp: + enabled: true + endpoint: http://127.0.0.1:9223 + +skip_chrome_sqlite: true +EOF + +# Write blocklist.yaml for sync-all on a trusted box +cat > ~/.config/agentcookie/blocklist.yaml << 'EOF' +version: 1 +policy: blocklist +domains: [] +EOF + +# Pair with the Mac source +agentcookie pair --as sink \ + --peer \ + --code \ + --pair-url ``` -The sink wizard: -1. Drops `~/.config/agentcookie/{sink.yaml, allowlist.yaml}` with `cdp.managed: true` (zero Keychain involvement). -2. Runs the X25519 + HKDF pairing handshake against the source's pair URL. -3. Installs the sink LaunchAgent. -4. Starts the sink, which spawns a dedicated Chrome subprocess for the agent's cookie target. +### Step 4: start Chrome with CDP enabled -### Step 4: confirm both daemons are up +```bash +google-chrome --remote-debugging-port=9223 & +# Or headless: google-chrome --remote-debugging-port=9223 --headless=new & +``` + +If Chrome runs on a different port (e.g., 9228), update `live_cdp.endpoint` in sink.yaml. + +### Step 5: start the sink ```bash -launchctl list | grep dev.agentcookie -ssh 'launchctl list | grep dev.agentcookie' +agentcookie sink ``` -Each should show `dev.agentcookie.source` (laptop) and `dev.agentcookie.sink` (Mac mini) with a PID. +For a persistent daemon, write a systemd user unit: -### Step 5: verify a real sync round-trip +```bash +mkdir -p ~/.config/systemd/user/ +cat > ~/.config/systemd/user/agentcookie-sink.service << 'EOF' +[Unit] +Description=agentcookie sink +After=network.target + +[Service] +ExecStart=/usr/local/bin/agentcookie sink +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target +EOF + +systemctl --user daemon-reload +systemctl --user enable --now agentcookie-sink.service +``` + +### Step 6: verify the install ```bash +# On Mac +agentcookie doctor +agentcookie status --json + +# On Linux +agentcookie doctor agentcookie status --json -ssh 'agentcookie status --json' ``` -The source should report a recent push timestamp. The sink should report a recent write count. If either is empty, log into a github.com tab on the source's Chrome to force a cookie write and re-check. +On Linux, look for: +- `live_cdp: endpoint reachable` - must be OK +- `tailnet: bind address` - must be OK +- `LastWriteMode` containing `livecdp` in status output +- `live_cdp: injected N cookies into M context(s)` in sync output -### Step 6: report to the user +Ignore expected FAILs on Linux: codesign, Chrome.app path, launchctl (these are macOS-specific). + +The message `wrote 0 cookies` for Chrome SQLite is expected on Linux. Success is the live CDP inject line. + +### Step 7: report to the user In plain language. Example: -> Done. agentcookie is running on both `` and ``. The source pushes cookies as soon as they change in Chrome on ``; the sink writes them into a dedicated Chrome instance at `~/.agentcookie/chrome-profile` on ``. Your agents on `` connect to that Chrome via CDP at `~/.agentcookie/chrome-profile`. After this install, the user does not run agentcookie commands by hand again. +> Done. agentcookie is running on both `` and ``. The source pushes cookies as soon as they change in Chrome. The sink injects them via CDP into Chrome at port 9223. browserUse, Puppeteer, Playwright, or any Chromium automation connecting to that port will see your logged-in session. + +## Flow: Mac source to Mac sink + +For Mac-to-Mac, the wizard works: + +```bash +# On the second Mac +agentcookie wizard install --as sink \ + --peer \ + --code \ + --pair-url http://:9998/pair +``` + +The macOS sink writes to Chrome's encrypted SQLite, the plaintext sidecar, and per-CLI adapter session files. ## What to do if something errors -**`agentcookie: command not found` on the sink after `go install`.** The sink's `$PATH` lacks `~/go/bin`. Tell the user (or fix by sourcing `~/.zshrc` on the SSH command, or invoke the binary by absolute path: `~/go/bin/agentcookie`). +**`agentcookie: command not found`**: The binary is not on `$PATH`. Either use the full path (`/usr/local/bin/agentcookie`) or add the bin directory to PATH. -**Sink pairing returns `connection refused`.** Tailscale ACLs may be blocking tailnet-internal traffic on port 9998. Check `tailscale status` shows the source as reachable. If the source is online but unreachable, the user has restrictive ACLs to relax. +**Sink pairing returns `connection refused`**: Tailscale ACLs may be blocking tailnet-internal traffic on port 9998. Check `tailscale status` shows the source as reachable. If the source is online but unreachable, the user has restrictive ACLs to relax. -**Sink wizard hangs at `Chrome did not publish DevToolsActivePort`.** The managed Chrome subprocess failed to start. Most likely: Google Chrome is not installed at `/Applications/Google Chrome.app`. Install Chrome or set `cdp.chrome_binary` in sink.yaml. +**`live_cdp: endpoint reachable FAIL`**: Chrome is not running with `--remote-debugging-port`. Start Chrome with the debug port, or check that the port in sink.yaml matches the actual port. -**`agentcookie status` reports zero pushes after install.** The source watcher has not seen a Chrome write yet. Open a tab on the source's Chrome (any allowlisted domain) and refresh. Push should appear within 2 seconds. +**`agentcookie status` reports zero syncs**: The source watcher has not seen a Chrome write yet. Open a tab on the source's Chrome (any domain) and refresh. The sync should appear within 2 seconds. -**Sink Chrome subprocess crashes repeatedly.** Check `~/.agentcookie/logs/sink.err.log`. Most common cause: stale lockfile from a prior Chrome session sharing the user-data-dir. Solution: `rm ~/.agentcookie/chrome-profile/SingletonLock` and let the supervisor restart. +**Doctor shows `sync-all` but cookies don't land**: The policy label and actual behavior can diverge. Verify success with `live_cdp: injected N cookies into M context(s)` in the sync output, not the policy label. Also check `LastWriteMode` contains `livecdp`. ## Out of scope for this skill -- Code-signing the binary so Keychain access is granted without a prompt. -- Web Store extension install (planned for v0.3). -- Linux sink support (planned for v0.3). -- Bidirectional sync (planned for v0.3). -- Adding new domains to the allowlist after install (the user edits `~/.config/agentcookie/allowlist.yaml` on each side; LaunchAgents pick up changes on next restart, which they do automatically every 10 seconds after a config save). +- Changing the cookie policy or allowlist/blocklist rules (the user edits `~/.config/agentcookie/blocklist.yaml` directly) +- Rotating pairing keys (re-run wizard on both sides) +- One source to many sinks (not yet supported) diff --git a/skill/prompts/install-on-both-machines.md b/skill/prompts/install-on-both-machines.md index 88d7633..5234bfb 100644 --- a/skill/prompts/install-on-both-machines.md +++ b/skill/prompts/install-on-both-machines.md @@ -2,26 +2,69 @@ Copy this into Claude Code, OpenClaw, Hermes, Codex, Cursor, or any agent that can run shell commands locally and over SSH: -> Install agentcookie on this laptop and my Mac mini so my Chrome sessions sync continuously. Use Tailscale to find the Mac mini. Confirm with me which machine is the source and which is the sink, then run the full install end to end. After install, verify both daemons are running and tell me what you see. +> Install agentcookie on this Mac and my Linux agent box so my Chrome sessions sync continuously. Use Tailscale to find the Linux box. Confirm with me which machine is the source and which is the sink, then run the full install end to end. After install, verify both sides are running and tell me what you see. The agent should: 1. Detect Tailscale and identify the peer machine. 2. Confirm source vs sink with you. -3. Run `agentcookie wizard install --as source` here, in the background. +3. Run `agentcookie wizard install --as source` here on the Mac, in the background. 4. Read the pairing code from `~/.agentcookie/pairing.json` once it appears. -5. SSH to the Mac mini and run `agentcookie wizard install --as sink ...` with that code. -6. Report back that both daemons are up. +5. SSH to the Linux box and: + - Install the agentcookie binary + - Write `sink.yaml` with `live_cdp.enabled: true` and the tailnet IP + - Write `blocklist.yaml` with `policy: blocklist` and `domains: []` + - Run `agentcookie pair --as sink` with the code and URL + - Start Chrome with `--remote-debugging-port=9223` + - Start `agentcookie sink` +6. Report back that both sides are up and show the verify output. -Total elapsed time: about 30 seconds. You do not need to be at the Mac mini's screen. No Keychain prompts will fire on the Mac mini. +Total elapsed time: about 60 seconds. You do not need to be at the Linux box's screen. + +## For Mac-to-Mac instead of Mac-to-Linux + +> Install agentcookie on this Mac and my Mac mini so my Chrome sessions sync continuously. Use Tailscale to find the Mac mini. Confirm which is source and sink, then run the full install end to end. + +The wizard works on macOS sinks: + +```bash +ssh "agentcookie wizard install --as sink \ + --peer \ + --code \ + --pair-url " +``` ## When the prompt is not enough If the agent gets stuck, the most common reasons (in rough probability order): -1. SSH from your laptop to your Mac mini does not work passwordlessly. Fix by setting up SSH keys, or use Tailscale SSH (`tailscale ssh` is the command). -2. Google Chrome is not installed on the Mac mini. The wizard needs it. Install Chrome. -3. Go is not installed on the Mac mini, so `go install` fails. Either install Go (`brew install go`) or transfer a prebuilt binary from the laptop via `scp`. +1. SSH from your Mac to the Linux box does not work passwordlessly. Fix by setting up SSH keys, or use Tailscale SSH (`tailscale ssh`). +2. Google Chrome is not installed on the Linux box. Install Chrome. +3. Chrome is not running with `--remote-debugging-port`. Start it with the debug port. 4. Tailscale ACLs are restrictive. Default Tailscale config allows everything between your own devices; if you have custom ACLs, allow tailnet-internal traffic on ports 9998 (pairing) and 9999 (sync). -After fixing, re-paste the prompt. The wizard is idempotent and will pick up where it left off. +After fixing, re-paste the prompt. The install is mostly idempotent. + +## Verifying success + +On Mac: +```bash +agentcookie doctor +agentcookie status --json +``` + +On Linux: +```bash +agentcookie doctor +agentcookie status --json +``` + +Look for: +- `live_cdp: endpoint reachable` OK +- `tailnet: bind address` OK +- `LastWriteMode` containing `livecdp` +- `live_cdp: injected N cookies into M context(s)` in sync output + +Ignore expected FAILs on Linux: codesign, Chrome.app path, launchctl. + +The message `wrote 0 cookies` is expected on Linux. Success is the live CDP inject line, not the sidecar or SQLite write. From 869ac1435e4843d9079579789b79e0b6b7872917 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 00:46:05 +0000 Subject: [PATCH 2/3] fix: probe existing Chrome before starting, soften lede, split goreleaser configs 1. Featured how-to: ATTACH, not start Chrome - README and skill now probe existing debug ports (9222-9229) first - Only start Chrome as fallback when nothing is listening - Avoids KTD2 failure mode (second Chrome fighting existing one) 2. Soften README lede - 'wakes up logged in' -> 'Cookie-authenticated sites show logged-in UI' - Explicitly note Google/Workspace stay logged out (DBSC) 3. Isolate linux GoReleaser builds from darwin - .goreleaser.yaml: darwin-only with codesign hook - .goreleaser.linux.yaml: linux amd64/arm64 with cross-compiler - release.yml updated to use --config .goreleaser.linux.yaml Verified: - go test ./... passes - goreleaser check on both configs passes - README/skill no longer start Chrome as featured first step Co-authored-by: Matt Van Horn --- .github/workflows/release.yml | 40 ++++++--------------- .goreleaser.linux.yaml | 65 +++++++++++++++++++++++++++++++++++ .goreleaser.yaml | 27 --------------- README.md | 27 ++++++++++++--- skill/SKILL.md | 23 +++++++++++-- 5 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 .goreleaser.linux.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d23846c..0f9c94d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: contents: write jobs: - # Build Linux binaries on Ubuntu (CGO_ENABLED=1 requires native gcc) + # Build Linux binaries on Ubuntu using .goreleaser.linux.yaml build-linux: runs-on: ubuntu-latest if: ${{ vars.RELEASE_CI_ENABLED == 'true' }} @@ -29,41 +29,21 @@ jobs: sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu - # Build Linux amd64 - - name: build linux amd64 - run: | - CGO_ENABLED=1 go build -ldflags "-s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version=${{ github.ref_name }}" -o dist/linux-amd64/agentcookie ./cmd/agentcookie - env: - GOOS: linux - GOARCH: amd64 - - # Build Linux arm64 - - name: build linux arm64 - run: | - CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -ldflags "-s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version=${{ github.ref_name }}" -o dist/linux-arm64/agentcookie ./cmd/agentcookie + # Use goreleaser with the linux-specific config + - name: goreleaser linux + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean --config .goreleaser.linux.yaml --skip=publish env: - GOOS: linux - GOARCH: arm64 - - # Create tarballs with the frozen naming scheme - - name: create linux tarballs - run: | - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" # Strip leading v - - mkdir -p release - - # Linux amd64 - tar -czvf "release/agentcookie_${VERSION}_linux_amd64.tar.gz" -C dist/linux-amd64 agentcookie - - # Linux arm64 - tar -czvf "release/agentcookie_${VERSION}_linux_arm64.tar.gz" -C dist/linux-arm64 agentcookie + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: upload linux artifacts uses: actions/upload-artifact@v4 with: name: linux-binaries - path: release/*.tar.gz + path: dist/*.tar.gz # Build darwin binaries on macOS (codesigning requires macOS) build-darwin: diff --git a/.goreleaser.linux.yaml b/.goreleaser.linux.yaml new file mode 100644 index 0000000..52e1fda --- /dev/null +++ b/.goreleaser.linux.yaml @@ -0,0 +1,65 @@ +version: 2 + +project_name: agentcookie + +before: + hooks: + - go mod tidy + +builds: + # Linux amd64 build: runs on ubuntu-latest with default gcc + - id: agentcookie-linux-amd64 + main: ./cmd/agentcookie + binary: agentcookie + env: + - CGO_ENABLED=1 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} + + # Linux arm64 build: runs on ubuntu-latest with cross-compiler + - id: agentcookie-linux-arm64 + main: ./cmd/agentcookie + binary: agentcookie + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + goos: + - linux + goarch: + - arm64 + ldflags: + - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} + +archives: + - id: agentcookie-linux + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + files: + - LICENSE + - README.md + - docs/quickstart.md + - docs/threat-model.md + - docs/architecture.md + - docs/protocol.md + - docs/faq.md + - examples/source.yaml + - examples/sink.yaml + - examples/blocklist.yaml + +checksum: + name_template: checksums.txt + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" + - merge diff --git a/.goreleaser.yaml b/.goreleaser.yaml index de22112..67aa532 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -25,33 +25,6 @@ builds: - cmd: scripts/sign.sh {{ .Path }} output: true - # Linux amd64 build: runs on ubuntu-latest with default gcc - - id: agentcookie-linux-amd64 - main: ./cmd/agentcookie - binary: agentcookie - env: - - CGO_ENABLED=1 - goos: - - linux - goarch: - - amd64 - ldflags: - - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} - - # Linux arm64 build: runs on ubuntu-latest with cross-compiler - - id: agentcookie-linux-arm64 - main: ./cmd/agentcookie - binary: agentcookie - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-gnu-gcc - goos: - - linux - goarch: - - arm64 - ldflags: - - -s -w -X github.com/mvanhorn/agentcookie/internal/cli.Version={{ .Version }} - archives: - id: agentcookie name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" diff --git a/README.md b/README.md index 7f1a04b..bd5726a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Your agent runs on a Linux box (a Grok Bot VM, a cloud agent runtime, a homelab server) and needs to act as you on every site you're already logged into. agentcookie keeps that box's Chrome session in sync with your Mac's, continuously, encrypted over your Tailscale tailnet, with zero per-site auth ceremony. -The Linux Chrome wakes up logged in. browserUse, Puppeteer, Playwright, or any Chromium automation that connects to Chrome's debug port sees your session already there. +Cookie-authenticated sites show the logged-in UI after live CDP inject. Google/Workspace sessions stay logged out unless a human signed in on the box (DBSC binds those sessions to device keys). browserUse, Puppeteer, Playwright, or any Chromium automation that connects to Chrome's debug port sees your non-DBSC sessions already there. ## What it looks like @@ -160,17 +160,36 @@ Replace: - `your-mac.tailnet` with your Mac's Tailscale hostname (`tailscale status` on either machine) - The pairing code and URL with the values printed by the Mac source wizard -### Start Chrome with CDP enabled +### Attach to the existing Chrome (or start one as fallback) + +On Grok Bot and most agent runtimes, Chrome is already running with a debug port. Probe before starting a new one: + +```bash +# Check if Chrome is already listening on common debug ports +for port in 9223 9222 9224 9228 9229; do + if curl -s "http://127.0.0.1:${port}/json/version" >/dev/null 2>&1; then + echo "Chrome found on port ${port}" + # Update sink.yaml to use this port + sed -i "s|endpoint: http://127.0.0.1:.*|endpoint: http://127.0.0.1:${port}|" \ + ~/.config/agentcookie/sink.yaml + break + fi +done +``` + +If no Chrome is listening, start one as a fallback: ```bash -# On Linux, start Chrome with the debug port +# Only if no existing Chrome debug port was found google-chrome --remote-debugging-port=9223 & # Or headless google-chrome --remote-debugging-port=9223 --headless=new & ``` -If Chrome runs on a different port (e.g., 9228), update `live_cdp.endpoint` in sink.yaml. +Starting a second Chrome when one is already running on the same port causes conflicts (the KTD2 failure mode). Always probe first. + +You can also use `agentcookie doctor` which probes ports 9222, 9223, 9224, 9228, 9229, and 9400 and reports which endpoint is reachable. ### Start the sink diff --git a/skill/SKILL.md b/skill/SKILL.md index e54f9f3..a928f2b 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -124,14 +124,31 @@ agentcookie pair --as sink \ --pair-url ``` -### Step 4: start Chrome with CDP enabled +### Step 4: attach to existing Chrome (or start one as fallback) + +On Grok Bot and most agent runtimes, Chrome is already running with a debug port. Probe before starting a new one: + +```bash +# Check if Chrome is already listening on common debug ports +for port in 9223 9222 9224 9228 9229; do + if curl -s "http://127.0.0.1:${port}/json/version" >/dev/null 2>&1; then + echo "Chrome found on port ${port}" + # Update sink.yaml to use this port + sed -i "s|endpoint: http://127.0.0.1:.*|endpoint: http://127.0.0.1:${port}|" \ + ~/.config/agentcookie/sink.yaml + break + fi +done +``` + +If no Chrome is listening, start one as a fallback: ```bash +# Only if no existing Chrome debug port was found google-chrome --remote-debugging-port=9223 & -# Or headless: google-chrome --remote-debugging-port=9223 --headless=new & ``` -If Chrome runs on a different port (e.g., 9228), update `live_cdp.endpoint` in sink.yaml. +Starting a second Chrome when one is already running on the same port causes conflicts. Always probe first. ### Step 5: start the sink From 1cbc5de9ccde3859102b815720bd56655e90fd5f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 00:47:36 +0000 Subject: [PATCH 3/3] feat: add extra Chrome profile discovery to 1.0 release copy Include #115 (auto-discover extra Chrome profiles) and #116 (paste extra-profile cookies through adapters) in the 1.0 release: CHANGELOG [1.0.0]: - Mac extra Chrome profiles auto-discovered and decrypted - Extra-profile cookies flow to sidecar, adapters, and live CDP - Sink adapters union extra-profile cookies through same blocklist - doctor/status report discovered profile stores README Status / Working today: - Extra Chrome profile discovery on Mac - Sink adapters union extra-profile cookies README Honest limits: - Linux extra-profile Chrome SQLite stays unread (no libsecret) RELEASE_NOTES_TEMPLATE.md: - Extra Chrome profile discovery in Highlights - Linux extra-profile limit in Honest limits Verified: - go test ./... passes - goreleaser check on both configs passes Co-authored-by: Matt Van Horn --- .github/RELEASE_NOTES_TEMPLATE.md | 2 ++ CHANGELOG.md | 8 ++++++++ README.md | 3 +++ 3 files changed, 13 insertions(+) diff --git a/.github/RELEASE_NOTES_TEMPLATE.md b/.github/RELEASE_NOTES_TEMPLATE.md index d9dea90..ab80595 100644 --- a/.github/RELEASE_NOTES_TEMPLATE.md +++ b/.github/RELEASE_NOTES_TEMPLATE.md @@ -6,6 +6,7 @@ Continuous Mac to Linux cookie sync over Tailscale. Your agent runtime wakes up - **Mac to Linux sync**: Your Mac's Chrome sessions flow to a Linux agent runtime (Grok Bot, cloud VM, homelab server) via live CDP injection over Tailscale - **Live CDP injection**: Cookies go directly into Chrome's in-memory store via `Storage.setCookies` - no SQLite write, no Keychain, no libsecret +- **Extra Chrome profile discovery**: Mac profiles (Profile 1, Profile 2, etc.) are auto-discovered and decrypted; extra-profile cookies flow to sidecar, adapters, and live CDP alongside Default profile cookies - **Tailscale-only transport**: AES-256-GCM sealed envelopes over your tailnet's WireGuard channel - **Security-by-default**: Linux sinks with missing policy ship nothing; explicit `policy: blocklist` required for sync-all @@ -43,6 +44,7 @@ See the [README](https://github.com/mvanhorn/agentcookie/blob/main/README.md) fo - **Linux SQLite write is 0**: Expected. Success is the `live_cdp: injected N cookies into M context(s)` line. - **Omitted policy ships nothing**: On Linux, missing `blocklist.yaml` or omitted `policy:` means allowlist-empty. Write `policy: blocklist` with `domains: []` for sync-all on a trusted box. - **Google/DBSC cookies**: Need local sign-in on the sink. Copied cookies expire in minutes. +- **Linux extra-profile SQLite stays unread**: Discovery and doctor/status name stores, but Chrome SQLite decryption requires macOS Keychain (no libsecret). Sidecar/plaintext and live CDP remain the Linux path. - **CDP port is loopback-only**: Same-user processes can attach to `127.0.0.1:9223` and read injected cookies. This is the same-user trust boundary. - **Cookie values never logged**: Cookie values do not appear in logs or doctor output. diff --git a/CHANGELOG.md b/CHANGELOG.md index 61eb453..a5031cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ The 1.0 release ships Mac to Linux cookie sync as the featured path. Your Mac's - No Chrome SQLite write on Linux (expected; success is the live CDP inject line) - Tailscale-only transport with AES-256-GCM sealed envelopes +### Extra Chrome profile discovery (#115, #116) + +- Mac extra Chrome profiles (Profile 1, Profile 2, etc.) are auto-discovered and decrypted +- Extra-profile cookies flow to the sidecar, adapters, and live CDP alongside Default profile cookies +- Sink adapters union extra-profile cookies through the same blocklist policy +- `agentcookie doctor` and `agentcookie status` report discovered profile stores +- Linux: discovery and doctor/status name stores, but Chrome SQLite is not decrypted (no libsecret); sidecar/plaintext and live CDP remain the Linux path + **Install:** Download from [GitHub Releases](https://github.com/mvanhorn/agentcookie/releases/tag/v1.0.0). Verify against `checksums.txt`. See the README for the full how-to. diff --git a/README.md b/README.md index bd5726a..969da90 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,8 @@ The secrets bus (bearer tokens, API keys, OAuth refresh tokens) is untouched by - Mac to Mac continuous sync (second Mac, Mac mini) - Live CDP injection on Linux (cookies go into Chrome's in-memory store) - Three cookie delivery surfaces on macOS sink (Chrome SQLite, plaintext sidecar, per-CLI adapters) +- Extra Chrome profile discovery: Mac profiles (Profile 1, Profile 2, etc.) are auto-discovered and decrypted; extra-profile cookies flow to sidecar, adapters, and live CDP alongside Default profile cookies +- Sink adapters union extra-profile cookies through the same blocklist policy - Per-CLI secrets bus for bearer tokens and API keys - 520+ unit tests across 26 packages @@ -298,6 +300,7 @@ The secrets bus (bearer tokens, API keys, OAuth refresh tokens) is untouched by - CDP port is loopback-only; same-user processes can attach and read injected cookies - Sidecar at `~/.agentcookie/cookies-plain.db` is plaintext at rest (not a success metric; verify with live CDP) - Google/DBSC cookies need local sign-in on the sink; copied cookies expire in minutes +- Linux extra-profile Chrome SQLite stays unread (no libsecret); discovery and doctor/status name stores, but decryption requires macOS Keychain - No live key rotation yet; re-run wizard on both sides to rotate - Cookie values never appear in logs; do not use `cookies --json` as a verify step