Commit graph

110 commits

Author SHA1 Message Date
Anish Sarkar
659ed39c2e fix(reddit): ask a 404 once more on a fresh exit before it reads as gone
An existing post's page answered 404 on one run in about 30, and the call
returned no items without an error. Reddit answers a missing post, user or
subreddit with a 200 page holding none, so a 404 is one exit's bad answer: it
is asked once more on a fresh exit, then read as nothing.
2026-10-23 02:51:30 +05:30
Anish Sarkar
f18ceeed8d fix(reddit): close a stopped stream before the call returns
A call that stopped at max_items left its generator to the garbage
collector, which closed it in another context: releasing the subreddit
call's held session raised a ValueError, logged on every such call (live,
2026-10-23). scrape_reddit now closes the stream itself, as Instagram's does,
and so do the fan_out loops inside it, so their workers' sessions close
before the call returns too.
2026-10-23 02:50:07 +05:30
Anish Sarkar
c324ac4b9b perf(instagram): read a feed's post pages on up to three exits at once
A feed read the posts the embed lacks, its pinned ones and those past the
embed's six, one after another on one exit: six pages took 16.8s there and
7.8s on three exits (2026-10-23), which also keeps each under its ~14 reads.
The feed walks its grid in windows of only the posts still wanted, so a full
feed reads no more pages and order stays the grid's; one blocked exit
cancels the others and is the target's refusal, as before.
2026-10-23 02:29:58 +05:30
Anish Sarkar
f05a5b074b perf(instagram): read a page without warming the exit on the home page first
The warm fetched instagram.com for a csrftoken, which only the JSON API
needs. Every logged-out page the plugin reads, profile, embed, reels tab,
post and reel, works as an exit's first request and leaves the next page
working (probe 2026-10-23, three countries), so fetch_html no longer warms.
fetch_json still does. Every Instagram call makes one request fewer.
2026-10-23 02:28:02 +05:30
Anish Sarkar
8a0e8b9db0 perf(reddit): read a subreddit call's pages on one held session
A call naming only a community ran outside fan_out, so each page opened its
own exit and paid its own warm-up: 6 requests where 3 do. It now holds one
session, as fan_out does for every other target, and still streams, so it
stops at max_items rather than reading every post's comments first. Live, a
subreddit with its details went from 10.7s to 8.8s median.
2026-10-23 02:27:04 +05:30
Rohan Verma
70d5557bc0
Merge pull request #2320 from MODSetter/feat/plugin-usage-meter
feat(plugin-host): count plugin calls per licence
2026-10-22 13:54:00 -07:00
DESKTOP-RTLN3BA\$punk
adc2cc0608 docs(plugin-host): say a resent rollup replaces the earlier copy in PostHog 2026-10-22 13:52:10 -07:00
DESKTOP-RTLN3BA\$punk
3fb9074cd3 fix(plugin-host): keep a query's encoded space or path after an apostrophe redacted 2026-10-22 13:42:31 -07:00
DESKTOP-RTLN3BA\$punk
726e93f435 fix(plugin-host): hide a URL's query past an apostrophe, an upper-case scheme or trailing punctuation
An apostrophe followed by & or = stays inside the address, so
?q=patients'&location=clinic no longer logs its second field. HTTPS:// and
WSS:// are cut like their lower-case forms, and only one sentence-ending
punctuation mark after an address is left outside the redaction, so a query
ending in !!! is hidden too.
2026-10-22 13:36:44 -07:00
DESKTOP-RTLN3BA\$punk
3abca654b7 fix(plugin-host): give each rollup event a fixed uuid so a day sent again is not counted twice
Without a uuid PostHog assigns a fresh one per send, so a manual --day rerun, or a send whose mark failed, counted the day twice. The uuid is uuid5 over the event name, day, plugin, tool and outcome.
2026-10-22 13:34:45 -07:00
DESKTOP-RTLN3BA\$punk
996ad4e3c1 Merge remote-tracking branch 'origin/dev' into feat/plugin-usage-meter
# Conflicts:
#	docs/architecture/plugin-host.md
2026-10-22 13:25:42 -07:00
DESKTOP-RTLN3BA\$punk
a05e0ca7bb Merge remote-tracking branch 'origin/dev' into fix/privacy-claims
# Conflicts:
#	plugins/remote/proprietary/reddit/tests/test_fetch_resilience.py
2026-10-22 13:22:39 -07:00
Anish Sarkar
e1188eefa0 Merge remote-tracking branch 'upstream/dev' into fix/reddit-instagram-anonymous 2026-10-23 01:03:32 +05:30
Anish Sarkar
f97f71ac50 feat(instagram): tell the agent to fetch a video's URL from its post when needed
A posts feed's newest posts come from the embed, which has no video URL, and
reading each post's own page would double a video-heavy feed's requests
against an exit's ~14 reads. scrape's description now says to scrape the
post's URL for a video's file instead. Its URL is signed and expires within
hours to days, so fetching it when needed also hands back one that works.
2026-10-23 00:54:25 +05:30
Anish Sarkar
18d6e444d4 fix(instagram): fill what the embed's copy of a post lacks from the pages already read
A post taken from the embed had no product type, alt text or owner's name,
so a reel in the posts feed did not say it was one. The profile grid carries
the first two and the profile page the name. Its video URL stays missing
rather than cost the post's own page; Known gaps says so, and that the reels
feed and a post URL carry it.
2026-10-23 00:48:47 +05:30
Anish Sarkar
b355d595f6 perf(instagram): stop asking a reels feed for the profile's embed
Each reel is read from its own page for its video URL, which the embed's
copy lacks, so the embed went unused: a second per profile and one of the
about 14 reads an exit gets before Instagram rate-limits it.
2026-10-23 00:48:35 +05:30
Anish Sarkar
c3cb1ed20e feat(reddit): order a search by relevance unless a sort is asked for
Newest first, the old default for every call, answered "fastapi" with job
posts that only mention it. sort is now optional: a search asks Reddit for
relevance, or for the newest under a post date limit, which stops paging at
the first older post; a subreddit's listing still orders by newest.
2026-10-23 00:44:26 +05:30
Anish Sarkar
ee926dcb7b fix(reddit): read search results shown with a preview of their text
Ordered by relevance, Reddit wraps most results in
search-post-with-content-preview rather than search-post-unit, around the
same unit, so a search for fastapi read 1 of its 7 posts.
2026-10-23 00:44:05 +05:30
Anish Sarkar
64ea2f150c feat(instagram): read profiles from the pages a logged-out visitor sees
web_profile_info answers require_login to every logged-out session, so
pages/ reads what a visitor's browser is served instead. Details join the
profile's page (bio, following, link) with its embed (exact follower and post
counts, its newest posts in full), into the web_profile_info shape the parsers
read. The posts feed is the page's grid, each post taken from the embed or
else its own /p/<code>/ page; pinned posts are the ones the grid lists before
the embed's first. The reels feed is the /reels/ tab, which marks pinned reels
and counts plays.

