Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Historical entries retain their original delivery coordinates.

## Unreleased

## 0.18.83

Prefer a provisioned Lightpanda for LinkedIn profile reads while Chromium stays authoritative for the remaining transports.

- `createBrowserSession` accepts a browser-engine selection: `"auto"` resolves to Lightpanda only for cookie-yielding realms with a provisioned binary, `"chrome"` keeps the existing lane, and `"lightpanda"` requires both eligibility and provisioning.
- The LinkedIn profile transport defaults to `"auto"` for its qualified reads — identity, personal stats, connections, and organizations — and keeps cookie values out of process arguments by seeding through the driver's stdin batch.
- Chromium fallback is limited to a recognized Lightpanda protocol incompatibility raised before navigation inside the original deadline; provider rejections, authentication failures, and cleanup failures never fall back.
- Contact reads stay on Chromium because their overlay harvests live `network requests` bindings, which Lightpanda has not been proven to report. Article, comment, post, feed, search, and every other provider read also keep the unqualified Chromium default.

## 0.18.82

Publish observed, subject-bound `posts.publish` and `media.publish` operations
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Install [Bun 1.3.14](https://bun.sh/docs/installation) if needed, then install
GhostGet and read a public page:

```sh
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.82/hraness-ghostget-0.18.82.tgz
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.83/hraness-ghostget-0.18.83.tgz
ghostget read https://example.com
```

Expand All @@ -48,9 +48,9 @@ which always names the latest published release. Upgrading from Wrench? Read the
The optional Agent Skill teaches your agent when and how to use GhostGet:

```sh
npx skills add hraness/ghostget#v0.18.82
npx skills add hraness/ghostget#v0.18.83
# With Bun instead:
bunx skills add hraness/ghostget#v0.18.82
bunx skills add hraness/ghostget#v0.18.83
```

Start a new agent session, then ask: “Use GhostGet to read https://example.com
Expand Down Expand Up @@ -182,7 +182,7 @@ firewall.

## Built-in provider catalog

This v0.18.82 source tree supports executable actions for 21 services: Beeper,
This v0.18.83 source tree supports executable actions for 21 services: Beeper,
Bluesky, ClasificadosOnline, Facebook, Facebook Groups, Facebook Marketplace,
GitHub, Gmail, Hacker News, Instagram, iMessage, LinkedIn, Reddit, Substack,
Threads, TikTok, Twitch, WebMCP Registry, WhatsApp, X, and YouTube. LinkedIn
Expand Down Expand Up @@ -266,7 +266,7 @@ For that same released coordinate, install GhostGet in an agent or application
that owns its own model, planning, tool loop, approvals, and interface:

```sh
bun add https://github.com/hraness/ghostget/releases/download/v0.18.82/hraness-ghostget-0.18.82.tgz
bun add https://github.com/hraness/ghostget/releases/download/v0.18.83/hraness-ghostget-0.18.83.tgz
```

```ts
Expand Down
2 changes: 1 addition & 1 deletion dist/apple-photos-client.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @bun
import {
GHOSTGET_VERSION
} from "./index-41extnj8.js";
} from "./index-xgd7hzj2.js";
import {
canonicalJson,
sha256
Expand Down
2 changes: 1 addition & 1 deletion dist/beeper-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from "./index-26yq8q16.js";
import {
GHOSTGET_VERSION
} from "./index-41extnj8.js";
} from "./index-xgd7hzj2.js";
import {
canonicalJson,
canonicalJsonSha256Matches,
Expand Down
2 changes: 1 addition & 1 deletion dist/index-41extnj8.js → dist/index-xgd7hzj2.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @bun
// src/version.ts
var GHOSTGET_VERSION = "0.18.82";
var GHOSTGET_VERSION = "0.18.83";

export { GHOSTGET_VERSION };
4 changes: 2 additions & 2 deletions docs/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,12 @@ delivery proceeds through a new source-qualified version.

## Install the canonical release

These commands require the matching published immutable v0.18.82 release.
These commands require the matching published immutable v0.18.83 release.

For the CLI:

```sh
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.82/hraness-ghostget-0.18.82.tgz
bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.83/hraness-ghostget-0.18.83.tgz
ghostget --version
ghostget doctor --json
```
Expand Down
14 changes: 7 additions & 7 deletions kb/launch/social-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG
Post 9 of 9, 192 characters

```text
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.82.
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.83.

https://ghostget.com/blog/introducing-ghostget/
```
Expand Down Expand Up @@ -115,7 +115,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG
Post 9 of 9, 192 characters

```text
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.82.
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.83.

https://ghostget.com/blog/introducing-ghostget/
```
Expand Down Expand Up @@ -173,7 +173,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG
Post 9 of 9, 192 characters

```text
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.82.
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.83.

https://ghostget.com/blog/introducing-ghostget/
```
Expand All @@ -197,7 +197,7 @@ GhostGet is for Claude Code, Codex, Cursor, and other agents that run commands o

The plan is for GhostGet to stay small. Your agent does the thinking, and GhostGet runs only actions someone has reviewed. Each new service arrives as reviewed actions with their own previews.

GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.82.
GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.83.

https://ghostget.com/blog/introducing-ghostget/
```
Expand All @@ -224,8 +224,8 @@ Topics: Developer Tools, Artificial Intelligence, Open Source
- Sometimes a post goes through but the answer gets lost on the way back. GhostGet writes down every send before it leaves and never sends it again on its own until it knows what happened.
- Anything beyond a read starts as a preview that shows the service, the account, and exactly what will be sent. Your agent can prepare it. Nothing is sent until someone confirms that exact preview.
- GhostGet is for Claude Code, Codex, Cursor, and other agents that run commands on your Mac or Linux machine and need to read the web and use the accounts you already have.
- GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.82.
- Latest release: v0.18.82. https://ghostget.com/blog/introducing-ghostget/
- GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.83.
- Latest release: v0.18.83. https://ghostget.com/blog/introducing-ghostget/

## Beats

Expand All @@ -251,4 +251,4 @@ Topics: Developer Tools, Artificial Intelligence, Open Source
- claimsTotal: 248. verification/claims.json, every claim
- claimsEvidenced: 229. verification/claims.json, status evidenced
- claimsConfigReadback: 15. verification/claims.json, layer configuration-readback
- status: Latest release: v0.18.82. package.json version
- status: Latest release: v0.18.83. package.json version
193 changes: 178 additions & 15 deletions kb/plans/lightpanda-semantic-capture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
type: plan
area: browser-verification
status: completed
status: in-progress
---

# Prefer provisioned Lightpanda for public semantic capture
Expand Down Expand Up @@ -86,26 +86,162 @@ Qualification, bounded to read-only evidence:
writes were issued, and the auth realms were verified unchanged
afterward. See Result for the outcome.

## Engine-aware contained sessions and the LinkedIn default

The follow-on integration adds a `BrowserEngineSelection`
(`"chrome" | "lightpanda" | "auto"`) option to `createBrowserSession`,
reusing the full custody contract — artifact roots, socket directory,
task-owned proxy, cleanup journal, stdin cookie seeding, operation
deadlines, and recovery handles — rather than a parallel session path.
`"auto"` resolves to Lightpanda only when the auth realm yields explicit
cookies (`cookie-source`, `cookies-file`, or `browser-profile` with an
attached `cookieSource` and no `storageState`) **and** a validated
Lightpanda 1.0.0 binary is provisioned; everything else resolves to
Chromium. Automatic fallback to Chromium fires only on a recognized
`LightpandaCompatibilityError` raised before navigation, within the
original deadline. Provider rejections such as a LinkedIn 401 never
fall back.

Lightpanda global arguments keep `--config`, `--session`,
`--content-boundaries`, `--max-output`, `--action-policy`, and the plain
task-owned `--proxy` URL; Chromium-only flags (`--profile`, `--state`,
`--headed`, `--executable-path`, `--allowed-domains`, launch `--args`)
stay off. Cookies seed through the driver's stdin batch so values never
enter `argv`. First-party context after import opens the reviewed
origin's `/robots.txt` (the same realm page Chromium uses pre-cookie)
rather than a signed-in root that redirects to `/feed` and can
challenge a fresh automated session.

`createLinkedInProfileBrowserTransport` threads `engine` through
`LinkedInWebExecutionOptions` and defaults to `"auto"`, making
Lightpanda-first the default for its qualified funnel: identity probe,
personal-profile stats, connections, and organization reads. Contact
reads pin Chromium at their call site: the overlay harvests live
`network requests` bindings, and Lightpanda's well-formed empty response
has not been proven to report in-flight requests. Article, comment,
post, feed, and search transports keep the unqualified Chromium
default. An explicit `engine: "chrome"` pins the old lane.

### Live qualification evidence for the LinkedIn lane

- Adapter probe (`browser-profile` + live `cookieSource`): two
consecutive green runs on real auth — exact same-account subject from
`/voyager/api/me`, 915,578-byte profile HTML, 857,941-byte connections
HTML, stable session, healthy containment, complete cleanup, both auth
realms byte-identical.
- Integrated `createBrowserSession` run: `auto` resolved to Lightpanda,
launched, seeded cookies via stdin batch, opened `/robots.txt`,
closed, and preserved the realm. The identity read returned
`provider-response-401`.
- Discriminator: with the **same** freshly acquired cookie snapshot,
a plain HTTP `GET /feed/` returned a 302 authwall bounce while
Lightpanda returned the same 401 — and the previously-green adapter
then reproduced the same 401. The Chrome session itself had become
invalid in the interval; all session-path 401s are dead-auth noise,
not an engine or integration defect. No engine makes a dead session
succeed.
- `network requests` returned a well-formed empty list in Lightpanda,
but whether it reports in-flight requests is unproven — the contact
overlay's request-binding harvest stays on Chromium until observed.
Synthetic fixtures covered cookie import fidelity.

### Cleanup admission under a durable publisher

An independent review found the first integration could not survive the
production authenticated-read path: cleanup admission publishes a durable
resource identity, and the live control-witness binder requires
`engine: "chrome"` plus `browserLaunched: true`. A CDP-attached Lightpanda
session reports `engine: "lightpanda"` and `browserLaunched: false`, so it
could never bind. The remediation:

- The control-witness bind is now engine-gated (`chrome` only). Lightpanda
sessions publish `prepared` then `launch-intent` and stay unbound.
- Launch-intent quiescence is engine-agnostic: it checks the daemon's
exact `active` flag and session/socket identity pins, so an
active-while-settling Lightpanda session retries inside the bounded
convergence window instead of hard-failing the strict Chrome parser.
- `auto` with a publisher runs a throwaway preflight before the durable
`prepared` publish — spawn, `open about:blank`, one stdin cookie import,
`close`, serve-child reap, and a daemon-observed `inactive` check on
unpublished roots. A protocol incompatibility resolves to Chromium
before any durable identity exists; a post-publish compat failure can
never register a second identity and fails closed.
- Executable resolution is gated on the selected engine, so a stale
`LIGHTPANDA_PATH` cannot break unrelated Chromium sessions.

A live run through `createBrowserSession` under a publisher against the
real driver and a cookies-file realm completed `prepared` →
`launch-intent` publication, `journal-quiescent`, and both root-removal
journal entries, with every private root deleted. A second live run with
no provisioning env resolved `auto` to Chromium, confirming the graceful
degradation leg.

A second review round hardened the preflight itself:

- The inactivity proof now runs only after a fully successful probe —
a probe failure already decided the outcome, and running it could wrap
the compat error in an `AggregateError` the `instanceof` fallback check
cannot see. Quiescence proof uses the production
`convergeBrowserCleanupResourceProof` (10s bounded window, settling
retries, exact identity pins) instead of an ad-hoc 2s poll, with inner
command timeouts clamped to the remaining operation deadline.
- A daemon that can never prove the closed session inactive is now
classified as a protocol incompatibility — settling-class failures
become a bare `LightpandaCompatibilityError` so `auto` still resolves
to Chromium, while the unproven throwaway roots are preserved rather
than deleted. Identity, boundary, and malformed-output faults stay
fail-closed.
- Proxy creation is tracked through `networkProxyCreation.pending` and
closed late in teardown, mirroring the contained-session path, so a
deadline abort cannot leak a live loopback listener.
- The owned serve-child reap uses a dedicated 6s bound because
`lightpanda.close()`'s own worst case (~1s SIGKILL grace + 3s exit
wait) exceeds the generic 2s resource-teardown bound.
- `close()` sets `acknowledged` only after the reap succeeds, so a reap
failure stays open and retryable through the launch-intent recovery
instead of being masked.

