Skip to content

Releases: DonaldMurillo/gofastr

v0.88.2

Choose a tag to compare

@github-actions github-actions released this 17 Oct 01:27
9ed623e

Fixed

  • A page can no longer freeze while a navigation waits for its
    sheets.
    On a page where the fullbundle module left a
    data-cui-bundle-href marker in place (markup carrying the attribute
    in its content, or a module from another release), every navigation
    waited for the module, found the marker again and asked again, in a
    loop that starved the page's timers and input. The wait now asks for
    the module once. It is also bounded at 3 s, like a sheet, so a
    fullbundle.js fetch that never answers no longer holds every
    navigation and the scroll settle for good.
  • A cached or prefetched screen honours its loading region. Since
    the sheet wait, a loading region whose data-cui-after timer fired
    during the wait was cut off by the commit on a cached replay or a
    prefetched screen, with no data-cui-min hold and no exit animation.
    Both now hold and exit as a fetched page does.
  • Clicking A, B, then A again no longer fetches A a third time.
    When the first A's late answer came back, its cleanup dropped the
    in-flight entry the second A owned, so a further navigation to A
    fetched again and replaced the live one.
  • A tap on a control below an open popup reaches that control. A
    dismissable disclosure closed on the touch release, before the
    browser aimed the tap's click, so an in-flow panel collapsed and the
    click landed on whatever moved under the finger. A tap on a link,
    button, field, label or summary now closes it through that click; a
    tap on plain content still closes it on the release.
  • An ordinary navigation drops a record pane still loading. A pane
    waiting for its sheets when a plain navigation started could still
    open and push its record URL, so the page that then loaded sat under
    the record's URL. A pane that came back declined or failed fell back to
    a plain navigation to the record, replacing the newer page. The pane
    is now dropped once any navigation starts, whatever its answer.
  • The docs site's phone menu scrolls on a short screen. On a phone
    held sideways the open menu ran past the bottom of the viewport and
    its last links could not be reached. It now stops at the viewport
    edge and scrolls.
  • The docs site's phone menu rows are full tap targets. Each link
    in the open menu was a 28px row of 13px text, short of the 44px touch
    target and smaller than the page's body text. The rows are now 44px
    tall at the body size, and so is the menu's search field text. The
    open menu also sits above a page's floating section pill, which
    otherwise covered its last row.
  • Page switches on a static export take one request, not two. An
    exported page lives at <path>/index.html, which GitHub Pages, S3,
    Netlify and Cloudflare Pages serve at <path>/ and answer <path>
    with a 301. Every link was written as <path>, so each click paid a
    redirect round trip before the page arrived. Links to exported pages
    now carry the trailing slash; on a throttled mobile connection a
    docs-site switch went from about 465 ms to about 315 ms. An installed
    PWA's service worker keys its precached pages the same way, so a
    client-side switch still finds them offline, and a hidden widget's
    chrome (a section menu opened from the header) and the PWA offline
    page link them the same way too. A link the export does not write, such as a search result,
    still lands on the page, and the current-page highlight now treats
    /about and /about/ as one page.
  • A static export's sitemap names its base path once. A site whose
    SitemapConfig.BaseURL already ends in the export's base path
    (https://donaldmurillo.github.io/gofastr with --export-base /gofastr) listed /gofastr/gofastr/... URLs in sitemap.xml and
    robots.txt, and every one of them 404ed. The base path is now added
    only when the URL does not already end in it, and each page URL
    carries the trailing slash the export's links do.

v0.88.1

Choose a tag to compare

@github-actions github-actions released this 16 Oct 17:00
810bcff

Fixed

  • A client navigation no longer paints the new page unstyled. Since
    v0.88.0 each page links only the component sheets it renders, so
    nearly every navigation brings markup whose sheet the current page
    never loaded. The runtime swapped first and linked the sheet after,
    and on a phone with a slow connection every page change flashed
    unstyled before the styles arrived. The navigator now scans the
    incoming markup, links its sheets while the old page is still on
    screen, and commits once they load. It waits only on the sheets that
    markup renders, never on an unrelated one still in flight, and each
    sheet is bounded at 3 s so a stalled fetch delays one navigation and
    never freezes the next. The page title, the screen-reader route
    announcement and a loading region's Min hold and exit animation
    all run after the wait. This covers every swap path: fetched
    partials, whole documents, cross-layout swaps, cached and prefetched
    screens, envelope fills, intercepted panes, and a page that has not
    yet linked its full bundle after a first-paint subset. A deferred
    part lands only once its own sheets have loaded. A hover prefetch
    starts its page's sheets when it lands, so the click usually finds
    them loaded; visible and eager prefetches leave them for the click.
    An envelope module served beside an older cached kernel still
    navigates, without the wait. The core runtime grows 200 bytes at
    gzip level 1 (15556 → 15756).
  • Going back to a page whose load is still in flight works. Click
    A, then B, then A again before either answers: the runtime joined
    the third click to the first A request, which B had already
    superseded, so B rendered under the URL /a. A navigation now joins
    only an in-flight request that is still the live navigation.
  • Headless disclosures light-dismiss on a touch. A disclosure with
    Dismiss also closes on the release of a touch or pen press outside
    it. iOS Safari sends no click for a tap on content that is not
    clickable, so a tap on the page left the popup open.
  • The docs site's phone menu closes on a tap outside it, and its
    close button stays reachable.
    The panel hung from the top of the
    viewport, so on the static export, where a preview banner sits above
    the bar, the open panel covered the bar and its toggle. It now hangs
    from the bar.

v0.88.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 13:13
1b56117

BREAKING. v0.88.0 stops first paint waiting on the runtime, and a
live page whose head sheets fit 16 KiB gzipped carries them in its
first response (a heavier page inlines the rules its markup can match
when those fit; static exports and a policy that cannot take hashes
keep the links).
It adds browser-local data (core-ui/localdb,
framework/localentity), site search (battery/sitesearch), static
export (gofastr export) and the app binary as an OS service
(battery/service, experimental), and fixes the defects of the
regression audit. Four changes break callers:
ui.ContentRowConfig.BelowHeader and gallery.ContributeCSS /
BaseCSS are removed, live pages read the route graph from
/__gofastr/manifest.js, and core/config enforces validate: tags.
Run gofastr upgrade --to v0.88.0 before you bump go.mod: it points
at each line of an app that needs an edit.

Added

  • battery/service: the app binary as a per-user OS service
    (experimental).
    service.New(Config{ID, Name, Port}) and
    svc.Run(ctx, os.Args[1:], build) give a binary the verbs
    service install|uninstall|start|stop|restart|status|logs|open|mcp|run|rotate-secret.
    install copies the binary to a managed path and registers it as a
    launchd agent (macOS), a systemctl --user unit (Linux) or a
    logon-triggered Task Scheduler task (Windows), then starts it on the
    fixed loopback port; a held port fails the start instead of moving.
    Every request passes a Host pin, a cross-site refusal and a signed
    session cookie, which open mints by handing the browser a 0600
    local page that POSTs a 60-second single-use token. CLI clients reach
    the daemon over a Unix socket in the 0700 state directory, checked
    against the daemon's OS user by peer credentials. A signed session is
    the local user, so owner-scoped entities work without battery/auth;
    with auth linked, auth keeps identity. Agents reach the App's MCP tools
    through service mcp, a stdio server tunnelled over that socket and
    run as the same local user; Runtime.MCPCommand() gives the argv with
    --state-dir, so it reaches the right daemon from any directory. The
    dev run keeps its own state directory and port (one per isolated
    worktree), and gofastr dev runs it gated on the address it passes. stop, reinstall and
    uninstall wait for the old process to exit, and an instance lock
    keeps one process per state directory. Windows has no crash restart,
    and its backend is unit-tested but has not run on a real Windows
    machine. Guide: gofastr docs service; example:
    examples/gofastr-runs, which lists the machine's running gofastr
    apps and serves the list to agents.
  • Running Apps record themselves in a local run registry. App.Start
    writes a 0600 JSON record (name, pid, URL, MCP path or command,
    service id, dev flag) into <UserConfigDir>/gofastr/runs once the
    port is bound and removes it when the server stops.
    framework/localrun.List returns the live runs and deletes the
    records of dead pids. GOFASTR_RUNS=off opts a process out,
    GOFASTR_RUNS_DIR moves the directory, and test binaries, and the
    apps they start, skip it unless the directory is set. Guide:
    gofastr docs local-runs.
  • App.WrapServer wraps everything the listener serves. Router
    middleware (Use) never sees the interactive setup wizard or the
    worker and agent roles' reduced muxes; a WrapServer wrapper does.
    The first one registered is outermost, and a call after Start has
    bound the server, or after Shutdown, panics.
  • ui.BoxConfig.Bounded caps a Box at the --ui-box-bounded-size knob
    (20rem) and scrolls what overflows: a window for a long list in a panel,
    or for previewing a viewport-height component such as ui.Workbench.
  • ui.TerminalErr marks a failure line in a ui.TerminalBlock
    (danger tone, --ui-terminal-block-err-color knob), beside
    TerminalOut and TerminalOK.
  • HeroConfig.Lede (inline markup in place of Subtitle) and
    HeroConfig.Footer (content under the actions: an install line,
    a byline, a stats line).
  • SectionConfig.Band gives a section without an eyebrow the
    page-band treatment (display-size heading, more air under the head).
  • ui.StackConfig.StickyHeader pins the page frame's <header> and
    publishes its height (--size-header-height) as --ui-sticky-top.
    Every top-sticky part below reads it, at any layer depth: a Sticky
    ContentRow's nav column and toolbar, ui.StepRail, the
    interactive.SectionMenu rail, ui.TableOfContents,
    ui.AnchoredRail, ui.GlobalSearch, ui.NetworkRetryBanner, a
    top-edge ui.Sticky, a FormFrame side panel and an anchored
    ui.Section stop under the header instead of sliding behind it. A
    region that scrolls on its own (a bounded ui.Box, the pinned nav
    column, a ui.ListDetail list, the PaneHost drawer, the Workbench
    rail) resets the offset to 0. The layout declares the sticky header
    once; no component takes a flag for it.
  • SectionMenuConfig.Filter puts a filter box above an
    interactive.SectionMenu's list, in the rail and in the phone sheet.
    Typing hides the links that don't match, marks the match, shows
    "matches/total" in each group header and says FilterEmpty when
    nothing matches; Escape clears it. FilterPlaceholder sets the
    prompt. InlineTrigger keeps the phone pill in flow, for a menu shown
    inside a card or a demo.
  • SectionMenuConfig.SwapAt moves the width where an
    interactive.SectionMenu trades its rail for its phone pill
    from 900px to a ui.ContentRow breakpoint (interactive.SwapMD,
    48rem, or interactive.SwapLG, 64rem), so a menu in a row's nav
    column swaps where the row stacks.
  • ui.TOCConfig.PhoneSheet folds a contents list into a bottom sheet
    on phones.
    Below Breakpoint (md by default) a pill floating at the
    bottom of the screen names the section in view and opens the list;
    at and above it the list renders as before. The sheet is a native
    popover rendered with the page: it opens without script, closes on
    Escape, a tap outside, its close button or a followed link, and with
    script moves focus to the current entry when it opens.
    ui.ContentRowConfig.PhoneAsideFlush drops the stacked aside's rule
    and padding so an aside holding only that list leaves no band.
    The pieces are public: interactive.SheetPill (the pill SectionMenu
    uses too) and interactive.PhoneSheet (pill plus inline sheet for any
    list that marks its current entry aria-current).
    ui.AnchoredRailConfig.PhoneSheet folds a rail the same way (below
    SwapAt, 900px by default); its pill names the section by the link's
    text, without the parts marked interactive.SheetChipClass (the
    rail marks its number and count chips). PhoneSheetTop on
    either (interactive.PhoneSheetConfig.Top) pins that pill as an "On
    this page:
    " bar under the sticky header and hangs the list
    from the bar, for a page whose SectionMenu pill already holds the
    bottom edge. In a ContentRow aside the bar's aside moves above the
    main column on a phone and sticks there. Anchored sections and the
    rail's in-view band clear the bar through --ui-sheet-bar-offset.
  • entityui lists warn when their row links lead nowhere. The first
    time a list renders rows whose <base>/<id> matches no registered
    screen, it logs one warning per base naming the route to register.
    Until now every row link 404ed with nothing in the log. The check runs
    through App.EntityUI with a mounted uihost (UIHost.HasScreen);
    a custom host opts in through entityui.ScreenHost. NoLinks() lists
    skip it, and it remembers at most 256 bases (#503).
  • The startup banner says how to get past the 401s. When an entity
    requires auth and nothing on the app signs callers in, the banner
    prints one line: wire battery/auth, or set Exposure.Public.
    battery/auth calls the new App.NoteAuthenticator() once Init has
    run and a middleware that resolves callers exists
    (SessionMiddleware(mgr), mgr.Middleware(), or
    RequireAuth(mgr.JWT())), so an Init with no middleware keeps the
    hint up. battery/desktop calls it when its window identity takes
    effect; a custom auth layer or a TokenMiddleware-only app calls it
    too, and App.AuthenticatorNoted() reads it back (#503).
  • An unknown ?include= names the relation it was close to.
    ?include=issue on an entity whose relation is issue_id now answers
    did you mean "issue_id"?. Only a near miss (the _id form, the
    foreign key, or the target entity) gets a suggestion; the error never
    lists an entity's relations (#503).
  • gofastr init scaffolds sign-in. The sample app shipped CRUD
    that answered 401 with no way to sign in. It now wires battery/auth
    (users and sessions in the app's database, SessionMiddleware) and
    writes screens_auth.go with /login and /register; the login form
    carries ?next= through to the redirect. Registering
    lands on /login?registered=1 with a notice rather than a session,
    because the register endpoint answers a new and a taken address alike.
    Entity CRUD mounts under /api, so a screen can own /posts. .env
    gains a per-checkout JWT_SECRET. Auth dev mode follows GOFASTR_DEV
    (set by gofastr dev), never .env, so outside the dev loop auth
    issues Secure cookies and refuses to boot without a secret. init .
    refuses a directory whose .env git already tracks (#503).
  • ui.AuthCard has a Notice slot. It renders a role="status"
    message in the success color between the alert and the body, for
    outcomes like "account created" that the danger-styled Alert would
    misreport (#503).
  • battery/sitesearch: full-text search over every rendered page.
    The battery renders the pages a static export would write (dynamic
    routes through StaticPaths), indexes each page's <main> with its
    h2/h3 sections as deep links, and answers at
    ...
Read more

v0.87.2 — gofastr dev answers while the app is down, isolation remaps a port once

Choose a tag to compare

@DonaldMurillo DonaldMurillo released this 12 Oct 19:27
bdea4ca

Added

  • gofastr dev answers while the app is down. During the first
    build, after a failed build, and after the app exits, the dev address
    returns 503 with Retry-After and says which of the three it is, the
    build or lint output included, instead of refusing connections. Open
    tabs reload into that page when a build fails and back into the app
    when it is up again. A second gofastr dev on a busy address now
    stops at startup instead of after its first build, except in a linked
    worktree, where isolation moves it to the next free port (#483).
  • entityui.UI.WithCreatePath sets the create screen's route. An app
    that mounts its create screen at /deals/new instead of
    /deals/create passes one path function, and New, Duplicate, Create
    another, related-list Add and relation-picker New all link there.
    Returning false or an empty path hides an entity's create links.
    Without the option, links still use <base>/create (#502).

Fixed

  • Entity state-move buttons no longer fail with HTTP 415. The RPC
    runtime now sends Content-Type: application/json on writes that have
    no body, which the transition route requires. GET and HEAD send no
    content type, and multipart forms keep the browser's boundary (#500).
  • Worktree isolation remaps a listen port once. In a linked
    worktree, gofastr dev --addr localhost:8791 printed one port while
    the app bound another: the child remapped the address it was handed,
    and App.Start remapped the result of isolation.ListenAddr again.
    Runtime.Env now passes every recorded mapping to the child, and
    active runtimes in one process with the same project, isolation ID
    and port config share their mappings, so the app binds the address gofastr dev names.

v0.87.1

Choose a tag to compare

@github-actions github-actions released this 11 Oct 21:51
b0fb145

Changed

  • gofastr init writes the agent detail files to .agents/. They
    went to a visible agents/ folder at the project root, beside
    entities/, migrations/ and the app's own packages. They now go to
    a hidden .agents/ folder, and AGENTS.md, CLAUDE.md and the
    gofastr-host skill link there. Run gofastr agents sync (or
    gofastr init --reinit) once in an existing project: it writes
    .agents/, deletes the old generated files in agents/ (only files
    with the auto-generated header, so a hand-written file there stays)
    and removes the folder once nothing else is in it. It also points the
    generated agents/ references in AGENTS.md's intro at .agents/,
    and --reinit refreshes a stock CLAUDE.md from the earlier release.
    An agents symlink is left as it is.

Added

  • A sidebar can stay pinned under a sticky page header.
    ui.ContentRowConfig.BelowHeader, with Sticky, sticks the nav
    column and the toolbar below --size-header-height instead of behind
    the header, and makes the column the rest of the viewport tall, so a
    long menu scrolls on its own while the page scrolls beside it.
    interactive.SectionMenuConfig.FillColumn makes a section menu fill
    that column. The docs site's /components menu uses both.

v0.87.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 06:04
b6619ab

BREAKING. v0.87.0 reskins the default theme, gives entities screen
hints and state machines, draws entity screens through the new
framework/entityui, and rebuilds the admin on it. It deprecates
nothing: a removed API is gone. What changed in
v0.87.0

(gofastr docs release-0-87) is the full account, with every breaking
change, what replaces it, and the fixes this release carries. Run
gofastr upgrade --to v0.87.0 before you bump go.mod: it points at
each line of an app that needs an edit.

Highlights

  • Zinc default theme. style.DefaultTheme() and theme.Default()
    draw a near-black primary on white with 6/8/10/14px radii. Stroke,
    line-height, letter-spacing and opacity are tokens (Theme.Strokes,
    Leading, Tracking, Opacities), Theme.Knobs sets any
    --ui-* knob, and every kit dimension reads a token or knob, so a
    theme alone can redraw the kit (theme.Brutal()).
  • ui.ThemePicker switches a page between registered themes and
    remembers the choice.
  • Entity Display. EntityConfig.Display holds names, title
    fields, columns, named views, facets, form layout and field hints,
    checked at registration. dsl.ParsePredicate/ParseSort, a ne
    filter, crud.ListOptions.Where, SumAll and GroupCountAll back
    it.
  • Entity States. EntityConfig.States names a state field and
    its moves. Every write path enforces them; a move runs through
    RunTransition, POST <api>/<entity>/<id>/transitions/<key> or an
    MCP tool, and is audited. crud.WithStateOverride writes outside a
    move with a recorded reason.
  • framework/entityui. App.EntityUI draws list, record and create
    screens with no islands: views, facets, filter chips, saved views,
    inline editing, bulk actions, CSV export, a trash view and status
    overrides. framework/ui/resource is deleted.
  • The admin, rebuilt. battery/admin draws entity screens through
    entityui at /admin/entities/<entity>, renders every page through the
    app's UI host, and ships no CSS. New dashboard, command palette,
    saved views and queued bulk runs.
  • Prefixes. Kernel attributes are data-cui-* and kernel classes
    cui-*; fui-* is framework/ui's alone.
  • Security audit. The 2026-10-04 audit of the request path fixed
    scope, relation, upload, 2FA, verification and rate-limit gaps, each
    with a security test. The contract catalog holds 78 rules.

Breaking, in brief

  • A ShadowSet built field by field (every theme.go an earlier
    gofastr theme init wrote) needs Shadows.XS, or it panics at boot.
    --color-secondary, --color-border-strong and --color-accent
    change role.
  • Owned sheets must read the stroke, leading, tracking, opacity and
    logical spacing tokens: GOFASTR1807 refuses the matching literals.
  • data-fui-* kernel attributes and the kernel's fui-* classes are
    renamed to data-cui-* and cui-*; .fui-copied,
    fui-toast-stack-auto and is-leaving are hooks now.
  • framework/ui/resource is removed; battery/admin needs Config.UI
    and a UI host, moved its routes, and lost Theme, FontFaceCSS,
    EntityListLimit, Secret, RegisterRoutes, SortDirOf and
    /admin/rbac/_revoke.
  • The blueprint's screen filters:, transitions:, search:,
    island: and widget: keys and the entity_create/entity_edit
    blocks are gone.
  • The framework_docs_* MCP tools need
    framework.WithMCPTools(mcptools.Register).
  • Filter operators must suit the column type, and
    crud.ListOptions.Filters is validated.
  • i18nui.LabelForField, RenderOverlayResult's arity,
    queue.Browsable.ListJobs's arity, WorkbenchConfig.RailWidth and
    CopyButtonConfig.Target changed.
  • Auth and access: unverified accounts are claimed on mailbox proof,
    RequireTwoFA judges the request principal, scoped tokens and embed
    grants are held to their scopes across relations, X-Forwarded-For
    is read from the right, /.debug needs the admin role, and
    upload.Handler caps bodies at 32 MiB.
  • App.WithAuditLog adds a reason column to an existing audit table
    at boot.

Security

  • The repo builds with Go 1.27.2 (toolchain go1.27.2 in go.mod; the
    go 1.27.0 floor for apps is unchanged), which fixes the thirteen
    standard-library advisories govulncheck reports against 1.27.0 (among
    them GO-2026-6599 and GO-2026-6600 in html/template). Build apps with
    1.27.2. golang.org/x/tools moves to v0.50.0.
  • Security audit findings 83–100: bounded growth and lifecycle fixes
    across the auth, cache, queue, relay, stream, cron, rate-limit and
    credential-store packages. Each is listed under "Security hardening"
    in the release page's Fixes section.
  • a2a.Config.MaxConcurrentRunsPerOwner caps one owner's concurrent
    skill-handler runs, resumes included (0 = 16, negative = no cap).
  • PasswordResetPlugin.OnStop stops the reset-email workers at
    shutdown; reset email goes out from a bounded queue, off the request
    path, so a known address answers as fast as an unknown one.
  • credstore.ErrAmbiguousLegacyKey: a legacy provider|account key
    with more than one | is no longer handed to the first split; Get
    asks for re-entry and Put/Delete on any pair that could own it
    replace it.

v0.86.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 00:20
484747c

BREAKING. v0.86.0 rebuilds the UI layer and deprecates nothing: a
removed API is gone. What changed in
v0.86.0

(gofastr docs release-0-86) is the full account, with every breaking
change, what replaces it, and the fixes this release carries. Run
gofastr upgrade --to v0.86.0 before you bump go.mod: it points at
each line of an app that needs an edit.

Highlights

  • Headless components. Every framework/ui component renders
    through framework/headless, which owns structure, ARIA and the
    data-hui-* hooks behaviour binds to. core-ui/patterns is deleted,
    and the kernel's per-component runtime modules moved beside their
    components as registered behaviours.
  • fui-* classes. Every class the kit emits is renamed from ui-*
    to fui-*. Sheet names and data-fui-comp markers keep their names.
  • One layout primitive. app.NewLayout(name, spec, build) replaces
    the fixed header/sidebar/footer template, with outlets, fills, route
    areas, screen groups, resolvers, deferred outlets, loading content and
    transitions. ui.SiteHeader, ui.SiteFooter and ui.DocLayout are
    deleted: gofastr generate package copies owned replacements into the
    app.
  • Owned styles. An app's CSS lives in <name>.style.css sheets
    scoped to a layout, screen, component or the app, generated into typed
    Go by gofastr generate styles and checked against the theme's tokens
    by GOFASTR1801 to GOFASTR1822. The contract catalog holds 77 rules.
  • Theme. Size and font-weight tokens, Theme.Layout page
    dimensions, typed app tokens (Theme.Extend, <name>.tokens.css),
    component options, and a contrast check at boot.
  • Upgrade tooling. gofastr upgrade reads a project through the
    type checker. Tests fail on retired markup a page renders and on render
    panics.

Breaking, in brief

  • Button and form wiring is typed: an unknown data-fui-* key panics at
    render. FormFieldConfig.Input is a builder closure.
  • ui.DataTable and ui.Pagination take typed Query props instead of
    href patterns.
  • The fixed-template layout API and the --ui-layout-* and
    --ui-container-* variables are gone. Page dimensions are
    Theme.Layout tokens.
  • Ten legacy colour aliases are gone, auto-named scale tokens spell
    2xl/3xl, and style.ThemeRef.Hash is a method.
  • A theme with a hex primary or danger pair below 4.5:1 contrast panics
    at WithTheme.

v0.85.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 06:34
02daca9

Security

  • Round-4 adversarial probes: 54 over twelve property families the
    earlier rounds never opened (authorization at derived surfaces,
    layer-mismatch parsing, cheap-write state growth, transaction ordering,
    account lifecycle, dev surfaces in production, signed-payload confusion,
    untrusted project directories, invisible/bidi characters,
    interactive-layer caps, data at rest, config time-of-use), all fixed
    and promoted to permanent security tests.
    The changes worth naming:
    • Bearer JWTs fail closed on lifecycle change. RequireAuth
      re-resolves the subject via the user store on every request, so a
      deleted or role-downgraded account loses access at once instead of at
      token expiry, matching the session and API-token planes.
    • Delivery counts every attempt. The outbox relay increments
      attempts at claim (matching the queue and webhook batteries), so a
      relay that dies between claim and settle no longer redelivers forever;
      a non-cooperative queue handler runs on its own goroutine and stops at
      the deadline instead of wedging the worker.
    • Uploads and journals refuse symlinked leaves. core/upload
      LocalStorage refuses a key that folds onto an existing object (the
      battery/storage posture, now shared); the kiln journal opens its
      session file O_NOFOLLOW; a failed multipart create deletes the file
      parts it saved instead of orphaning them.
    • Numbers survive the wire. MCP tools/call decodes arguments with
      UseNumber, so an integer filter above 2^53 keeps every digit and
      addresses its row (small numbers still arrive as float64); the HTTP
      binder rejects NaN/Inf floats, which slip every range guard.
    • Derived reads stay in scope. The admin audit listing and its
      overview count filter by the caller's tenant; /openapi.json and
      /api/docs/openapi.json apply llm.md's per-request read-scope filter,
      with WithPublicOpenAPI the documented full-disclosure opt-in.
    • Subscriber seats are bounded. The SSE bus, MCP notification stream,
      ACP sessions, and CRUD event streams cap concurrent seats per principal
      (default 16) with a configurable overflow policy (refuse, the default,
      or evict-oldest); A2A task rows and push-notification configs are
      bounded per owner.
    • Dev and build inputs are contained. An exposed (non-loopback) dev
      MCP bind withdraws its mutating tools; the harness .env loader and
      the project isolation file refuse HTTPS_PROXY, PATH, LD_PRELOAD
      and other process-shaping names from an untrusted checkout; the config
      loader rejects non-finite floats.
    • Widget gating gains a level. RequireSession keeps its meaning (a
      browser session, anonymous allowed, per-session scoping) with its doc
      corrected; a new RequireAuthenticated fails closed for the anonymous
      session the first page load mints.
    • Invisible and bidi characters (C1 controls, zero-width, Trojan-Source
      overrides) are stripped or refused at the sinks where they forge or
      hide identity: email canonicalization, notify and email headers,
      upload keys and filenames, page titles, log and trace lines, generated
      blueprint enum values, the verify report, and the harness terminal.
      The shared predicate lives in the new core/textsafe package.
  • Three static-analysis rules from the round-4 bug shapes, taking the
    contract catalog to 59 rules: GOFASTR1410 (a local-filesystem
    Save/Put/Write reaching an open/create/rename with no folded-key
    refusal), plus the laxenvelope vet analyzer (one transport decodes an
    RPC envelope strictly while a sibling decodes it lax) and nonfinite
    (a ParseFloat result stored with no IsNaN/IsInf gate). Ten rule
    shapes were prototyped; the seven that fired too broadly on
    correct-by-design sinks were dropped, their target bugs covered by the
    fixes above.

BREAKING (round 4): an MCP tool handler's plain error is now
answered to the caller as a generic "internal tool error" (its text is
logged server-side); a handler that means its message to reach the caller
returns an *mcp.RPCError or an mcp.ToolResult{IsError: true}, the form
mcp.Gated and the framework's own tools now use. A duplicate multipart
form key is refused for a scalar field and collected into a list for a
schema.JSON field, matching the JSON body path. /openapi.json
and /api/docs/openapi.json now filter entities by the caller's
read scope (an entity the caller cannot read is omitted, matching
/api/llm.md); WithPublicOpenAPI opts into the full, unfiltered
document.

  • Round-5 adversarial probes: 130 probe files, 158 failing findings
    and 2 data races over the families the earlier rounds never opened
    (log-side scrubbing of panic payloads, stream-seat exhaustion,
    cache suppression on per-user responses, secrets at rest, strict
    decode at every inbound seam, terminal-state settles, missing
    deadlines, argv-exposed credentials, identifier gates in emitted
    code, panic containment at host callbacks), all fixed and promoted
    to permanent security tests.
    The changes worth naming:
    • Session gates refuse API tokens. RequireSession,
      SessionPolicy, and RolePolicy answer 401 to a caller
      authenticated only by a scoped gfsk_ token (TokenScopes in the
      context is the discriminator), so a leaked scoped token cannot
      reach the SSR screens its scopes never named. RolePolicy now
      honours an access.Decider.
    • Password-equivalent tokens are hashed at rest. Magic-link,
      password-reset, and email-verification tokens store
      sha256(token) keyed for lookup (the APIToken grammar), and
      EntitySessionStore does the same for the session cookie value:
      a read-only dump of the token or session table is no longer a
      pile of live takeover credentials. The session cookie is minted
      by one helper (mintSessionCookie) and carries SameSite=Strict
      on password login, magic-link verify, and the OAuth callback.
      BaseURL on the three link-minting plugins must be an http(s)
      URL or Init fails; the OAuth refresh-token save is a
      compare-and-swap (SaveIfRefreshUnchanged).
    • CRUD writes gate their foreign keys. A create/update whose
      BelongsTo FK names an owner/tenant/read-scoped row the caller
      cannot read answers 404, the same status reading that row gives,
      so existence is not leaked. owner.AllowCrossOwner, the target's
      CrossOwnerRead permission, tenant.AllowCrossTenant, and
      WithServerWrites contexts pass unchanged. CRUD responses carry
      Cache-Control: no-store; typed delete hooks error on drift and
      typed hooks merge nested in-place mutations; pagination cursors
      refuse duplicate and case-folded keys at any depth.
    • Streams admit by seat. rtc.Config.MaxSocketsPerUser
      (default 16, evict-oldest per principal) caps signaling sockets;
      a2a gains MaxStreamSeatsPerOwner (0 = 16, negative unlimited)
      and StreamSeatOverflow (Refuse answers 429, EvictOldest
      closes the owner's oldest stream); the harness control plane
      shares one control.SeatTable (DefaultStreamSeats 16) across
      its REST SSE, WebSocket, and MCP GET streams; kiln's
      /.kiln/events seats 16 per peer host.
    • Per-user responses suppress storage. The auth token and
      account listings, CRUD responses, A2A POSTs, uihost pages
      (no-store plus Vary: Cookie), the resource TableHandler, kiln
      JSON GETs, the harness REST surface, and the generated chat page
      all stamp Cache-Control: no-store: RFC 9111's storage
      restriction covers Authorization headers, not cookie
      authentication. The agent-card and Link-header pages Vary on
      Host and X-Forwarded-Proto when no BaseURL is pinned, and
      the idempotency middleware appends to Vary instead of
      overwriting it.
    • Strict decode at every inbound seam. handler.Bind's
      non-struct branch is gated on a JSON Content-Type, query params
      truncate at the first C0/DEL byte, and bindBody /
      UnmarshalStrict / DecodeStrict refuse trailing JSON. MCP
      prompts/get and resources/read, moduleproto frames
      (ErrInvalidFrame), ACP params (id-matched invalid-params at any
      depth), the harness mcpserver (refusals are -32602 with an
      any-depth key walk), OpenAI chunks and the OpenRouter /models
      listing, rtc signaling frames, and the uihost ?theme= embed all
      decode strictly now.
    • recover() output is scrubbed. textsafe.Recovered renders
      every panic value that reaches a log (auth audit and notify,
      semantic metadata, a2a invoke, the fanout subscriber, mcp gates,
      the middleware collector, stream close hooks, the semantic
      watcher), stripping control bytes and truncating. StateChannel
      also survives a panicking host SnapshotFor/FilterEvent
      callback.
    • State files are owner-only on overwrite. os.WriteFile with
      an 0600 literal only applies the mode at create, so overwriting a
      0644 file kept 0644. The shared fileperm.WriteOwnerOnly /
      SeedOwnerOnly helpers (open, chmod the handle, write) now back
      the harness memory/credstore/persist/tracing/DEK writes, kiln's
      world.json and session.db, and the generated CLI's
      config.json.
    • Terminal settles are fenced. The queue's Redis Reclaim
      drops stale entries through a fenced releaseClaim; webhook
      inbound envelopes never regress a terminal status (memory and
      SQL, both dialects); the outbox SQLite backoff predicate matches
      its Postgres sibling; admin queue replay writes its audit row.
    • Deadlines on every probe. Bare http.Get/Post/
      http.DefaultClient clients are gone from kiln's port probe, the
      bench readiness loop, semantic remote calls, and the eval probe;
      generated e2e/axe suites use a 10s-timeout client and the
      semantic CLI a 2s one; exec children with c...
Read more

v0.84.1

Choose a tag to compare

@github-actions github-actions released this 06 Sep 22:22
fb78fe3

Fixed

  • Client-side navigation works on a static export below a base path.
    The export rewrote every href for the base and left the embedded route
    graph root-relative, and the runtime takes the client-side path only for
    a link it finds in the graph, so on a GitHub project page every click was
    a full page load. The graph's paths carry the base too.
  • A static page no longer loads every component stylesheet twice. The
    export emitted one <link> per component without the data-fui-style
    marker the runtime dedupes on, so on load the runtime appended a second
    copy of each after app.css, and a host's own overrides lost the
    cascade: a sidebar title stretched to fill its column, a doc layout
    collapsed. The exported links carry the marker and id the runtime would
    have written.

v0.84.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 20:02
e4692d2

Fixed

  • A static export renders layout chrome for the page being exported.
    uihost.RenderStaticPage rendered with neither a request nor a route
    match in the context, so a layout component that reads the path, such as
    a sidebar for the section being read or a header whose language follows
    the page, rendered for no page at all: every exported page carried the
    chrome of the site root, and a translated page shipped with the original's
    sidebar. The static render now carries a request for the path and the
    route match, exactly as handlePage installs them for a live request.