A feed stops at the 12 a logged-out visitor sees, as web_profile_info's did,
and the tool says so. The login-wall message no longer claims profiles need
an account.
2026-10-23 00:39:11 +05:30
Anish Sarkar
be7dd49e6e fix(instagram): move to a fresh exit when a login redirect is a rate limit
After about 14 page reads on one exit, Instagram redirects to its login page
with is_from_rle and a 429. That is the exit asking too often, not a page that
needs a login, so it rotates like a 401/403 instead of failing the target.
2026-10-23 00:38:44 +05:30
Anish Sarkar
4a40a124d2 feat(reddit): read a user's overview, posts and comments from their profile feeds
Each tab is its own feed under /svc/shreddit/profiles/. Comments there are
<shreddit-profile-comment> elements, laid out unlike a post page's; one on
the user's own profile is in their u_<name> subreddit. The posts feed links
no next page, so it pages on from its last post's more-posts-cursor.
2026-10-23 00:37:28 +05:30
Anish Sarkar
8649d9cb31 feat(reddit): read a subreddit's details from its top post's page
A subreddit's own pages, /r/<name>/ and /about/, often serve a JavaScript
check, so its header is read from the page of the top post in its hot
listing, which carries the name, id, description and member count. Whether
it is NSFW comes from the listing.
2026-10-23 00:36:56 +05:30
Anish Sarkar
44ec9bb89b feat(reddit): read a post and its comment tree from the post's page
A post comes from /comments/<id>/, whose comment tree streams in after
</html>, so the page is parsed whole. Later top-level comments load by the
page's own POST to /svc/shreddit/more-comments/ until max_comments, which the
scraper now passes down as the .json read's limit was. Nested "more replies"
stay unfollowed, as the .json read stopped at its more stubs.
2026-10-23 00:36:42 +05:30
Anish Sarkar
72cf1ad6ca fix(reddit): read Reddit's logged-out pages over plain HTTP
Reddit closed logged-out .json on 2026-10-13 but still serves the HTML it
renders for a logged-out visitor. Each exit now warms with one listing sent
with the over18 cookie, which sets the loid jar; asked with no cookie, Reddit
answers a reCAPTCHA page and then a JavaScript check on that exit's full pages.
A challenge page rotates the exit rather than being solved, and no browser is
launched.

