Skip to content

Releases: Lazy-Coder-03/leftbrain

v0.5.1

v0.5.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Sep 11:34

Fixed

  • Signing in and connecting an account no longer fail with an internal error. The hosted
    server holds one connection to its key database, and a managed Postgres closes a connection
    that has sat idle. Writes reopened it; reads did not. Connecting an account reads before it
    writes - it looks up the application, then lists the account's keys - so the first person to
    arrive after a quiet spell met an internal error, and so did everyone after them, until the
    server was restarted. Authorised tool calls failed the same way and for the same reason.
    Every read now reopens a dropped connection the way writes always did, and the connection
    asks for TCP keepalives so an idle drop is noticed before somebody runs into it. A
    self-hosted server on SQLite was never affected.

v0.5.0

v0.5.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Aug 11:08

Upgrading to 0.5.0

  • If a client or key used the holidays tool, it is gone: tools/list no longer offers it and
    a call is refused as an unknown tool. For "is this date a working day" questions, use
    datetime business_days / add / diff with region and subdiv - the public-holiday
    calendar behind those is unchanged. There is no replacement for festival lookups; a date the
    server cannot source is a date it should not answer.
  • Keys scoped to holidays keep working for every other tool they name; edit the scope on the
    dashboard when convenient.
  • Reminder from 0.4.2: /external/mcp is retired; every tool is on /mcp.

