02 decision 17, the README's free and sold section, M6, M7 and M10, 06's
jobs and premium section, and the bundles proposal now follow ADR 0067.
Local paid bundles and the job-pack loader move before the questionnaire's
beta; the edit-versus-job line keeps user-directed redlines free.
The security questionnaire, RFP responses, the contract redline, bank
statements, medical chronologies and contract and lease registers ship as
paid bundles that run on the user's machine and unlock with a SurfSense
license, the 30-day trial included. The app, the general agent, the engines
and the shared job infrastructure stay free under Apache-2.0, and a user's
own files never need a license. Supersedes ADR 0047's job-content clause.
M7, the security questionnaire, now ships before M6's redline job on the
shipped Excel revise engine; 02 phase 5a builds no new xlsx engine and 5b
becomes the shared row-by-row runner. The clean-room fix leaves the critical
path for the contracts wave. The paid team library and its exception to
decision 9 are recorded as proposals, and the three open decisions point to
08.
The proposal is built, so it is deleted and ADR 0066 is accepted with the decisions made while building it: light pages, SVG and CSS only, a Markdown-only skill. agent.md and studio.md list the page tests, and comments and docs that named only Word and PDF as refinable name written web pages too.
The figure kit SurfSense inlined into saved pages is gone: the agent read its
code anyway, and models follow examples best. isometric.md now gives a short
helper the agent copies whole, and examples/ holds three complete figure pages
built on it, a spring scale, a pin-tumbler lock and a conveyor with three
workers, one for each kind of mechanism. SKILL.md and figures.md lay out the
work in rounds. A saved page is kept exactly as written, plus the no-network
policy. The examples are tested as pages save_page keeps.
The figure kit gains a true isometric camera fitted to the mechanism, rounded
parts with inked silhouettes and glints, soft shadows, coil springs, and small
parts that paint with what they sit on: screws, hole grids, ticks, knurls and
a pen for any face. figures.md becomes a craft guide (40 to 80 parts, detail
on every face, the mechanism filling the frame, checking the screenshot), and
kit.md's example is a dense spring scale. The kit is tested in Node.
Every other Studio document is light, and a frame's prefers-color-scheme
follows macOS rather than SurfSense's theme, so a page went dark inside a
light app. The design guide now asks for one light scheme with no fixed
colours, only text and marks in contrast with what they sit on; the skill's
example and the figure kit set no palette of their own.
A written page keeps itself as its spec, so Refine rewrites it whole into the
next version, checked as a draft is. The design guide its prompt carries
counts against the model's window. The refusal names web pages too.
Studio's Web page button takes Word's split: a model writes_script() lets
through writes the whole page in one reply from the agent's design guide,
checked for structure with three attempts and stored under the no-network
policy; any other model keeps the escaped template.
The agent writes an HTML page under outputs/ and surfsense_save_page keeps it
as a versioned Studio artifact: checked, with the figure kit inlined where the
page asks for it and the no-network policy stored ahead of it, indexed by its
text, and shown back as up to four screenshots Electron captures at 1,280 px
with reduced motion on. The surfsense-html skill teaches the contract, the
design guide, figures and SurfSense's own figure kit. create_artifact no longer
starts a Studio web page for the agent.
A stored or shown page gets a CSP meta right after its doctype that allows
only inline code and data or blob media; the viewer drops allow-popups and
Electron refuses a frame's navigation away from its srcdoc. A made file may
be a page, indexed by its readable text. Adds the design guide both page
paths will read.
Proposes letting a model at the agent's bar write a web page as HTML,
through a new agent tool and skill and Studio's Web page button, run in
a viewer frame with no network. ADR 0066 records the decision.
Each listed plugin gets a page at /plugins/<id>, built from its list entry
as the app's plugin page is: about, tools and details. SurfSense's own come
from a copy of the app's first-party list, the registry's from the signed
list, so a newly listed plugin gets its page without a change here. Cards
open the page and show the name with the app's verified mark.
The per-scraper marketing pages and the [slug] route are removed. Their
addresses redirect permanently to the plugin's page, and /reddit and
/instagram, which the app does not list, to /plugins. Homepage components
no page imported are removed with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accept job no longer needs a personal token: it acts as github-actions[bot]
and runs plugins-registry.yml on the submission branch itself, live included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding one no longer starts a sign-in: it answers 201 once the
authorization server's hosts are allowed, and Connect on its row
registers and opens the sign-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves dev's sign-in failure codes (#2267) and its OCR ADR (#2295)
against this branch:
- dev's ADR 0062 keeps its number; this branch's become 0063, 0064 and
0065, with every citation updated.
- A failed sign-in says why on the plugin's details, where this branch
follows it, in dev's words for its code.
- dev's refusal tests keep the plugin waiting to sign in, as this branch
saves it first, and its failure tests read the plugin's details.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While third-party plugins are off, every route that reaches a plugin's
server refuses one SurfSense does not publish with 403 third_party_off
before sending anything, and turning the switch off ends any third-party
sign-in under way. Yours says "Third-party plugins are off" with no Connect,
a plugin's details say "Not used" with only Remove, and Add custom plugin
holds Connect beside the same switch and consent as Browse. The in-place
sign-in fallbacks for screens outside Settings › Plugins, which none are,
and the test stand-in's probe route, which the API no longer has, are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin's details show its state alone, with at most one reason behind an
info button beside it: why it needs an OAuth app, the registry's reason for
a delisted plugin, or "Not used" for a third-party plugin while those are
off. Notes about its tools sit with its tools. The plugin's icon there is as
tall as its name and type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin's details show one action beside its menu: Connect while it is not
connected, whatever that takes, and Disconnect once it is. Edit connection,
Edit headers, Change OAuth app and License settings move into the menu, and
the status line holds no buttons. Every plugin in Yours not connected, of any
type, shows the same Connect in its row with why beside it in grey; an OAuth
sign-in started there runs in the plugin's details.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connect from Add custom plugin, Browse, a public page or search saves an
OAuth plugin and opens its details in Yours, where the sign-in runs; the
browser opens at once. A provider taking only registered apps, or a
registry entry declaring user_client, keeps the plugin as "Needs an OAuth
app" and its details set one up. A saved Browse entry shows its status as
Yours does, opening its details. A plugin never signed in reads "Not
connected" with Connect, nothing to disconnect, and the user's own OAuth
app is offered only where it is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sign-in listed the server's tools over Streamable HTTP alone, so an SSE
server whose POST asks for a sign-in, as Asana's, Atlassian's and
PayPal's /sse do, was signed in and then refused. A finished sign-in now
lists as adding a server does, Streamable HTTP then SSE, and records the
transport that answered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SSE client raises a refused stream as the HTTP library's error, which
SurfSense's MCP client never kept, so any status an SSE stream answered
was lost. It is now kept, and when neither transport answers, an SSE
address that answered anything but "nothing here" is the one reported: a
key it asks for, a server down, or 410, an address its server retired,
as DeepWiki did its /sse, reported as plugin_gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The own-app dialog lays out the steps it takes: register an app with
the provider, give it the callback address, copied in one click with
a note where a developer build's address moves, and paste the client
ID and secret, with scopes folded away.
- A plugin's own app is chosen when it is added, from Advanced settings,
as Claude Code takes it; the menu item on every OAuth plugin is gone.
- A provider whose metadata names no registration, as GitHub's, is asked
for the user's own app at once, with no registration sent only to
fail. Refusals SurfSense handles log one line, not the SDK's traceback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Advanced settings and Edit connection hold rows of a header name and a
hidden value, the first named Authorization with its value hinted
"Bearer your-key", in place of a key with a Bearer, Basic or custom
header switch. Only rows with a value are sent. The switch, its draft
and the Basic encoding are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The user's own OAuth app has a dialog of its own, opened only when a
server registers no app or refused SurfSense, from Add or Sign in. A
plugin signed in with one shows Change OAuth app; the API says which
with `own_app`. Advanced settings no longer holds it.
- The gateway names a plugin whose refresh is refused in the running
turn, and answers a call to a tool blocked mid-turn without reaching
the server. It declines the optional GET stream for Streamable HTTP.
- opencode 1.x fixes a turn's tools when it starts (measured), so its
budget goes first to the tools the user's message matches.
- Plugin dialogs scroll within a height below Settings', faded.
- The real Claude Code tests serve the API on a port, and one finds a
deferred plugin tool among 81.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A turn offered at most 24 plugin tools, the plugins connected first winning,
so one with many tools hid the rest and the agent called them not connected.
- Claude Code is given every ready tool and finds them with its own tool
search, pinned with ENABLE_TOOL_SEARCH=auto; SurfSense's server is always
loaded.
- opencode, which sends every definition with every request, is offered what
fits a tenth of the model's window: a mentioned plugin first, then each
plugin a tool in turn.
- A plugin left out is named to the user and to the agent, with how to bring
it in: a mention.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
opencode and Claude Code are given each plugin as a loopback address on the
API and a per-thread key, never its key, headers or token. The gateway checks
the plugin is ready on every request, adds its sign-in, and streams the answer
back.
- An OAuth plugin's 401 mid-session is refreshed once and sent again, so the
runtime keeps its MCP session.
- A 403 insufficient_scope is recorded as a check records it, and the running
turn names the plugin as needing more access.
- A server on the older SSE transport has its endpoint pointed back at the
gateway.
- opencode's proxy now refuses every host, and Claude Code's agent runs use
the chat proxy; the plugin proxy becomes runtime_proxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0062 records the connection model: Streamable HTTP or SSE, found from
the server; no sign-in, headers, or OAuth registered in the spec's order or
as the user's own app; one Add screen with Advanced settings. The plugins
doc describes the built behaviour, and the connections proposal it replaces
is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A connection is a transport, Streamable HTTP or legacy SSE, and one sign-in:
none, headers for any key, or OAuth that registers itself or uses the user's
own app. Detected from the server where it can be, shared by the Add custom
plugin dialog and the registry bot, with no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins stay remote MCP servers. A server started by a command is not added
as a custom plugin; local ones come with bundles, whose design is redone
first. The reasons move from the open questions into a decided section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
At llama.cpp b11050 a unified cache that overflows while decoding ends
every running reply with "Context size has been exceeded."
(server-context.cpp). `_CACHE_FULL` held only the two lines llama.cpp
writes to its log, which no client receives, so:
- before a reply's first token, the 500 was classified
`model_cannot_run` ("SurfSense cannot run this model. Pick another
model.");
- after it, the in-stream error raised `StreamedError`, which fell
through to `unknown`.
Match llama.cpp's own message, and classify a local runtime's
`StreamedError` that carries it as `runtime_busy` too. A hosted
endpoint's in-stream error and other local ones stay `unknown`.
Every 8 pages, once the reading has run its minute and an upload or note
waits at its priority, OCR keeps the pages read so far in ocr/pages.json and
gives way; the run that resumes reads only the pages left. It gives way only
with the pages on disk, and never after its last page.
A waiting source with ocr/ counts as a reading that gave way itself, so two
long scans do not take turns. ocr/ goes with a cancel or a failure, in OCR's
part or in Docling's after it, when Docling takes the file over, and when a
cancel lands while it waits in the queue, so a retried source is never taken
for one that gave way.
A thread that moved from opencode to Claude Code keeps its opencode session,
and reading it failed whole when opencode was not running. The engine the
thread left is now left out of the read, with a warning; the engine running
it is never skipped. Both readers take the engines as an argument and pick
the current one from the thread's agent_engine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>