The scraper reads through pages/, which turns each page into the .json shape
it always paged through. This change reads subreddit listings and search,
site-wide and in one subreddit. Routes are asked for as Reddit spells them,
and a form POST carries Reddit's own referer, since either mistake is a 403.
2026-10-23 00:36:11 +05:30
Anish Sarkar
97faf491e5 fix(plugins): refuse SSE addresses, and never move an SSE plugin behind the user's back
Adding a plugin by an address ending in /sse, as providers' docs still
give theirs, is refused before anything is contacted, and the Add dialog
says so as the user types it, with Add off. An address that is not /sse
but streams SSE is refused as before.

A plugin recorded on SSE by an earlier version is no longer moved to its
server's /mcp at a check: it is never contacted, and its status, in the
list and in its details, says SSE is not supported and how to replace it.

The registry bot names a submitted /sse address as SSE whatever it
answers, since a sign-in there read as a key to take, and gives the /mcp
beside it when MCP answers there. The submit form says Streamable HTTP
only.
2026-10-22 23:20:33 +05:30
Anish Sarkar
0f068bbf53 fix(agent): rank plugin tools without example requests
Only SurfSense's own plugins carried example requests and no registry entry
does, so they did nothing while the tools fit and, past the share, ranked
SurfSense's plugins ahead for words the others lack. Tools are now ranked
by what every plugin has: name, description, parameters and its plugin's
line. The list entries' `examples` field and its registry check go too.
2026-10-22 21:21:50 +05:30
Anish Sarkar
cd19a066cb feat(agent): hold the plugin tools an opencode turn needs, and grow them mid-turn
While every ready plugin tool fits a quarter of the model's window and the
provider's limit on tools, an opencode turn holds them all. Past that the
agent sees every ready plugin in SurfSense's MCP instructions and holds its
mentioned plugins, the best tools of plugins its message names, the tools
the thread used, and what keyword and embedding search ranks highest,
example requests included, until the share is spent.

A turn's holding only grows, by whole plugin, with the session's rules
rewritten before the step that needs them: after a plugin call, inside the
gateway before the result goes back; when the agent calls a tool it does not
hold, which reads in the thread as "Loaded X's tools"; and through
surfsense_open_plugin and surfsense_find_tools, offered to opencode only and
only when the tools do not all fit. Claude Code still holds every tool.

The window budget and the "Not all of a plugin's tools fit this turn" notice
go. List entries may carry example requests per tool, bounded by the
registry's check, and SurfSense's own plugins now do.
2026-10-22 21:14:15 +05:30
Anish Sarkar
d1fcef6bb8 feat(agent): give plugins to opencode 2.x and drop SSE
Each MCP server is added off and then connected, so one that hangs
holds no other, waited on until its tools are offered, with Code Mode
off and a hard call limit. The gateway takes elicitation out of the
runtime's offer, and turns a runtime's cancel of a call on SurfSense's
plugin host into the host's own stop once no other call of the thread
runs there; a saved result and a step's whole result read
state.content.