Removed

  • The holidays tool. It answered "which festival is when" from tables it could not vouch
    for. A dry run across 2026 for twenty Indian states and twenty-four countries, and a
    three-source comparison (the West Bengal gazette, an astronomical tithi computation, and the
    upstream tables) showed the upstream Durga Puja days off by one for 2026 and half the
    Bengali festivals absent altogether - and a date with no source behind it is exactly what
    this server exists to replace. Rather than ship it as a source of confident wrong answers,
    it is retired: holidays is no longer published, /docs/tools/holidays is gone, the
    catalogue is 17 tools, and datetime's festival anchors ({"festival": ...} as a date) are
    refused with the reason. A key whose stored scope names holidays keeps loading and simply
    no longer lists it. What stays: datetime's add, diff and business_days still
    honour region / subdiv, because counting working days needs only the public-holiday
    tables, which are the part of the upstream dataset that is what it says it is. (#80, #86, #91)

v0.4.2

v0.4.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Aug 08:18

Added

  • The scope grid says which tools reach the internet. weather, fx_rate, geo and
    url_check carry a network pill with a title that says the call's input goes to a
    third-party service, sit under a "Reaches the internet" heading, and a No network button
    beside All / None / Expand all unticks the four and leaves everything else as it was. The
    same partial serves the create-key form, the key's scope editor and both consent pages, so
    a key that arrives with all four ticked - every key with no scope, including one minted by
    approving a consent screen - is never decided on a page that does not mention the internet.
    The fact lives with the data: scopes.NETWORK_TOOLS is the external half of the catalogue,
    and a test holds it equal to the tool reference's network flag. (#103)

Changed

  • Docs. The Claude Code client page says what "Disabled for this project" means and how
    to clear it; the quickstart's paste-in prompt tells the agent the same. (#104)
  • One endpoint. /external/mcp is retired; the network tools are on /mcp. Adding
    leftbrain was two mcp add commands and, since OAuth, two sign-ins, for what every other
    layer - one key, one quota, one dashboard, one scope grid, one /docs/tools - already treated
    as one product. weather, fx_rate, geo and url_check are now published by the same
    server as the fourteen core tools, and the stdio leftbrain command carries them too.
    LEFTBRAIN_SERVE_EXTERNAL=0 (or --no-network) leaves the four out of a process; per-key
    control over the same four is the scope grid, which marks them (#103). A request to
    /external/mcp answers 410 with moved_to: "/mcp" and a message saying what happened,
    with no key required, so a client still configured with it is told rather than shown a bare
    404. The root document lists one endpoint and a network_tools flag. leftbrain-external
    remains for a process that should carry only the four. This flips the default from
    opt-in to opt-out for network access
    ; that was decided deliberately (#100).
  • Every parameter of fx_rate and url_check now carries its description on the wire.
    The pass that copies the reference's parameter docs onto the published schema only handled
    tools with modes; the two mode-less tools were never on the described server before. (#100)

Fixed

  • OAuth on /external/mcp. The 401 challenge named /mcp's protected-resource document
    whichever endpoint was asked for, and there was no document for the other mounts at all. A
    client that checks the document's resource against the URL it is connecting to - Claude
    Code does, as RFC 9728 requires - refused /external/mcp with "does not match expected"
    before any tool call. Every mounted endpoint now has its own document at
    /.well-known/oauth-protected-resource<endpoint>, and the challenge and the 401 body's
    how_to_authorize point at the one for the endpoint that was requested. A protected path
    that is not an MCP endpoint (/keys/me) is pointed at the core document. Static-key auth
    never read the challenge and is unchanged. (#101)
  • The 401 no longer sends an agent to a closed door. Its message said "get one at
    POST /keys/signup" whenever a key store existed; that route answers 404 unless
    LEFTBRAIN_OPEN_SIGNUP is on, which it is not on the hosted server. The clause now names
    signup only when it is open and says "sign in at /login to create one" otherwise. (#104)
  • A headless agent is told to register before the device grant. The 401's
    if_you_have_no_browser named /oauth/device_authorization alone, and calling it without a
    registered client answered a bare invalid_client. The 401 now names POST /register first,
    and the device endpoint's invalid_client carries an error_description that says the
    same and points at the agent guide. (#104)
  • A key minted through the device grant reads · device, not the approver's OS. The
    device flow exists so the approval can happen on a different machine from the agent, so the
    approving browser's operating system says nothing about where the agent runs - the same
    reasoning that already names a cloud client · web. (#104)
  • The site links to its repository again, and "feedback is off" says where else to go.
    feedback.py, the README and the report page all said the repository was private and that
    the feedback endpoint was therefore the only place to report a wrong answer. It is public, and
    on the hosted server the endpoint was not configured, so there was no route at all. The
    footer now links Source and Issues; the report page links the tracker whether or not filing
    is on; POST /feedback when off answers unsupported with the tracker in message and a
    tracker field; and the reason the endpoint exists is stated correctly - an agent mid-call
    holds a key, not a GitHub login. One source of truth: leftbrain.__repo__ (and
    Repository / Issues in pyproject.toml), overridden by LEFTBRAIN_FEEDBACK_REPO when a
    self-hoster files reports into their own fork. The Northflank guide names the two feedback
    variables. (#102)

v0.4.1

v0.4.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 22:03

Added

  • Number-theory predicates in math — is_even, is_odd, is_negative, is_positive,
    is_integer, is_square, is_perfect, is_prime, is_coprime. They live in the
    expression namespace so they compose, and they return booleans that coerce to 1 and 0, so
    is_prime(11) + is_prime(12) + is_prime(13) is 2 — which is the only batching math has,
    and is now documented rather than left to be rediscovered. A predicate applied to a free
    symbol names the symbol instead of answering None. (#57, #63)
  • Bounded ranges in numbers.sequence — kind=primes start=50 end=80 gives
    53, 59, 61, 67, 71, 73, 79. Same for squares and fibonacci. end is what asks for a
    range, so a start on its own is still ignored and still says so. (#57)
  • The physical constants in math — gravitational_constant … fine_structure_constant,
    each under a long name everywhere and its conventional short name (G, c, h, R) only
    in the modes whose answer is a number. c and G are the most common single-letter
    variables in algebra, so solve(["a + b = c"]) still treats c as an unknown, and a vars
    entry always wins. Values come from pint's CODATA registry, and every use is named in
    assumptions with its value and unit. (#55)
  • holidays modes festival, upcoming and compare. check and next are date-first;
    these are name-first and window-first — "when is Durga Puja", "what is coming up in October",
    "which dates does West Bengal observe that Assam does not". A multi-day festival comes back
    as one thing with its days named, not as unrelated rows sharing a prefix, and common names
    resolve to whatever the dataset calls the same festival (Durga Puja is filed as Dussehra)
    with the substitution stated. A name it does not carry is refused with the near misses,
    because "not in this dataset" and "no such festival" are different claims. (#87, #88, #94)
  • holidays.list can return iCalendar or CSV — format="ics" puts the calendar in a
    calendar, which is the thing people actually do with one. (#93)
  • Holiday names in other languages — language="hi", "bn", "ta" and eight more for
    India, discoverable through mode categories. (#76)
  • A festival can anchor date arithmetic — {"festival": "Saptami", "year": 2026, "region": "IN", "subdiv": "WB"} works wherever datetime takes a date, so "three days before Saptami"
    is one call rather than a lookup, some arithmetic and hoping both were right. (#92)
  • Somewhere to report a wrong answer. POST /feedback for agents holding a key and a form
    at /report for signed-in people, both filing an issue on the tracker nobody outside can
    see. Off unless LEFTBRAIN_FEEDBACK_TOKEN and LEFTBRAIN_FEEDBACK_REPO are set. Anything
    key-shaped is blanked before filing, and the reporter is recorded — a key prefix or a GitHub
    login — never their email. (#53)
  • Every published parameter now says what it is for, and mode lists what each tool can
    do. 0.4.0 gave them types; this gives them meanings, from the same text the documentation
    site already carries. (#64)

Fixed

  • 17 % 5 returned 0.85. % was read as a percentage wherever it appeared, so it became
    (17/100) 5 — wrong by a factor of 20, with % read as /100 as the only tell. Both are
    legitimate readings of %, so it now asks which was meant and takes
    percent="modulus"|"percent" as the answer, the way angle already works. mod is the
    spelling with one meaning and never asks. This also uncovered a precedence bug in mod
    itself
    : 2^10 mod 7 matched 10 as its left operand and answered 2^(10 mod 7) = 8
    instead of 1024 mod 7 = 2. (#97)
  • A SIP with no opening balance was refused. "growth and SIPs" is advertised at tool level,
    but principal was required — so "5,000 a month for ten years", which is what a SIP
    normally is, came back as 'principal' is required. The arithmetic was right all along.
    (#98)
  • A year the holiday source does not reach answered with an empty list. India's tables run
    1948–2100; asking for 2200 returned nothing, which reads exactly like "no holidays that
    year". It is refused now, naming the window. Every date also carries its provenance — source,
    calendar, covered years, language and categories. (#90)
  • holidays.check could not say that a date was a festival. is_holiday and is_weekend
    had to carry every question. It now answers with is_observed, day_off and
    observances[], each classified in one vocabulary that means the same thing in every
    country. is_holiday keeps its old meaning. (#89, #95)
  • A rejected math function named only what failed. primepi now comes back with the near
    misses and the accepted set, factorint returns real factors instead of a stringified dict,
    and the description no longer claims "ANY arithmetic" against a partial allowlist. (#63)

Changed

  • toolref moved out of leftbrain.web to leftbrain.toolref. It only ever needed
    contract and core, and leftbrain.web.__init__ imports starlette — a server extra an
    MCP-only install does not have. This is internal; nothing about the published tools changed.

v0.4.0

v0.4.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 20:14

Two batches of reported bugs, most of them the same shape: a parameter accepted and then
ignored, or a filter applied and never mentioned. Several answers that looked exact were
wrong, and this release makes each of them either right or refused.

If you read fields rather than prose, four response shapes moved:

  • geo_offline — coordinates is now zone_reference, because that is what it always held.
    tz_for_coords gained a coordinates echoing the point asked about, and a
    nearest_reference.
  • holidays.countries — 250 entries of {code, name, aliases} instead of ~500 bare strings
    mixing ISO-2, ISO-3 and the dataset's own abbreviations.
  • holidays.locale — renamed date_locale, which is what it does. locale still works.
  • numbers.allocate — exact_unrounded is a fraction (1000/7) where the decimal repeats.

And three behaviours changed on purpose:

  • The daily quota now counts tool calls, not HTTP requests. A handshake and a refused call
    cost nothing; a tools/call that did work costs exactly one.
  • collections.sort_by honours order when the key is given as keys. Anything relying on
    that silently sorting ascending will now sort descending, correctly.
  • math refuses trigonometry without an angle even where the expression folds to a
    constant, and datetime.recurrence refuses "every 2nd tuesday" rather than picking one of
    its two readings.

Added

  • OAuth 2.1 for /mcp, so ChatGPT plugins and Claude's web connectors can connect at all —
    neither will send a static key, and ChatGPT's dialog has no field for one. Discovery
    (RFC 8414, RFC 9728), Client ID Metadata Documents, dynamic client registration (RFC 7591),
    PKCE S256, and refresh-token rotation. Needs LEFTBRAIN_SECRET and LEFTBRAIN_BASE_URL;
    without both, nothing is mounted. lblz_ keys are unaffected — same responses, same quota
    headers, same tools/list filtering.
  • A consent screen at /oauth/consent, which is not decoration: leftbrain uses a static
    GitHub client id against a third party that sets a consent cookie, and now allows dynamic
    registration — every precondition of the confused-deputy attack the MCP guidance describes.
    Consent is recorded per client and checked first, and no signed state is set until a human has
    approved.
  • The device grant (RFC 8628) at /oauth/device_authorization and /device, for an agent on
    a machine with no browser: it shows its user a short code instead of asking them to paste a key
    into the conversation. No shipping MCP client drives this yet, so it is for agents calling the
    endpoints themselves; it will not make Claude Code connect.
  • A connector's key is an ordinary key. Approving creates one named after the app and where it
    runs — Claude Code · Windows, ChatGPT · web — visible on the dashboard, revealable,
    re-scopable and revocable, counting against the same cap. Reconnecting the same app reuses its
    key rather than consuming a second slot.
  • Per-tool call counts in the scope editor, so narrowing a key is a decision rather than a
    guess: a tool sitting at zero calls is one to untick.
  • POST /keys/me/scope, letting a caller propose narrowing its own key. It returns 202 and
    a URL for the owner to approve; nothing changes until they do. Widening is refused outright.
  • /docs/agents/auth, an authentication guide written for a model rather than a person, and
    linked from the 401 body and from RFC 9728's resource_documentation.

Added

  • holidays mode categories, mirroring subdivisions: the category values a country
    actually accepts. There is no single enum to publish — optional is valid for India and
    rejected outright for the United States — so the legal set has to be asked for per country
    rather than guessed at. (#75)

Fixed

  • Degrees were discarded the moment a trig argument contained pi. sin(pi) with
    angle="deg" returned 0 — the radian answer, as an exact integer, with nothing in
    assumptions to say the parameter had been dropped. SymPy folds sin(pi) to 0 while the
    expression is still being parsed, and the degree conversion ran afterwards, on a tree with no
    sin left in it to convert; a plain numeric argument survives parsing, which is why sin(30)
    was right the whole time. Degrees are now applied as the expression is built, so the fold
    happens on the already-converted argument: sin(pi) is sin(π°) ≈ 0.0548. cos(pi),
    sin(pi/2), tan(pi/4), cos(2·pi) and asin(1) were all wrong the same way. (#66)
  • The mandatory angle refusal was skipped for exactly those expressions. math refuses
    trigonometry without an angle rather than guessing between two readings that differ by a
    factor of 57 — but the check looked for trig in the parsed tree, so sin(pi) with no angle
    at all answered 0 instead of asking. Trigonometry is now detected in the source, which the
    parse cannot erase. (#69)
  • text.count ignored a substring it was given and answered a different question. "how
    many r in blueberry" reaches the tool as count(text="blueberry", substring="r"); substring
    is in that mode's own accepted-parameter list, was supplied, and was dropped without a word —
    the reply was a dozen unrelated summary counts, and whichever one a model then picked was the
    wrong answer. A needle with no what now counts that needle and says so; a needle given
    alongside a what that cannot read it is reported rather than dropped. The what refusal also
    omitted substring — the spelling that actually worked — from the values it listed. (#65)
  • numbers.sequence ignored start in silence. end, step and ratio were already
    reported when the kind did not read them; start was left off that list, so asking for the
    primes from 50 returned the primes from 2 with nothing said. (#56)
  • The daily quota billed HTTP requests, not tool calls — twice per call, and for calls it
    refused.
    Metering ran in the auth middleware, before the tool and once per request, so a
    hosted connector that re-initializes before each tools/call spent two units on every one, a
    connect spent four before any work, and a call refused for bad input had already been charged.
    Quota and rate limiting are now separate concerns: the per-minute limit still sees every
    request, because it is abuse protection, while a unit of the daily quota is spent once, after
    the tool has run, and only when it did work. X-RateLimit-Remaining-Today and
    meta.quota.remaining_today are the same number by construction, and both now count the call
    they ride on: an MCP POST holds its response start until there is a body to send with it,
    because a streamed reply would otherwise write its headers before the tool had run. (#62)
  • simplify removed a point from the domain without saying so. (x^2-1)/(x-1) came back as
    x + 1 with an empty assumptions list — but the input is undefined at x = 1 and x + 1 is
    2 there, so the two are not the same function. SymPy is behaving as documented; the silence was
    ours. Any answer that cancelled a factor now carries restrictions (["x != 1"]) and says in
    prose which points it gained. It covers every mode that can cancel, not just simplify —
    eval and factor do it too — and the denominators are read off an unevaluated parse,
    because x*(x-2)/(x-2) is already x by the time an evaluated tree exists. expand, which
    keeps the denominator, says nothing. (#68)
  • math.convert_form refused value, which the tool's own signature advertises. The mode
    re-renders a quantity, so value is the word a caller reaches for, and the flat schema lists it
    because stats takes one; it is an alias for expr here, and giving both different values is
    refused rather than silently picking one. (#67)
  • Every string argument was uncallable on five parameters, and latent on 27 more. A
    parameter annotated Any renders as anyOf: [{}, {"type": "null"}] — an empty schema with no
    type in it. An MCP client serialises its arguments against that schema, so with nothing to
    serialise against it emitted {"text": abc} unquoted: the call failed inside the client,
    never reached the server, and none of the contract's machinery applied — no envelope, no
    needs, and a retry produced the same malformed call. It made encode's headline purpose
    unreachable for any text containing a letter, and digit-only values happening to work is what
    made it look intermittent. All 32 now carry concrete unions; a test sweeps for the shape so it
    cannot come back, and another pins that "0.1" still arrives as the exact decimal string
    rather than a float. (#71)
  • collections.sort_by ignored order whenever the key was given as keys — which is the
    only way to sort on more than one field. "The top 3 departments by spend" took the first three
    rows of a silently ascending sort and returned the three lowest, in a well-formed response
    with nothing to contradict it. Anyone spot-checking with the singular key would have
    concluded order was fine. Mixed directions are now expressible too, as {field, order} or a
    -field prefix, and an order that cannot be applied is refused rather than dropped. (#84)
  • holidays applied a categories: ["public"] filter and never mentioned it. check
    returned is_holiday: false for 2026-10-18 in West Bengal — the middle of Durga Puja — so an
    agent would tell its user that October 18 is not a holiday there. Omitting subdiv already
    produced an assumption; this was the one narrowing that stayed hidden. The default is now
    named on every call that used it, and check reports a date the dataset knows under another
    category as a near miss rather than a bare false. (#72, and the visible half of #80)
  • geo_offline reported the zone's reference city as coordinates, whatever place was
    asked about: `...
Read more

v0.3.1

v0.3.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Aug 13:28

Added

  • Docs: connecting Claude on the web, and a ChatGPT section. Which clients can carry a bearer
    key and which cannot, and why: a config file can set a header, a browser connector dialog
    cannot. ChatGPT gets its own section covering the Plugins rename, the developer-mode
    prerequisite and where the toggle lives per plan, and why no configuration works directly —
    OpenAI's documentation states ChatGPT cannot present custom API keys, and expects OAuth 2.1
    against the MCP authorization spec. Both blocked clients get something to do rather than a
    shrug: the desktop app for Claude, a header-injecting proxy for ChatGPT, with a Cloudflare
    Worker to copy and the trade stated — the proxy's URL becomes the credential.

Changed

  • The per-key tool scope is a tree. It was a three-column grid of blocks whose columns ran
    to different heights, so a tool's modes wrapped into a shape unrelated to the tool above it.
    Each tool is now one full-width row with a disclosure triangle, a picked/total count, and
    its modes as collapsible children under a guide line. A tool with some but not all of its
    modes shows an indeterminate checkbox; unticking the last mode turns the tool off and
    ticking one turns it back on. The header offers All, None and Expand all. Counts
    are rendered by the server, so the scope is readable with JavaScript off.

Fixed

  • The loading skeleton now covers every page. site.js looked for .doc, then <main>,
    then document.body, so only the docs pages showed it correctly: the dashboard got the
    markup with no CSS to position or dim it, and the landing, login and error pages prepended
    skeleton lines above the nav. Every template now renders inside one #page wrapper, which
    the script targets and the stylesheet positions and dims. The docs pages still dim only the
    article, so the sidebar stays readable.
  • The site footer linked to the repository, which is private. It links to the tool reference
    instead.

v0.3.0

v0.3.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Aug 12:45

Added

  • /docs/tools answers a machine as well as a person. /docs/tools and
    /docs/tools/<name> now serve JSON to a client and HTML to a browser — the same content
    negotiation / already used, so there is one URL per thing rather than two. The index lists
    all eighteen tools (the four network ones included) with their modes and the contract's error
    codes; a tool gives every mode's purpose, description, parameters with type, default and
    required flag, and the example arguments. No key: discovery has to work before you have one.
    tools/list over /mcp remains the authenticated, scope-aware route. / advertises the new
    endpoint as tools.

  • Every response says what it cost (#28 §6): a meta block with tool, mode,
    latency_ms (wall time in the server), compute_ms (time in the engine), version,
    truncated — lifted out of the result so a caller reading a list knows it is not the whole
    list — plus request_id and quota when the server has them. X-Request-Id and
    X-Leftbrain-Latency-Ms carry the first two as headers; a caller-supplied X-Request-Id is
    kept so one id spans both sides of a trace. meta never affects ok, and compute_ms is
    the regression alarm for the compute ceiling: a response whose compute_ms exceeds its own
    deadline is a timeout that did not fire.

  • A 15-second ceiling enforced by killing the work (#28 §1 step 3). math, text,
    validate, collections and numbers calls now run in a worker process that can be
    terminated, because a thread cannot be: int.__pow__, sre and difflib are C loops that
    never reach a bytecode boundary, so no async exception, signal or thread kill is ever
    delivered — and the runaway holds the GIL, so the event loop and /healthz starve with it.
    That is how one math.eval 9^9^9^9 took the hosted instance offline for 35 minutes. A call
    that reaches the deadline returns timeout with stopped: "worker_terminated", the limit
    and the real elapsed time; a caller-supplied timeout is clamped to the ceiling; a burst
    that cannot get a worker returns the retryable busy rather than queueing behind a
    15-second wait. Workers carry RLIMIT_CPU and RLIMIT_AS as a kernel backstop, use
    forkserver on POSIX and are recycled every 200 calls to bound SymPy's caches. Configured
    with LEFTBRAIN_COMPUTE_TIMEOUT and friends (README). Needs pebble, added to the server
    extra; without it the server logs that isolation is off and runs in-process, and the library
    always runs in-process.

  • retryable on every failure, and four codes that say what was hit (#28 §1, §4): the
    envelope's failure half is now {ok: false, error, message, details?, retryable, hint?}.
    retryable is false for everything except busy and internal — a client that reads only
    ok: false retries, and an identical retry of a call that hit a limit multiplies the load that
    caused it. New codes alongside invalid_input / ambiguous / unsupported / timeout /
    forbidden / internal: too_large (a pre-check refused it before any work started),
    resource_exhausted (a memory or CPU limit rather than the clock) and busy (saturated;
    nothing was computed). contract.CODES is the one list, and TooLarge, ResourceExhausted
    and Busy are raisable from a tool. Only busy is retryable by default: internal is the
    catch-all for an exception no mode anticipated, and most of those are a deterministic
    consequence of the input, so a site that knows its own failure was transient passes
    retryable=True at the raise instead.

  • Layer-0 size caps: the enormous is refused before it is computed (#28 §2e/§2f/§2g). Every
    one of these reached the engine before; math.eval 9^9^9^9 took the hosted instance down for
    ~35 minutes. Each now costs microseconds and comes back as too_large naming the limit and the
    knob to turn:

    • math: the expression is walked as an AST and the answer's size estimated in log10 space
      before anything is evaluated — SymPy computes Integer ** Integer while parsing, so
      9^9^9^9, 2^(2^100), 2^1000000, factorial(factorial(20)), gamma(10**10) and
      exp(10^20) never start. The bound is sys.get_int_max_str_digits() (4 300 by default),
      which is the point beyond which CPython cannot render the integer at all. precision is
      capped at 5 000 digits and series order at 50 terms.
    • numbers: sequence caps the largest term at 1 000 digits (geometric ratio 2, n 10 000
      ended at 2^10000 and returned 15 MB); allocate caps parts at 10 000 (1 000 000 returned
      116 MB).
    • text: regex_replace refuses an output over 200 000 characters; diff caps each side at
      10 000 lines, words or characters (difflib is quadratic — 100 000 lines never returned).
    • collections: pivot refuses more than 200 distinct pivot columns.
    • datetime: business_days caps the range at 3 660 days; now and convert_tz cap the zone
      list at 50.
    • holidays: next caps n at 100.
    • A global 256 KB ceiling on any successful response backs all of them up, configurable with
      LEFTBRAIN_MAX_RESPONSE_BYTES. A mode with a knob should refuse earlier and say which knob;
      this only catches what slipped past one.

Fixed

  • Extreme magnitudes are rendered rather than lost or refused. Four behaviours for one
    class of input: numbers.parse "1e400" was invalid_input (Decimal holds it exactly —
    the parser's suffix regex simply never let it try); the same value as a JSON number
    arrived as inf and came back as a successful "Infinity" with nothing said;
    convert "1e-400" leaked OverflowError: int too large to convert to float; and
    finance.compound principal=1e300 was internal plus a bare InvalidOperation.
    Now: scientific notation parses however large, a value that arrived as infinity says its
    magnitude was already lost before the tool saw it, and convert computes exactly at any
    size — only value, which is a JSON number, saturates to Infinity/0, with the exact
    answer in the new value_exact and the loss named in warnings. An ordinary conversion
    gains neither field. An amount too big to be money is too_large, not a crash.

  • The small list (#28 §3.13): sixteen findings that were each wrong or silent rather than
    dangerous. random.sample with more groups than items left some empty and said nothing;
    random.float decimals=100 ignored the parameter (a binary float has ~17 significant digits,
    so it is now 0..15 and anything larger is refused); encode.base64 urlsafe=true decoded
    standard-alphabet text anyway; encode.url left a malformed %zz escape untouched with no
    note; validate.url accepted embedded credentials and punycode/Cyrillic look-alike hosts
    silently, and now warns about both; validate.ip rejected the scoped IPv6 form
    fe80::1%eth0 (RFC 4007), which is now valid with the zone reported;
    validate.sql_parse counted a trailing comment as a third statement;
    numbers.parse rejected the Unicode minus −5, which is what copy-paste from a document
    produces; collections.find_duplicates with case_insensitive did not also trim, so
    "a@x.com " and "A@x.com" were different; collections.aggregate had no median
    although math.stats did; datetime.recurrence refused every other tuesday, the short
    form people actually write; holidays.list for a year the calendar has no data for looked
    identical to a year with no holidays, and years past 2075 are now marked as estimated;
    text.sort sorted éclair after Zebra without saying it does no locale collation; and
    text.count now reports graphemes — what a reader sees, so a ZWJ family emoji is one
    character rather than five — and warns when text carries bidi controls or zero-width
    characters, which are invisible, count towards length, and are how a filename is made to
    read backwards.

  • Unknown parameters are refused instead of silently dropped (#28 §2a). Every tool kept the
    keys it recognised and discarded the rest, so datetime.age dob=… ref_date=… returned the age
    as of today (the parameter is on) and math.plot_points start=… end=… plotted the default
    range — an answer computed from defaults after the caller's arguments were thrown away, with
    nothing in the response to tell it from a right one. Each of the fourteen core tools now
    declares MODE_PARAMS, what every one of its modes reads, and a call carrying anything else is
    invalid_input naming the mode, listing what it accepts and suggesting the closest match.
    tests/test_mode_params.py derives the same map from the source and fails if the two drift.
    Retired names (from/to) now say what replaced them instead of being ignored. overlap also
    honours a per-interval tz, which it used to drop — two windows 12½ hours apart were compared
    as wall clocks and reported as overlapping by 8 hours.

  • Mutually exclusive parameters say which one won (#28 §2b): numbers.round significant=2 decimals=5 returned 120 with no word about decimals, and finance.emi months=12 years=5
    gave a one-year loan to a caller who described a five-year one. Both now record the choice in
    assumptions.

  • Eleven confidently wrong answers (#28 §3.3–3.13). None of these failed; each returned
    ok: true and a value a caller would act on. encode.json parse reported a valid document
    as invalid when it arrived through MCP already decoded (text is typed Any, so
    {"a": 1} became Python's {'a': 1} repr); encode.json stringify emitted literal
    Infinity and NaN, which no strict JSON parser accepts, and now refuses;
    datetime.parse turned 01/02/50 into 2050 with no note — there is now a stated pivot
    (49 and below is 20xx) recorded in assumptions, because for a date of birth a silent
    century is a hundred years wrong; datetime.recurrence with `FREQ=MINUTELY...

Read more

v0.2.0

v0.2.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Aug 06:46

Two new core tools — fourteen in all — and new modes on seven of the existing ones, plus a
dashboard that lets you delete old keys and a hosted free tier of 1,000 calls per key per day.
Everything is additive: every call that worked on 0.1.0 returns the same shape on 0.2.0.

Added

New tools

  • finance tool (13th core tool): emi (instalment, totals and amortisation schedule that
    reconciles to zero), compound (future value with optional per-period contributions and the
    effective annual rate), cagr, npv_irr (IRR by bisection), gst (inclusive/exclusive split
    into CGST/SGST or IGST, rounding difference reported) and percent (change vs percentage
    points, stacked vs additive discounts, exact bill splits). The rate's period and whether an
    amount is GST-inclusive are required, not guessed.
  • color tool (14th core tool): convert between hex (3/4/6/8-digit), RGB, HSL, HSV/HSB,
    naive CMYK and Lab with alpha preserved; describe names the nearest of the 148 CSS colours
    by CIE76 ΔE and words the colour from fixed HSL bands; swatch returns a real PNG (stdlib
    zlib, 16–256 px, one colour or two side by side); contrast gives the WCAG 2.x ratio,
    AA/AAA for normal and large text and the smallest lightness change that passes; mix
    blends in sRGB or Lab; harmony rotates hue for complementary, analogous, triadic and
    split-complementary sets; nearest snaps to a caller's palette with the runner-up;
    simulate shows deuteranopia, protanopia and tritanopia (Viénot–Brettel–Mollon 1999);
    grayscale greys by rec709, rec601, lab, average or hsl with an optional ramp and strip.
    Bare triples such as 58, 26, 241 are refused with needs.options (rgb, hsl, hsv).

New modes and parameters

  • datetime now takes a list as tz — zone names or {tz, label} objects — and answers with
    the shared instant as utc plus one full per-zone entry under zones, each carrying its
    label back. convert_tz accepts the same {tz, label} entries in to_tz. One round-trip
    for "what time is it in each of our offices right now"; the single-tz form is unchanged.
  • datetime free_slots: common free slots for two or more participants in different time
    zones, from weekly windows (09:00–17:00 on mon…fri) or one-off local ranges, intersected
    in UTC through zoneinfo. Each slot is shown in every participant's local time and in UTC,
    per_day totals the overlap per date, a window spanning a DST change is expanded to its real
    length with a note in assumptions, and no common time is ok: true with slots: [] and a
    warning naming who never overlaps.
  • convert fuel_economy: mpg_us / mpg_uk / km_per_l / l_per_100km with exact
    constants; a bare mpg is ambiguous (US or imperial gallon), and any conversion that crosses
    L/100 km states the inverse relation in assumptions.
  • convert cooking: cups, tbsp, tsp, ml, fl oz ↔ g, kg, oz, lb. Mass ↔ volume needs
    ingredient, looked up in a built-in density table (18 staples; the grams-per-cup used are
    stated); a missing or unknown ingredient returns the table as needs.options. cup selects the
    US (240 ml, default and declared), metric, UK or Australian (20 ml tablespoon) system.
  • convert sizes: category=shoe converts US men / US women / UK / EU / cm on a generic
    adult chart, snapping to the nearest half size with a warning; category=clothing maps XS–XXL
    to chest and waist cm bands for a chart chosen by region (us inch-based, eu EN 13402-3)
    and gender, both required. Every result warns that sizes are approximate and names the chart.
  • numbers semver: compare or sort version strings as versions (1.10 > 1.9), with
    SemVer 2.0 pre-release precedence, build metadata carried but ignored for ordering, and a
    leading v / missing minor or patch tolerated and recorded in assumptions.
  • text similarity: Levenshtein distance and a 0–1 ratio for a pair, or text + items to
    rank candidates and return the best match with its index — mapping typed input onto a menu,
    spotting near-duplicate names. Case-folded and whitespace-normalised by default, both stated.
  • collections: every record input (items, a/b, data) also accepts CSV text — the
    delimiter is sniffed, the header row detected (first row with no numeric, date or boolean cell;
    has_header overrides), each field typed as number (exact Decimal, via numbers.parse),
    ISO date, boolean or text, and blank rows and N/A/-/null cells skipped and counted, all
    stated in assumptions; above 5,000 rows the call is refused. New table modes over the same
    input: filter (where predicates with the validate.assert vocabulary, compared in the
    field's type), pivot (by × pivot_columns, one aggregate of column, row and grand
    totals, empty cells null), running (cumulative total, restarting per by), outliers
    (Tukey hinges, 1.5×IQR, fences reported, at least 4 values), summarize (count/nulls/sum/avg/
    min/max/median for every numeric field, distinct counts for text, range for dates) and to_csv
    (delimiter, chosen columns). A lone numeric field is assumed for running/outliers and
    said so; several are ambiguous. decimals rounds computed values only; row-shaped results
    echo at most 500 rows with a warning, except to_csv.
  • validate cidr: membership (network + value → contains, for an address or a smaller
    block) and overlap (a list of networks → every pair with equal / a_contains_b /
    b_contains_a / disjoint), plus a block's size, usable hosts, bounds and masks. Host bits
    set are read as the network and recorded in assumptions.
  • validate id with kind=isbn returns both isbn10 and isbn13 for a valid number (a 979
    book has no ISBN-10 form).
  • encode hash and checksum take expected and answer matches — compared in constant
    time against the hex, Base64 (or, for checksums, decimal) form, case- and whitespace-
    insensitive, with a sha256sum-style <digest> <file> line accepted as-is. A mismatch is
    ok: true, matches: false: an answer, not an error. hmac shares the same comparison.
  • files file_hash: sha256 (default) / sha1 / md5 / blake2b / crc32 and the other hashlib
    algorithms, streamed in 1 MiB chunks so a file's size is bounded only by LEFTBRAIN_FILE_ROOTS,
    with bytes, the algorithm used, and expected → matches for verifying a download.

Keys and dashboard

  • leftbrain-keys set --all --daily N [--rpm N] [--from-daily N] changes limits on every key —
    or, with --from-daily, only on keys still at an old default — so a moved default can be
    migrated without a database shell or touching keys set by hand. Prints how many keys changed.
  • Dashboard Delete for a revoked or expired key: removes the row and its usage history for
    good (with a confirm). A key that still works must be revoked first; deletion is owner- and
    CSRF-checked like the other key routes.

Fixed

  • Dashboard: keys issued before the server could show keys again (including the old
    POST /keys/signup rows, still named self-serve signup) are marked legacy and explained
    on the row instead of the bare "created before reveal was enabled". They no longer hold one of
    the owner's active-key slots, so a first sign-in that inherits three of them can still create a
    key. /keys/me output is unchanged.
  • Demo endpoint: the 8 KB body cap now applies to chunked requests too, and deeply nested arguments return a contract invalid_input instead of a bare 500.
  • Docker image build failed after the changelog was bundled into the wheel (CHANGELOG.md was not copied into the image).

Changed

  • Free tier is 1,000 calls per key per day (was 5,000); LEFTBRAIN_DEFAULT_DAILY_QUOTA still overrides it. Every place the site quotes the limit — landing, sign-in, quickstart, the demo's 429 — now reads the configured value instead of a hard-coded number.
  • Site shows loading skeletons on navigation, form submits and demo runs; the colour legend on docs pages was dropped.
  • Docs key picker shows the key name only (prefix only for unnamed keys).
  • Docs examples are labelled Request/Response and colour-coded: blue blocks are what you
    send, green blocks are what comes back, and a neutral block marks a setup command.

v0.1.0

v0.1.0 Pre-release
Pre-release

Choose a tag to compare

@Lazy-Coder-03 Lazy-Coder-03 released this 26 Aug 17:49

First public release, and a pre-release — the package ships as
Development Status :: 3 - Alpha. Tool names, parameters and the response envelope may
still change before 1.0, so pin an exact version if you build on it.

Added

  • 12 core tools, offline and free of API keys: math, datetime, scale, convert,
    holidays, numbers, text, collections, validate, random, geo_offline,
    encode. Each takes a mode, so a client pays for twelve tool descriptions rather than
    a hundred.
  • 4 external tools over key-less public APIs: weather (Open-Meteo), fx_rate (ECB
    reference rates via Frankfurter), geo (geocode, reverse, driving route), url_check.
  • Optional file tools for agents that cannot open files themselves: pdf_text,
    pdf_info, image_info, image_to_base64, base64_to_file, file_info, read_text,
    list_dir, restricted to the paths in LEFTBRAIN_FILE_ROOTS.
  • One response contract for every tool:
    {"ok": true, "result": …, "assumptions": [], "warnings": []}. Failures return
    ok: false with an error code and a message; an ambiguous input returns needs with
    the concrete options instead of guessing (IST, 03/04/2025, ton, KB). result is
    never null, and exact and decimal forms are returned together so nothing is re-rounded.
  • MCP servers over stdio: leftbrain, leftbrain-external, leftbrain-files.
  • MCP over Streamable HTTP: leftbrain-serve puts every tool set in one stateless
    process — /mcp, /external/mcp, optionally /files/mcp, plus GET /healthz and a
    JSON service description at /. Dockerfile and compose file included.
  • Use from Python without MCP: import leftbrain as lb gives lb.math_tool(...) and
    friends, and lb.TOOLS maps every tool name to its function.
  • Hosted service at https://leftbrain.idlesync.in:
    • GitHub sign-in; keys belong to the account's verified primary email.
    • API keys prefixed lblz_, up to 3 per account, 5,000 requests/day and 60/minute, with
      quota headers on every response and 429 plus Retry-After when a limit is reached.
    • Dashboard: create a named key (shown once), reveal an existing one again, revoke, and
      see today's usage.
    • Docs with Windows PowerShell / macOS / Linux tabs — quickstart, MCP client setup, a
      custom-agents guide, and a copy-paste prompt that configures any coding agent. A
      signed-in reader's own key is filled into every example on the page.
    • A per-tool reference generated from the MCP servers themselves: parameters come from
      the published schemas and every example is executed while the page is built, so the
      docs cannot drift from the code.
    • A key-less live demo on the landing page (numbers, convert, datetime, text).
  • Per-user key store on SQLite or Postgres, resolved from LEFTBRAIN_KEYS_URL,
    DATABASE_URL or LEFTBRAIN_KEYS_DB, with a leftbrain-keys admin CLI (create, list,
    enable, disable, revoke, set limits, usage, stats).
  • Key expiry. Keys are created with a lifetime — 30, 90 (default) or 365 days, or never
    (the dashboard and CLI warn that a key which never expires is a liability if leaked).
    An expired key is refused with 403 {"error": "expired", "message": "key expired on <date>; create a new one at /dashboard"},
    drops out of the 3-active cap, and can no longer be shown again. The dashboard shows
    when each key expires and flags one within 7 days; the docs key picker skips expired
    keys and reminds you when the selected one is about to expire. GET /keys/me reports
    expires_at and expired; leftbrain-keys create --expires 90d|never and
    set <prefix> --expires … manage it from the CLI. Existing keys are migrated with no
    expiry.
  • Deployment guide for a free Northflank sandbox in docs/deploy-northflank.md.

Changed

  • Range parameters use the name their domain already uses rather than from/to, which
    collided with the Python keyword and forced a from_ spelling on the wire:
    datetime diff, business_days and cron_next take start/end; math integrate
    takes lower/upper; math limit takes point; math convert_form takes form;
    geo_offline distance and external geo route take origin/destination. The from,
    from_ and bare to spellings are gone. from_unit/to_unit, from_qty/to_qty and
    from_tz/to_tz are unchanged.
  • The MCP wrappers accept every parameter their core tool supports, so a tool is not less
    capable over MCP than it is from Python.

Fixed

  • The GitHub OAuth callback handles malformed or unexpected responses instead of failing
    with a server error.
  • Docs :::os tab blocks are parsed fence-aware, so a ::: inside a code sample no longer
    ends the block early.
  • Static assets are stamped with a content hash, so a stylesheet or script change reaches
    browsers without waiting for a version bump.

Security

  • Security headers on every response: X-Content-Type-Options: nosniff,
    Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: DENY and a
    Content-Security-Policy with frame-ancestors 'none'.
  • X-Forwarded-For is counted from the right using LEFTBRAIN_TRUSTED_PROXY_HOPS
    (default 1), and X-Real-IP / CF-Connecting-IP are ignored — a caller cannot forge the
    address a rate limit is keyed on.
  • The demo endpoint runs behind an allow-list of tools, modes and argument names, an 8 KiB
    body cap and per-argument size caps, and never returns a traceback.
  • Keys are authenticated by comparing a SHA-256 hash. The revealable copy is
    Fernet-encrypted under a key derived from LEFTBRAIN_SECRET; leave that unset and only
    the hash is stored.
  • Anonymous signup is closed unless LEFTBRAIN_OPEN_SIGNUP=1. Sessions use a signed
    cookie, every mutation is CSRF-checked, and any page carrying a key is sent
    Cache-Control: no-store.