Skip to content

Suprnova v4.4.0

Choose a tag to compare

@eas4ai eas4ai released this 20 Oct 17:18
· 190 commits to main since this release

Added

  • Handlers become agent tools with #[tool]. A routed #[handler] marked
    #[tool] is a tool agents can call. #[tool_input] derives the input's
    JSON Schema (draft 2020-12) from the struct and its serde and validator
    attributes, so an agent is told what the server enforces. tool_scopes!
    declares scopes, and a ToolCaller argument tells the handler and its
    policies who is calling: a public guest, a user or an agent account.
    A tool call runs the route's middleware, validation, policies and rate
    limits, under per-caller and per-address budgets. An invalid or duplicate
    name, a missing description or an undeclared scope fails the build. See
    Agents.

  • An MCP endpoint serves the application's tools.
    Router::agents(AgentsConfig::new()) serves POST /mcp over MCP's
    Streamable HTTP transport, revision 2026-07-28, and to initialize-based
    clients of revisions 2025-06-18 and 2025-11-25, without sessions. It serves
    protected resource and authorization server metadata at the well-known
    locations under the application's root, lists every tool to a client
    without a token and only the tools a token may call to one with a token,
    runs public tools without a token, answers 401 and 403 with the scopes a
    call needs, so a client's first call of a tool that needs a token sends it
    to sign in, refuses a foreign Origin, and bounds the body, the answer
    and the run time. Structured results follow the session's revision: on
    2026-07-28 a tool's output schema and result pass through as declared, and
    on the two older revisions a non-object output schema is left out of
    tools/list and a non-object result comes back as text alone.

  • Users approve MCP clients through a built-in OAuth server.
    AgentsConfig::oauth_server(OAuthServer::new()) mounts the new Magnetar
    authorization server plugin: OAuth 2.1 with the authorization code grant and
    PKCE S256, clients identified by a client ID metadata document or dynamic
    registration, exact redirect URIs, tokens bound to the MCP endpoint and the
    approved scopes, refresh tokens rotated on every use, revocation, and tokens
    stored only as digests. Default approval and connected-agents pages ship,
    and ConnectedAgents lists and revokes a user's grants. Its issuer is
    APP_URL.

  • Tokens from an outside identity provider sign users in. OutsideIssuer,
    added with AgentsConfig::resolver, accepts the user tokens a provider you
    name signs, checked against its published keys for issuer, audience, expiry
    and scope. It finds the user by sub, by an id the issuer assigns
    (id_claim, such as Microsoft Entra's oid) or by a claim the token marks
    verified.

  • Agents hold accounts of their own under auth.md.
    AgentsConfig::agent_auth(AgentAuth::new()) serves auth.md v0.6.0 from the
    new Magnetar agent auth plugin: anonymous, service_auth and ID-JAG
    registration, the /auth.md document, a claim page with a six-digit code,
    revocation by token and by Security Event Token, registration rate limits
    and audit events. A claim dispatches AgentClaimed, delivered at least once
    with a stable event_id. The agents:key, agents:revoke-all and
    agents:prune commands print a signing key, revoke every agent credential
    and run the maintenance pass.

  • Live actions can be agent tools, in the browser and headless. #[tool]
    on a Live action offers it to agents, with fields marked #[agent_visible]
    as the result. The Live runtime registers a mounted component's tools with
    WebMCP. An action marked headless is also callable over MCP with no
    browser, through a sealed state handle bound to the caller; the Gate
    ability live:{component}.mount checks the mount properties an agent sends,
    and #[resolve(...)] fills a property from the caller instead.

  • Pages offer server tools to agents in the browser.
    Router::share_tools, with_tools on a route, a group, an Inertia response
    or a Live document, and PageTools::add choose the tools each page
    registers with WebMCP. A page lists only the tools its viewer may call, a
    page of public tools stays shared in RenderCache, and the page refreshes its
    data after a write tool. With WEBMCP_ORIGIN_TRIAL_TOKEN set, every HTML
    response carries the Origin-Trial header.

  • The React, Vue and Svelte kits register page and form tools. New
    projects call installPageTools(router) and ship usePageTool, for a tool
    that lives while its component is mounted, and useFormTool, which makes a
    form a tool that fills and submits it through Inertia. A destructive form is
    filled and left for the user to submit. suprnova generate-types writes
    each form request's input schema to frontend/src/types/tool-schemas.ts.
    The kits' core.ts, suprnova-webmcp and the Live runtime read a page's
    tools by the keys they know and ignore the rest, so a later release can add
    a field without breaking a scaffolded application. A change an older reader
    must not ignore raises the payload's v.

  • Rust front ends compiled to WebAssembly register WebMCP tools. The new
    suprnova-webmcp crate registers typed page tools, with the input schema
    the server derives, for as long as their handles live, and calls the
    application's server tools with the session and the CSRF token. Built for a
    target without a browser it compiles and answers unsupported.

  • Tests call tools as any caller. TestClient::tool and
    TestClient::live_tool call a tool as a page does, and TestClient::mcp
    calls the MCP endpoint as a guest, a user with chosen scopes or an agent,
    claimed or not, without an OAuth or auth.md ceremony. Agents::fake mints
    a token for a chosen caller, and TestResponse::page_tools reads the tools
    a page offers.

  • Test requests can come from an address and carry any body.
    TestClient::with_peer_address sends requests as if from that TCP peer, so
    Request::ip() and limits keyed by the client address see it.
    TestRequest::body sends a body of any content type as it is.

  • Applications build their own consent pages on AgentApprovals and
    AgentClaims.
    AgentApprovals::of(&request) reads the signed-in user's
    pending authorization as a PendingApproval and records the decision as an
    ApprovalDecision. AgentClaims::of(&request) reads a PendingClaim and
    confirms it with the user's code as a ClaimConfirmation. Both answer 403
    to a tool call, so an agent never approves or claims for the user it acts
    as. The framework's default pages use them too. ConsentPageHeaders
    refuses framing with frame-ancestors 'none', and sets no-store and
    no-referrer, on every answer of a consent route, error pages included,
    and keeps the application's nonce policy.

  • url::same_origin_path checks a return address, and
    Redirect::intended_url reads the intended one.
    same_origin_path
    accepts only a path that starts with a single / and holds no control
    byte, so a sign-in page checks a return_to with it before
    Redirect::set_intended_url stores it. Redirect::intended_url() returns
    the address Redirect::intended would follow, without spending it, as
    Laravel's getIntendedUrl does.

  • An application's migrator can create the agents tables and a
    case-insensitive email index.
    suprnova::agents::migrations() creates
    both plugins' tables, only what is missing. Its rollback leaves them in
    place, and its documentation gives the statements that drop them.
    suprnova::auth::migrations::UniqueEmailIgnoringCase::on(table, column)
    adds a unique index on LOWER(column) on SQLite, PostgreSQL and MySQL
    8.0.13 or later. On MariaDB and older MySQL it checks that the column has a
    unique index of its own and a _ci collation. It refuses a table that
    already holds addresses that differ only in case, and its index name fits
    every engine's identifier limit.

  • Tests build a router with agent auth without a signing key.
    suprnova::testing::install_test_agent_auth_key() gives the test process a
    generated key when the environment names none.

  • #[tool_output] and #[tool_schema] derive schemas through the
    framework.
    #[tool_output] marks a tool's output type, any struct or
    enum, and derives Serialize and JsonSchema with no serde or
    schemars dependency of the application's own. #[tool_schema] gives a
    type nested in a tool's input or output its schema, and
    suprnova_webmcp::tool_schema does the same for a page tool's input.

  • Tools can carry WebMCP's debugging hint.
    #[tool(annotations(debugging))] on a server tool or a Live action, the
    debugging option of the kits' usePageTool and useFormTool, and
    PageTool::debugging mark a tool meant for debugging and developer
    tooling rather than end users. The MCP endpoint does not send it.

  • New projects serve agents. suprnova new writes a fresh agent auth
    signing key into .env, readable by its owner only (mode 0600), and
    .env.example names both variables with no value. Every kit serves the
    MCP endpoint, the OAuth server and agent auth from src/agents.rs, and its
    migrator creates both plugins' tables. The Inertia and Live kits' migrator
    also creates a case-insensitive unique email index, and their sign-in sends
    a signed-in user who opens a sign-in link on to a same-origin return_to
    and keeps the intended address through sign-in and registration. Every kit
    folds an email address to ASCII lowercase before validating it, and bounds
    names at 255 characters and addresses at 254.

  • The agents maintenance pass prunes the OAuth server's ended rows. The
    five-minute pass in serve and agents:prune delete lapsed pending
    authorizations and codes, expired tokens, and grants revoked longer ago
    than the retention, whenever the router mounts the OAuth server, with agent
    auth or without. A live grant or token is never pruned. agents:prune
    needs agent auth only for --retry-failed.

  • The Inertia and Live kits ship their own approval, claim and
    connected-agents pages.
    The React, Vue and Svelte kits render
    Agents/Approve, Agents/Claim and Agents/Connected at
    /oauth/approve, /agent/claim and /connected-agents on the consent
    facade, and an approval leaves for the client by a location visit. The
    Live kit serves the same three pages as plain server-rendered documents
    with native forms, and an approval leaves through a short page whose meta
    refresh and link go to the client, which form-action 'self' allows. Every
    route runs ConsentPageHeaders, no consent page offers a tool, and text a
    client, an agent or the user chose sits in <bdi>. See
    Starter Kits.

  • The API kit serves agents, with a sign-in for consent only. A new API
    project serves the MCP endpoint and both plugins with the framework's
    default pages. A browser signs in at /login only to approve a client or
    claim an agent: the session runs on the consent pages alone, ends after
    15 minutes idle, keeps its cookie for the browser session, and ends at
    POST /logout. GET /api/connected-agents lists the user's connected
    clients and agents, and DELETE /api/connected-agents/clients/{grant} and
    /agents/{registration} revoke one. Registration mails a link that
    verifies the address, which a claim needs, and
    POST /api/email/verification-notification mails a new one.

  • Auth::email_verification() verifies an address for its signed-in
    owner.
    issue mints a single-use link token for the signed-in user, and
    issue_for one for an account the request just registered. The
    application mails the link. verify verifies the address only for the
    user signed in where the link opens, only while the account still holds
    the address the link was mailed to, and answers Verified,
    AlreadyVerified, OtherAccount, AddressChanged, Expired, Unknown or
    SignIn. Anyone can register another person's address with a password of
    their own, so a link that verified whoever opened it would verify that
    account. Verifying changes no credential and starts no session.

  • Consent pages refuse a form tool's submission. A kit's form tool sends
    the form it submits with the Suprnova-Form-Tool header, which
    suprnova::agents::FORM_TOOL_HEADER names. AgentApprovals::of and
    AgentClaims::of refuse it with 403, as they refuse a tool call, so a form
    tool on an approval or claim page cannot approve or claim.

  • New projects ship two notes tools. The Inertia and Live kits serve
    search_notes at GET /notes/search and create_note at
    POST /notes/tools, under the notes:read and notes:write scopes, each
    acting on the caller's own notes. The notes page offers both to an agent in
    the browser.

  • The Live input takes maxlength. The component library's input sets
    the attribute only when a caller passes one. The Live kit's account forms
    bound the name at 255 characters and the address at 254.

  • #[tool] warns over WebMCP's budgets. The build warns when a tool's
    name passes 30 characters, when its description passes 500, and when an
    input field has no description, and boot logs the same. The warnings never
    refuse a tool. A tool that keeps a long name on purpose sets
    budget = ignore in its marking, which quiets the build warning for that
    tool alone; boot still logs its budget warnings.

  • Live tools describe their arguments. arguments(amount = "How much to add.") on a Live tool marking gives each action parameter the description
    an agent reads; an argument name the action does not have fails the build.
    A mount parameter takes a doc comment.

  • The Inertia frontends pin webmcp-types. The React, Vue and Svelte
    kits' browser helpers use the community's webmcp-types 0.1.10 definitions
    for the model context, tool definitions and tool results.

Changed

  • Tool names are identifiers. A tool name is 1 to 64 ASCII letters,
    digits and underscores, so every model's tool calling accepts it; a name
    with a dot or a hyphen fails the build. The shipped tools are renamed verb
    first: posts.create is create_post, notes.search is search_notes.
    A browser instance of a Live tool joins the tool name and the instance key
    with a hyphen, and the whole fits in 64 characters: a root mount that does
    not fit fails at declaration, and the Live checker reports an island that
    does not. An application with dotted tool names renames them.
  • MagnetarLifecycleEvent and LifecycleEvent carry an attributes
    map.
    Agent auth events record the registration, the client address and
    the fields auth.md names for each kind in it; it is empty for the other
    kinds. Code that builds either struct field by field, or destructures it
    without .., no longer compiles.
  • Three public enums have new variants. LifecycleEventKind gains the
    nine agent auth audit kinds, from AgentRegistrationCreated to
    AgentRegistrationRevoked. CryptPurpose gains LiveAgentHandle and
    HandlerArgKind gains Context. A match over one of them without a
    wildcard arm no longer compiles.
  • The agents feature is on by default. An application that keeps the
    framework's default features compiles the agent tools, the OAuth server and
    agent auth, and Magnetar's migrate creates their eight tables when it next
    runs. Nothing is served until the router calls Router::agents.
  • generate-types and serve write frontend/src/types/tool-schemas.ts.
    The file holds the input schema of each form request, or an empty list,
    beside the Inertia props types. Each run brings it up to date and names
    each request it cannot describe exactly as the server does.
  • Redirect::intended follows only a path, an http or https URL, or a
    relative reference.
    It ignores a stored value with another scheme, such
    as javascript:, mailto: or localhost:3000/x, and one with surrounding
    whitespace or a backslash anywhere, and redirects to its fallback instead.
    A browser trims whitespace and reads \ as /, so such a value could
    leave the application. See
    Storing the intended URL.
  • An Inertia error page keeps a no-store the replaced response carried.
    The error page set Cache-Control: no-cache, private on every page it
    rendered. It now sets no-store when the response it replaces said
    no-store, so a page that must not be stored stays unstored when it fails.
  • Magnetar's auth_tokens table gains an address_digest column. A
    token that verifies an email address keeps the SHA-256 of the address it
    was mailed to. Magnetar's migrate adds the nullable column the first time
    it runs.
  • A custom Magnetar engine, store or token binding refuses email
    verification until it implements the new methods.

    MagnetarPasswordAuthEngine gains email_verification_issue and
    email_verification_verify, TokenStore gains issue_bound and
    inspect, and UserStore gains mark_email_verified_for. TokenFields
    gains read_address_digest and write_address_digest. Each has a default,
    so existing code compiles, and the defaults refuse, so
    Auth::email_verification() fails closed until they are implemented.

Fixed

  • Concurrent writes no longer fail on MariaDB 11.8 and later. These
    releases turn innodb_snapshot_isolation on by default, under which a
    transaction that updates a row another transaction changed after its
    first read fails with error 1020, "Record has changed since last read",
    where MySQL waits and works on the latest row. Two overlapping writes to
    one table failed one of them with a 500, because each advances the
    RenderCache ledger, and two requests spending one email verification or
    password reset link at once could fail the losing request with a database
    error instead of answering that the link was used. Every connection the
    framework opens to MySQL or MariaDB now runs
    SET SESSION innodb_snapshot_isolation = OFF once, when it opens. A
    server without the variable, MySQL included, refuses it as unknown and
    the connection opens as before. The statement never fails a connection:
    any other refusal, as a MySQL-compatible proxy or service may give, is
    logged once per pool at warn, and the connection opens with the
    server's own setting. See
    MySQL semantics on MariaDB.
  • DB::transaction_with_attempts retries by the database's error code.
    It re-runs an attempt the database refused with MySQL or MariaDB error
    1213, 1205 or 1020, or Postgres SQLSTATE 40001 or 40P01, and returns any
    other database error at once, even one whose message mentions a
    deadlock. It used to match the word "deadlock" in the message, so a lock
    wait timeout and MariaDB's error 1020 were never retried. An error with no
    database code is still matched by its message.
  • A Postgres schema dump loads where an administrator installed an
    extension.
    schema:dump copied the comment an extension sets on
    itself, such as COMMENT ON EXTENSION vector. Loading that dump as the
    application's role failed with "must be owner of extension", because the
    extension belongs to the role that installed it. The dump now leaves those
    comments out, and CREATE EXTENSION sets them again.
  • Magnetar's tables hold long values on MySQL and MariaDB. Byte
    columns were limited to 255 bytes. These hold passkey and OAuth sign-in
    steps, two-factor secrets and recovery codes, and saved provider tokens.
    So were the session user agent and the passkey credential id, public key
    and provider scope columns. A longer value failed to save, so passkey,
    two-factor and social sign-in could fail there. Magnetar's migrate
    widens the existing columns the first time it runs. It keeps each
    column's character set and collation, and leaves a column that an index
    covers alone, with a warning.
  • Two servers migrating Magnetar's tables at once no longer collide. A
    table, index, column or marker that another process created first counts
    as created.
  • Registration answers a validation failure with 422.
    Auth::password().register answered a weak password or an unusable
    address with 401. It now answers the framework's validation error, with
    the message under the field.
  • Email addresses longer than 254 characters never reach the database.
    Sign-in with one answers as an unknown address does. Sign-up, the account
    import and OAuth email completion refuse one as a validation error. Such
    an address is still counted toward lockouts. An account that already
    holds one can no longer sign in by email; migrate logs how many exist,
    once per database.
  • Session metadata stays within its columns. A session keeps at most
    1,024 characters of the user agent. A lockout records the client address
    only when it parses as an IP address.
  • New projects get every default feature of the framework. A new
    project's Cargo.toml turns the framework's default features off and lists
    the ones it keeps. The list lacked search-media and agents, so a new
    project had neither media search nor agent tools. It now lists both.
  • A form request on GET or HEAD reads its query. As Laravel validates the
    query of a GET, a form request on a GET or HEAD request without a body
    reads its query string as it reads a url-encoded body: it decodes,
    validates and names its fields as request.query_into() does. Such a
    request was refused with 415. A request with a body still reads the body,
    and other methods are unchanged.
  • Precognition and the real request read a query alike. A query value
    holding a JSON object reads as that object where a struct or a map belongs,
    and stays text everywhere else, in Precognition, a GET form request and
    request.query_into(). Precognition decoded the object wherever it stood,
    so a route could pass Precognition and fail the real submission.
  • An Option Live mount parameter can be left out. Every mount parameter
    was registered as required, and no codec decodes null, so an Option<T>
    parameter could never be None. It is now optional: left out or null, it
    mounts as None.
  • A page's scripts and styles no longer become the previous URL. The
    session recorded any successful GET as the previous URL: a script or
    stylesheet that StaticFiles served, a request a global middleware
    answered before any route matched, and an AJAX request. Without a
    Referer, Redirect::back() could then send the user to a .js file. As
    in Laravel, the session now records a GET only when a route answered it,
    the fallback route included, and skips a request that sends
    X-Requested-With: XMLHttpRequest. A file that StaticFiles serves is
    never recorded.
  • Two uses of one email verification link at once no longer answer 500 on
    MariaDB.
    MariaDB 11.8 and later end a transaction that writes a row a
    concurrent one changed, so the second of two concurrent uses of one
    Magnetar verification token answered 500. The losing use now reads the
    token as already used, on every engine. A consume the database ends in a
    deadlock or a serialization conflict runs again, up to three times, and
    answers a conflict rather than an internal error if every run conflicts.
  • Pages keep their styles under the Vite dev server. In development,
    SecurityHeaders allowed the dev server in script-src and connect-src
    only, so the browser refused the stylesheet link the framework puts in the
    head, and a page under the balanced preset rendered with no styles. The
    dev server is now allowed in style-src, font-src and img-src as well.
  • A panic the panic hook reports carries its request. A panic raised
    inside a request or job scope but outside the framework's own catch
    reached the tracker without the scope's request id, request, route and
    user, which an error reported in the same scope carries. It now carries
    them. The hook never waits on a lock the panicking thread may still hold:
    a field it cannot read at once is left out, and if the request's auth
    state is locked, the event names no user rather than a wrong one.
  • A panic reported while another report resolves its stack is kept. The
    panic hook gave up the whole event when the symbol resolver was busy, so
    the one event a reporter exists for could vanish and only a counter said
    so. The hook now sends the event without frames and says so in its
    payload; the message, location, request and release all arrive.