SurfSense no longer speaks the older HTTP+SSE transport, on either
engine: connecting a server that answers only there is refused as one,
the gateway's SSE endpoint goes, and the registry refuses an SSE entry.
A plugin recorded on SSE is tried at its server's /mcp at its next
check, moving there, asking to sign in again, or staying, never
offered, with a status that says why.
2026-10-22 19:35:26 +05:30
DESKTOP-RTLN3BA\$punk
dd9e796c12 fix(plugin-host): keep a URL's query hidden when it holds an apostrophe 2026-10-22 00:49:56 -07:00
DESKTOP-RTLN3BA\$punk
26855811d5 docs(plugin-host): say which web addresses the log redaction cuts
It cuts http(s) and ws(s) addresses up to the first space. An address without
its scheme, with escaped slashes or with an unencoded space keeps the rest, so
the docs no longer say a page or search never reaches a log.
2026-10-21 23:06:00 -07:00
DESKTOP-RTLN3BA\$punk
5506c58b14 fix(plugin-host): log a blocked target's error class, not the post it names
Reddit's and Instagram's refusals quote the subreddit, post id or shortcode,
and the fan-out workers logged the whole error at WARNING, which reaches
stderr in production. They now log the class; the tool's error still tells
the user which target was refused.
2026-10-21 17:21:25 -07:00
DESKTOP-RTLN3BA\$punk
e09ad886f4 test(plugin-host): check the serving host writes the counts and sends the rollup itself
Every earlier test read through the meter, whose reads write first, so a host that never ran the meter's jobs still passed. These read the file with a store of their own and watch a stand-in PostHog while the host serves.
2026-10-21 17:08:59 -07:00
DESKTOP-RTLN3BA\$punk
193d9bbe20 fix(plugin-host): keep scraped URLs, queries and ids out of the logs
Perf and warning lines printed the pages, searches, ASINs, video, place and
job ids users asked plugins for. They now name a host or a count. The log
redaction also cuts every web address in a record to its scheme and host, so
a URL quoted in a library's error never reaches a log either.
2026-10-21 16:50:55 -07:00
DESKTOP-RTLN3BA\$punk
7caa1ff3f1 fix(plugin-host): read at most 1 MiB of a call before the license check
The meter read a whole request body before the check refused it, so an unlicensed caller could make the host hold a body of any size; before the meter, a refusal came after the headers alone. Past 1 MiB the meter stops reading, hands the server what it read and the rest, and leaves the call uncounted. An agent's tool call is a few kilobytes.
2026-10-21 16:47:38 -07:00
DESKTOP-RTLN3BA\$punk
5decf8870c build(plugin-host): give the usage meter's file a folder the host can write
The image makes /srv/remote/usage/, owned by the host's user, where USAGE_DB_PATH points by default; production mounts a persistent volume there. A local run's file stays out of git and the image. .env.example lists USAGE_DB_PATH, USAGE_ADMIN_TOKEN, POSTHOG_API_KEY and POSTHOG_HOST.
2026-10-21 16:35:15 -07:00
DESKTOP-RTLN3BA\$punk
907ec1cc42 feat(plugin-host): count each plugin's tool calls per license, day, tool and outcome
host/usage_meter/ counts only tools/call requests to /<id>/mcp: per UTC day, Keygen license id (or unlicensed), plugin, tool and outcome (ok, tool_error, refused, cancelled, error), with calls and total milliseconds, and each license's first day with a call. It sits outside the license check so refusals count, reads only the tool's name, keeps a name the plugin lacks as unknown, and never keeps the arguments, the key or the thread's token.