A third review round verified every remediation in code and found one
remaining minor defect, now fixed: the LinkedIn transport's one-shot
`initialBatchPending` rewrite flag was consumed by any command,
including stdin-free lifecycle invokes such as the preflight's
quiescence `session info` probe. A Chromium fallback after that probe
would have navigated to the signed-in root directly instead of warming
the realm page first. The flag now clears only when an invocation
carries a stdin batch.

Note: cookie replay into a contained browser is not novel risk — the
Chromium lane already seeds the same acquired cookies for
`browser-profile + cookieSource` auths. The session invalidation window
overlapped other signed-in-browser lane work, so attribution to the
Lightpanda reads is not supported by the evidence.

## Result

- Delivered and verified: Lightpanda-first public semantic capture in
Ghostget `v0.18.79` and Direct `v0.7.29`, released, mirrored
byte-exact on npm, and promoted to production.
- LinkedIn authenticated Lightpanda read: **qualified for the identity
read only.** One `/voyager/api/me` probe through the isolated
Lightpanda session returned the exact same-account subject bound to
`linkedin-main` (`subjectMatches: true`), with imported cookies,
healthy containment, complete cleanup, and both auth realms
byte-identical afterward. This contradicts the adapter note's earlier
finding for that realm: a bound `cookie-source` handoff is accepted
where whole-profile reuse is impossible. One read is not provider-wide
qualification — profile, connections, and RSC contact reads each
still need per-operation evidence before a Lightpanda transport can
be offered.
- LinkedIn authenticated Lightpanda reads: **qualified for identity,
personal stats, connections, and organization reads.** Live adapter
evidence covered identity, profile HTML, and connections HTML on real
auth; unit tests cover engine selection, fallback, custody, and
seeding. The profile transport defaults to `"auto"` — Lightpanda
first for cookie-yielding realms with a provisioned binary, Chromium
otherwise. Contact reads stay on Chromium pending live
network-request-observation evidence. This contradicts the adapter
note's earlier finding for that realm: a bound `cookie-source`
handoff is accepted where whole-profile reuse is impossible.
- Residual live gap: the integrated session path (including durable
cleanup admission) is now mechanically green — a publisher-enabled
Lightpanda session published, journaled, and removed every private
root against the real driver. The identity read still lands inside
the dead-session window, so one provider-200 run through the
integrated path is pending re-authentication; `engine: "chrome"`
remains the documented escape lane.
- Chromium remains mandatory for visual evidence, attached or
profile-backed sessions, connected accounts, and every authenticated
provider read by default. Lightpanda authentication stays disabled
unless a per-provider qualification like the one above passes.
profile-backed sessions, connected accounts, and the unqualified
LinkedIn transports (article, comment, post, feed, search) and all
other providers by default.