Counts are written to SQLite (USAGE_DB_PATH) every minute and at shutdown, off the event loop; a failing meter is logged once and never fails a call. Once a day, rows older than 13 months are forgotten and, with POSTHOG_API_KEY, yesterday's totals go to PostHog as personless plugin_usage_daily and plugin_licences_daily events with no license id; python -m host.usage_meter.rollup --day sends one by hand. With USAGE_ADMIN_TOKEN, /admin/usage/first-seen and /admin/usage/daily serve the records to a bearer token; admin is reserved as a plugin id.
2026-10-21 16:34:16 -07:00
DESKTOP-RTLN3BA\$punk
b1b511a45f feat(license-check): name the Keygen license a request's key belongs to
validate-key already answers with the license as data.id. The check keeps it per key from the same answer, with no extra Keygen call, and sets checked_license for the request, allowed or refused, never the key.
2026-10-21 16:33:58 -07:00
DESKTOP-RTLN3BA\$punk
64807193bc refactor(plugin-host): share the request-body reader and the route path
The usage meter reads a tools/call body before the license check, as Stop's calls do, and finds the /mcp path as PostOnly does.
2026-10-21 16:33:56 -07:00
Anish Sarkar
807045fabd docs(registry): simpler plugin submission form
Say what servers the registry supports, drop the Inspector paragraph,
shorten the no-secrets box, and word the access choices as Free and Paid,
so a free account no longer reads as needing one sold by the publisher.
The intake's FREE and PAID match the new choices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:36:12 +05:30
github-actions[bot]
2dae4d5c6c chore(registry): sign the plugin list 2026-10-20 21:56:08 +00:00
Anish Sarkar
878be9a281 chore(registry): mark Notion as verified
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 03:23:34 +05:30
Anish Sarkar
b7fa6cfb74 feat(registry): entries name their headers, the user's own app and transport
A registry entry can name the headers a key goes in, say `auth.user_client`
for a provider that takes only registered apps, and record `transport`. The
submission form and the bot check them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:29:33 +05:30
Anish Sarkar
4d47e14d4f feat(registry): list a plugin from an issue form a bot checks
A publisher or an agent fills in the Submit a plugin form; no git. The bot
contacts the server as the app does, reads how it signs in and its hosts,
checks the declared tool names against what it lists without sign-in, fetches
the icon, and keeps one comment with the entry or what to fix. A maintainer's
registry: accept label has it open the pull request into dev.

Tool names are required, since the plugin's page shows them; a token entry's
key wording is optional, the app's own being used without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:36:38 +05:30
DESKTOP-RTLN3BA\$punk
433d2664fd fix(plugins): review fixes before merge
- Google Search: Google's sorry wall is recognised by host and path, not a
  "/sorry" substring, so a result linking to a page with "sorry" in its URL
  no longer burns the wave's exit (goto and the captcha check share
  captcha.is_sorry_url).
- Walmart: a redirect still pending after the last allowed hop is a refused
  try, so the next exit is tried instead of reading an empty page.
- Amazon: a hedge whose sticky lease is unavailable falls back to the
  gateway instead of spending the call's hedge budget on nothing.
- Fixtures: live tracking and session tokens scrubbed from the captured
  pages (Indeed, Amazon, Google, Google Maps, TikTok, YouTube).
- plugin-host.md: shared_access() everywhere, and the Reddit and Instagram
  gaps now describe their fail-fast latches.
2026-10-14 15:01:56 -07:00
DESKTOP-RTLN3BA\$punk
b249438b69 Merge remote-tracking branch 'origin/dev' into perf/plugins-speed
# Conflicts:
#	docs/architecture/plugin-host.md
#	plugins/remote/host/app.py
#	plugins/remote/proprietary/license_check/tests/test_license_access.py
2026-10-14 05:59:47 -07:00
DESKTOP-RTLN3BA\$punk
d908693ef6 fix(reddit, instagram): fail fast where anonymous reads are closed
Reddit refuses logged-out .json and Instagram's profiles sit behind a login.
Reddit now gives up after one rotation instead of about 2m50s and latches the
refusal, so calls answer at once with an error naming the countries tried.
Instagram no longer rotates on its require_login 401 and points to /p/ and
/reel/ URLs, which still work. Both use the shared license cache, progress and
text results.
2026-10-14 05:56:49 -07:00
DESKTOP-RTLN3BA\$punk
affc5b1325 perf(web-crawl): crawl pages concurrently, cap the HTML before extraction, bound the output
- Pages are crawled level by level, several at once, with an exact page budget.
- HTML is capped at 1 MB before trafilatura and parsed once: a 7.3 MB page
  took 17-25 s to extract, now about 1.5 s. Extractors warm up at startup.
- The tier ladder follows the HTTP status and challenge markers: a 404 fails
  in about 1 s instead of trying the browser tiers. The dynamic tier runs on
  the shared browser pool with a bounded settle.
- Output is bounded: links and contacts are capped with totals, and every
  page gets its share of the agent's text. A 3-page crawl reply went from
  1.85 MB to 49 KB.
- Fixes: GitHub and GitLab URLs filed as social, 404s and Cloudflare pages
  returned as success, loadedUrl ignoring redirects, links resolved against
  the pre-redirect URL, the oldest date on the page used as its date,
  permalink anchors in titles, and raw HTML from the stealth tier.

A 3-page docs crawl went from about 28 s to 4-8 s.
2026-10-14 05:56:49 -07:00
DESKTOP-RTLN3BA\$punk
e9da4198e4 perf(tiktok): comments and trending over TikTok's JSON API, listings on the shared browser pool
- Comments and trending read TikTok's unsigned JSON endpoints, with the
  browser as fallback: comments went from 25-96 s to about 2 s.
- Listings run on the shared browser pool, with event-driven waits, a JS
  click, no home-page warm-up, media blocked, a hedged second country, and
  concurrent targets; profile and video pages go over HTTP.
- Geo-blocked and login-bounced exits end the attempt at once.
- user_search recovers an empty first body inside the page instead of
  starting a new browser, which made it intermittent.
- Fixes: textLanguage, isAd, isPinned, isSlideshow, mentions and slideshow
  media dropped by parse_video, business accounts marked verified, withheld
  comment pages, and the profile item counting toward max_items.
2026-10-14 05:56:49 -07:00
DESKTOP-RTLN3BA\$punk
92a52c8001 perf(walmart): review pages in parallel, fail-fast exits, and ratings for variants
- Review pages after the first are fetched in parallel and deduplicated;
  start URLs and search terms run together, in order.
- Fail-fast transport with hedging; 444, 403, 5xx and a /blocked redirect are
  blocks that rotate, not pages; raw bytes and orjson instead of the lxml
  round trip.
- A failed detail returns the listing card with detailsError instead of an
  error item and an extra listing fetch.
- Fixes: variant products with 0 stars and 0 reviews, the sponsored flag and
  search provenance lost on details, duplicate reviews at page boundaries,
  reviews of other variants unlabelled, specifications and fulfillment always
  null, and lone surrogates in review text.

200 reviews went from 34-99 s to about 10-13 s. WALMART_PARALLEL=0 restores
sequential fetches; WALMART_CANONICAL_URL=1 (off) skips the /ip redirect.
2026-10-14 05:56:49 -07:00
DESKTOP-RTLN3BA\$punk
386ebd83d5 perf(amazon): bounded tails, no fake Google referer, and real prices on fallback pages
- Fail-fast transport with one hedge on slow fetches; sources run together
  in order; the offers panel runs alongside the product page and sellers as
  soon as they are known; one detail wave for the default call.
- No Google referer on search, best-seller and offers requests: in an A/B it
  cut blocked search pages from 10 of 10 to 3 of 10.
- Sponsored cards are unwrapped instead of failing as product_not_found, and
  max_items fills from the current page before paginating.
- Fixes: listPrice read the per-unit price, search positions and
  reviewsCount ('89.7K' read as 897), isAmazonChoice/isBestSeller always true,
  duplicate ASINs, null prices, stock and delivery on Amazon's fallback product
  page (about 70% of pages), offer prices, the reviews link, and zip_code
  silently ignored (now reported, and failures cached).

A default 10-product search went from 31-74 s, with null prices, to 8-11 s.
AMAZON_NO_GOOGLE_REFERER=0 and AMAZON_HEDGE=0 restore the old behaviour.
2026-10-14 05:56:48 -07:00