## Durable memory

Expand All @@ -123,3 +259,30 @@ Qualification, bounded to read-only evidence:
non-HTTP(S) target allowed, and batch `--allowed-domains` filtering
is not wired for the Lightpanda global arguments — the task-owned
proxy enforces containment instead.
- When an authenticated read fails, prove the session is alive before
suspecting the engine: replay the same acquired cookies over plain
HTTP. Identical rejection across transports means dead auth, not an
engine defect — and three consecutive session-path 401s cost real
qualification time before this was checked.
- Post-cookie first-party context should be a cheap realm page
(`/robots.txt`), never a signed-in root: heavyweight landings can
challenge fresh automated sessions before any in-page request runs.
- A CDP-attached browser has no daemon-launched process for the
controlled cleanup witness — `session info` reports
`browserLaunched: false` and a non-Chrome engine even while its owned
serve child runs. Quiescence must be proven at the launch-intent
phase from daemon-observed inactivity after close plus serve-child
reap, and an `auto` selection with a publisher must preflight
compatibility before the durable identity exists, because a retry's
fresh identity can never register afterward.
- Keep executable resolution gated on the selected engine: validating an
unrelated engine's environment lets a stale `LIGHTPANDA_PATH` break
Chromium sessions that never asked for it.
- A one-shot command wrapper that keys on batch stdin must clear its
armed flag only when a batch actually arrives: stdin-free lifecycle
invokes (`session info`, preflight probes) otherwise consume the
rewrite before the navigation it protects.
- On the Lightpanda lane, `session.runBatch` puts each command on
process argv while the Chromium lane sends one stdin payload — cookie
values must always take the `dependencies.runBatch` stdin channel, and
future callers must not seed cookies through `session.runBatch`.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hraness/ghostget",
"version": "0.18.82",
"version": "0.18.83",
"description": "GhostGet gives your AI agent named web actions: read a page, archive one media item, or use a connected account, without credentials or a browser to steer.",
"license": "MIT",
"type": "module",
Expand Down
Loading
Loading