npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@knpkv/herdr-hub

v0.67.0

Published

Authenticated fleet approvals, PWA, coordinator, and hostd runtime

Readme

@knpkv/herdr-hub

Formerly @knpkv/herdr-approvals: renamed, with the same hostd and fleetctl binaries and the same exports.

Host runtime, fleet CLI, approval PWA, push worker, and shared Rly shell for the Herdr packages.

It publishes three binaries:

  • hostd: local fleet authority plus optional Tailscale listeners, approvals, Connect, and push
  • fleetctl: status, history, job submission/following, Work checkpoint recording/snapshots, and fleet-wide apply submission with per-host outcomes
  • relay: opens the canonical tailnet Relay UI; post sends short agent updates; me updates creature self-state; mcp serves relay_me over stdio

Each executable is also an import entry: @knpkv/herdr-hub/hostd, @knpkv/herdr-hub/fleetctl and @knpkv/herdr-hub/relay. Importing one runs that CLI, so a host can bundle its executables from them.

Run it on its own

npx -y -p @knpkv/herdr-hub hostd   # serves the super-app on http://127.0.0.1:4778/

It runs on Node 26 with no fleet configuration, no Tailscale and no other host. Without a file at the default path ~/.config/fleet/config.json it starts standalone and prints one line on stderr:

hostd: no fleet config at <path>; running standalone on http://127.0.0.1:4778/ (set FLEET_CONFIG_PATH to use one)

Both listeners are loopback-only: the dashboard on 4778 and the Work listener on 4777. A taken port stops startup. Nix and agent-delegation jobs are refused; state lives in $XDG_STATE_HOME/fleet (else ~/.local/state/fleet). On Windows the default configuration is %APPDATA%\fleet\config.json and state lives in %LOCALAPPDATA%\fleet; file modes do nothing there, so owner-only comes from the user profile's ACL over those folders. On Windows hostd runs npm-installed commands (agent-usage, claude, codex, host-coordinator) as Node with the script their npm .cmd shim names, never through a shell; a native .exe runs directly, and any other .cmd or .bat fails as not found. HERDR_HUB_HOME puts configuration and state under one directory (config/, state/) on any OS, and HERDR_HUB_CONFIG names the directory holding the installed-apps apps.json. Point FLEET_CONFIG_PATH at a fleet configuration to run it as a fleet host, described below; a FLEET_CONFIG_PATH that names a missing file is an error, not standalone.

relay post --kind status "Tests passing" resolves the current Herdr pane's stable agent ID and posts to the configured canonical authenticated hub. Kinds are status, progress, skill-feedback, note, and blocker. Status text allows 240 characters, other kinds 1024. --event-id ID allows retries across invocations, and --goal-step ID links an update to a goal step. An omitted event ID is generated once; transport retries reuse it. FLEET_CONFIG_PATH selects the fleet configuration, and HERDR_PANE_ID must identify the current pane. The CLI offers no agent ID override.

relay me --mood focused --say "Running the delivery tests" --accessory glasses updates the current pane's creature through POST /v1/connect/me. CLI and MCP resolve the current pane with herdr agent get, then send its stable ID in x-herdr-agent-id; the request body accepts no host, agent ID or reply ID. The hub applies the same server-side authorization as relay post: identity is host-scoped, authenticated from the actual Tailscale peer and checked against that host's fresh, complete inventory. A same-host sibling could write another agent's self-state. A foreign host cannot claim that agent. No per-pane credential is added.

Supply a mood or words, or both. Moods are happy, focused, worried, sleepy, excited, and waiting; accessories are none, headphones, chef-hat, hard-hat, scarf, and glasses. Words are trimmed, nonblank, at most 140 characters, and contain no control characters. Omitted fields retain their previous values; a first update defaults the accessory to none. The owner-only post database stores only the latest self-state per host and stable agent ID, with a server timestamp and replyTo: null. Confirmed archive clears it. Updates allow a burst of three, then refill one token every ten seconds per agent. Excess writes return 429 TooManySelfUpdates with retryAfterMillis; transport does not retry an uncertain write. The authenticated /v1/connect/agents poll carries this state as self, or null when absent. P1 exposes the data and commands; creature UI integration follows the design references.

relay mcp serves only relay_me {mood?, say?, accessory?} over MCP stdio. It shares validation, current-pane resolution and canonical-hub transport with relay me, reads the same FLEET_CONFIG_PATH and HERDR_PANE_ID, and returns validation, rate-limit and identity failures as MCP isError tool results. Standard output carries MCP frames; logs go to standard error. Agent runtime registration is configured separately.

Running relay without a subcommand prints the configured approvalHub.url and opens it when this machine is online on the tailnet and the hub answers. --no-open, BROWSER=none, CI, or a non-interactive stdout skip opening and print the reason. Browser authentication uses the hub's actual-peer Tailscale WhoIs check and allowed users; no bootstrap code is needed. The command verifies the configured hub's stable node identity and probes its public Relay manifest with a one-second timeout, a bounded schema-checked body, and no redirects. A responding manifest establishes listener reachability, not authorization or Relay backend readiness.

Off-tailnet or when the hub does not answer, relay never opens the local dashboard. If the configured loopback listener answers, stderr says Relay is only served over the tailnet (hub not reachable); this machine's dashboard is at <local url>. If it does not answer, stderr says hostd is not running. Start it with: systemctl --user start hostd. Both exit nonzero with empty stdout. The local dashboard URL comes from localPort; it does not serve the Relay conversation. The command never starts or stops hostd. relay --help still lists its subcommands, and relay post retains its existing behavior.

Voice is host-bounded in v1: the hub authenticates the Tailscale node and checks the claimed agent against that host's fresh, complete inventory. A same-host process can claim a sibling's ID, matching the host-scoped self-state boundary. POST /v1/connect/posts and paged GET /v1/connect/posts?host=HOST&agentId=AGENT&cursor=SEQUENCE exist only on the canonical listener. Reports are plain text, never transcripts or HTML; consumers render them as text. Owner-only SQLite retains 90 days and 128 reports per host and agent. Retries are keyed by host, stable agent ID, and event ID. The latest retained report becomes the agent's voice; confirmed closure keeps its text and time as archive lastWords, including across existing-database migration. Failures are tagged InvalidReport, UnboundAgent, Denied, Conflict, and Unavailable.

hostd reads FLEET_CONFIG_PATH, defaulting to ~/.config/fleet/config.json. The file is decoded by @knpkv/herdr-fleet; invalid or missing fields fail startup. When no file exists there, hostd runs standalone: npx -y -p @knpkv/herdr-hub hostd serves the super-app on http://127.0.0.1:4778/ with the fleet off, keeps state in $XDG_STATE_HOME/fleet (else ~/.local/state/fleet; %LOCALAPPDATA%\fleet on Windows), refuses Nix and agent-delegation jobs, and prints one stderr line naming the path it looked for. A taken port stops startup with an error naming the address. With crossHost: false, the loopback listener, the Work-only listener bound to workBindAddress (loopback by default), and immediately queued safe jobs are available, with no Tailscale dependency. Cross-host listeners authenticate the actual socket peer with Tailscale WhoIs. Forwarded identity headers are never trusted. An approve or reject needs an allowedUsers login on a node in approvalNodes, on the hub's listener as on each peer's approval listener: fleet hosts run agents under the same login, and a script can read the dashboard's proof and set any Origin, so the login alone does not show a person decided.

The hub is behind login. Its own listeners (the tailnet hub listener and loopback) admit a request only after the access gate (internal/access-gate.ts) has decided who the caller is and checked that against what the route's family requires. Today's per-route checks still run after it. Per-host listeners (a peer's tailnet listener, the approval, Work and LAN listeners) keep their node checks.

Who a request is:

  • A fleet machine's node is a Host, the hub's own node included, until someone logs in in a browser there. The exception is the hub calling a peer's per-host route, which is a Service.
  • An allowed login on any other node is a Person, with or without a session.
  • On loopback:
    • a request carrying a tailscale-user-login header is no one;
    • one with the hub's CLI token is the Cli;
    • one with a local session is the local user with a session;
    • anything else is the local user without one.

What the gate answers:

  • A caller without a session gets the login gate on a page (design page 22) and 401 {"error":"login_required"} from the API, wherever logging in would let them through. A page is a hub page, or an app's page a browser navigates to (Sec-Fetch-Dest: document, or an Accept asking for HTML); an app's other requests get the JSON. The session routes and static files stay open.
  • Agents and hosts keep their own credentials. On loopback agents, fleetctl and herdr-hub apps present the CLI token (x-hub-cli-token, read from <hubState>/cli/token): it views, submits jobs and restarts apps. Since it submits jobs, it may also try again any failed job from Needs you (POST /v1/modules/fleet/failures/retry), a nix.apply included; dismissing one is a person's call. A fleet host's node views, submits jobs and reports as itself, but a browser there is a person's: it sees a page, and makes a person's writes, only after logging in there.
  • Anyone a login wouldn't help gets 403 {"error":"forbidden"}: no one, a credential that can't hold the permission, or a person approving from a node that can't approve.
  • A session's write without its CSRF token (x-herdr-csrf) gets 403 {"error":"csrf_required"}. The super-app's browser clients send it themselves: every non-GET request to the hub's own routes carries the token from the page's latest session read (requests to other origins and to /apps/ never do). A write that finds no token yet (the standalone Connect page, or a click before the page's first read) reads the session first, once for all the writes waiting; a write the hub refuses as csrf_required (the session changed, maybe in another tab) reads it again and is sent once more. Logging out clears the token.
  • An app's write (a non-GET under /apps/<id>/) needs no CSRF token, since apps are never given one. It needs the session and an Origin equal to the listener's own (scheme, host and port), so a page on another loopback port can't use the cookie; another origin, or no Origin at all, gets 403 {"error":"origin_required"}.
  • A route no family names gets 404, so a new route can't slip past the gate.
  • A caller the hub can't read (WhoIs or the session store unavailable) gets 503 {"error":"identity_unavailable"}; the hub logs the cause as a warning.
  • The browser terminal's WebSocket upgrade passes the same gate.

An admitted session request renews a sliding session, at most once a minute.

People log in at /v1/session. The hub stores only each session token's SHA-256 digest, in person-sessions.sqlite in the state directory, so sessions survive a restart. At most 8 sessions are open per login and node, and 64 in all; logging in past that drops the oldest of the same login and node first. Reading a session never writes. A sliding session is renewed only once the request has matched its login and node.

  • Tailscale (tailnet hub listener). POST /v1/session/tailscale logs the WhoIs login in on its node.
    • The token goes in a __Host-herdr_session cookie (Secure, HttpOnly, SameSite=Strict, Path=/).
    • A session counts only on the node and login it was opened for.
    • On a person's own device it renews on use: it ends after 8 idle hours, or 7 days after logging in. On a fleet machine it ends 8 hours after logging in.
    • The cookie lasts 8 days, past any session's end, so the browser still sends it once the session ended and the page says so.
  • Local user (loopback). herdr-hub login trades the CLI token for a one-time code (60 seconds, single use, dropped after 5 wrong guesses) at POST /v1/session/local-code. It then prints and opens http://127.0.0.1:<port>/#login=<code>.
    • The page takes the code out of the address, then spends it at POST /v1/session/local.
    • The hub sets a herdr_local_session cookie (HttpOnly, SameSite=Strict, Path=/, 8 days, past the 8-hour session). Plain http can't set a __Host- cookie. Cookies don't separate ports, so every local server sees it, but each of those is the user's own process, which could run herdr-hub login itself.
  • Session reads and logout. GET /v1/session says who the caller is, and gives the session's CSRF token when they are logged in. Logged out, it says whether the request carried a session that has ended (ended), and over Tailscale whether the caller is on a fleet machine (fleetHost). POST /v1/session/logout needs the CSRF token in x-herdr-csrf and clears the cookie.
  • What a session proves. Before passkeys a session shows intent, not presence: anything on the same node can press the same button. Every process of the user can read the CLI token, so the Cli adds intent, not isolation. A person logged in on a fleet machine never owns Sandboxes.

The super-app's identity screens (design page 22):

  • Login gate. A browser without a session sees only the Relay mark and one card, desktop and phone:
    • over Tailscale, "Continue as ?" with Log in with Tailscale, plus "This computer runs agents. They keep working; only you, logged in here, can see and act." on a fleet machine;
    • after a session ended, "Your session ended. Log in again to continue." above the same card;
    • on loopback, "Log in as local user" with the herdr-hub login command and Copy. Logging in reloads into the hub.
  • Desktop sidebar session control.
    • A logged-in Tailscale person: their name and "Logged in until 21:05", opening a popover with the login, "Logged in with Tailscale on ", "Until 21:05" and Log out.
    • The local user logged in through herdr-hub login: "This computer" and "Logged in until …".
  • Renewal and lapse. An open page reads /v1/session every minute and when it becomes visible, which renews a sliding session. A session that lapses under an open page reloads it into the gate.
  • Waiting for passkeys: the "You can" list, the passkey states and the phone's session details.

Consumers that own durable orchestration import makeHostdProgram and HostdOperationsComposer from @knpkv/herdr-hub/hostd-runtime. The composer receives the decoded host configuration, the package's default typed operations, and a narrow fork function that registers work for the hostd process lifetime without exposing its closeable scope. It may decorate the typed run and runCoordinatorChat methods while reusing every default status, local recovery, and command adapter. Each run receives the accepted job ID and actor, the existing workerStarted callback, and a typed lifecycle callback. The required AgentDelegate.mode distinguishes ordinary consult work from transition_summary; durable adapters project those to Luna medium and Luna low respectively, without reading prompt text or defaulting an unknown intent. Status and wait remain outside model dispatch, while review and work remain Sol high. A durable adapter supplies recovery.matches and recovery.resume. It submits one OrchestratorCommand, calls lifecycle.accepted with its durable receipt, returns that receipt, and registers later queue, run, worker binding, and terminal work through fork. Fleet marks matching jobs durable before invocation, persists the receipt separately, and keeps the wrapper running until lifecycle.terminal mirrors settled, delivery_failed, or task_failed from the coordinator. After restart, recovery.resume receives the same deterministic job ID, fresh callbacks, either the receipt or null, and the exact persisted worker or null. The null receipt path resubmits the same idempotency key to close the crash window after coordinator commit but before Fleet receipt persistence; it must not create a second request. A present worker is reattached by replaying workerStarted without a new prompt. Terminal values are explicit bounded Fleet summaries produced with summarizeHostOperationTerminalDetail; full authoritative values remain in coordinator events. Consumers stream those durable events by request ID from the same coordinator instance. They call its authoritative workerStarted before the fleet callback so Work and Connect receive the same worker identity. A failed Luna dispatch becomes a new Sol dispatch through submitSolEscalation and OrchestratorLinkedSolDispatchReference. The accepted v2 Work handoff supplies both its bounded context delta and immutable expected revision; the adapter uses that persisted revision at worker start instead of refreshing lane authority.

Delegate approvals, the dashboard, and fleetctl job output show optional newWork as New Work goal: <repository>@<branch> — <title>. The validated branch and title are bound into the approval hash. fleetctl job and follow retain the raw record's error for failed jobs. Terminal output visibly escapes C0/C1 controls, DEL, and field-supplied line breaks while retaining ordinary Unicode text. Browser credential-redaction truncation preserves complete code points so bounded titles remain schema-valid. Durable operation composers may mark a failure disposition: "rejected" only when nothing committed; Fleet settles unaccepted jobs on execution or recovery and leaves uncertain or receipt-bearing jobs recoverable.

The normalized receipt is intentionally client-visible: Fleet stores it in acceptedReceipt and result, and authorized raw job and history routes expose it. It must not contain provider callback URLs, bearer tokens, credentials, or server-private locators. Recovery registers observation through fork and returns before terminal completion so listener startup cannot be held open by durable work.

makeHostdProgram({ node, ... }) requires node, the Node binary installed hub apps run on (process.execPath in the executable): an embedding that omitted it started no apps, so it is now a type error. makeHostdProgram does not run an Effect runtime or provide Node services. The executable hostd keeps the existing default operations and owns one Node runtime. An embedding consumer owns that same single runtime. A composer that cannot build its required durable runtime fails startup with HostdOperationsCompositionError; there is no implicit retry or fallback to a different adapter. The composer receives no raw command executor, raw Herdr input, repository selector, or session selector. Approval and HTTP authorization stay inside hostd. The composer also receives hasOutstandingWorkJob, which says whether any Work job (admit, recover, reconcile, reassign, abandon) in hostd's job store is still to run: waiting for approval, approved and queued, or running. A pending approval past its expiry no longer counts, even while it is still stored as pending. Those jobs' preflight tokens hash the whole Work store until they run, so a background writer defers its Work writes while it is true. Composed without a job store, it fails with FleetStoreError rather than answering no. The composer also receives startedWorker(jobId): the worker Fleet's job record says that job started (host, agent id, name, pane), or null when the job is unknown or started none. It is the authority for which agent a job started; pane metadata is not, since any local agent can write it. A job store that can't be read, or a composition without one, fails with FleetStoreError rather than answering null.

The default operations run nix.apply as the configured apply command followed by two arguments: the ref, then the Fleet job id. The job id lets the apply command record that job's own outcome, so a host that restarts mid-apply can settle the job from it. An apply command that takes only the ref must ignore a second argument. nix.check and nix.apply always run to completion and their exit code alone decides the job: output past 64 KiB per stream keeps its end, led by [N earlier bytes omitted]. When one fails, its detail carries the stderr tail and then, under stdout:, the stdout tail, where tools like brew bundle report the error. Other commands' output is parsed, so past 128 KiB the job fails and the command is stopped.

On the approval hub, approvalTls names absolute certificate and private-key paths owned by the hostd user. The hub terminates TLS directly on its configured Tailscale IP and approvalPort; certificate provisioning and renewal remain host policy. Non-hub tailnet members keep their plain tailnet listeners. Local-only machines do not read TLS material or invoke Tailscale.

Fleet-wide submission preserves partial results. One unreachable host does not erase successful submissions to other hosts, but the command exits with a typed failure after printing all outcomes. The runtime performs no Tailscale mutation; Nix remains responsible for certificates, ACLs, node enrollment, secrets, and service lifecycle.

Approval mutations require the exact active listener origin, including HTML form submissions. Originless fleet submission remains a separate authenticated CLI path. Only hostd sets submission provenance, never the request body, which rejects unknown fields. A POST /v1/jobs on the local listener that passed loopback authorization and carries no proxy header (Forwarded, X-Forwarded-*, X-Real-IP, Via) is a verified local submission: its nix.* and agent.* jobs queue without approval. Every other listener or proxied request is remote, and Work authority jobs always wait for approval. Existing browser push subscriptions are reconciled with the server on page load, so server-side expiry or revocation cannot leave a false enabled state.

Hub apps

Apps are separate npm packages that the hub runs as child processes and shows in its navigation (@knpkv/herdr-usage is one). herdr-hub apps manages them on this machine with the npm that ships with Node:

herdr-hub apps add @knpkv/herdr-usage        # asks y/N after showing what it is and what it may do
herdr-hub apps list                          # id, package@version, state
herdr-hub apps update [<id>[@<version>]]     # same major unless a version is given; --no-wait to skip watching it start
herdr-hub apps rollback <id>                 # back to the version kept by the last update
herdr-hub apps logs <id> --follow
herdr-hub apps disable|enable|restart|remove <id>
herdr-hub apps trust add @your-scope         # needs a terminal
  • Where it writes. apps.json lives in the hub's config directory. It names one writer, managedBy: "cli" or "nix". When nix manages it, every write is refused; edit hub.apps in your nix configuration instead. Apps install under <data>/hub-apps/<id>/<version>, and their logs go to <state>/logs/<id>.log.
  • What it checks. A package needs a hubApp field ({ "bin": "dist/bin.js", "contract": 1, "services": [...] }) and must be inside trust.scopes. The installed tarball must be the one npm view described. Registry signatures are checked where the registry has signing keys. The app runs once before it is listed, and must answer as the app it declared. Install scripts never run unless --allow-scripts is passed for that install, after listing every package that has one.
  • What it doesn't promise. An app runs as you, with access to your files. Its services limit what the hub lets it do, not what the operating system lets it do.
  • Where its page opens. An app's page runs inside the hub page, with its authority. On the hub's tailnet listener that includes approving jobs, so until apps get their own origin only @knpkv packages open on any listener but loopback; any other app opens on this machine's loopback hub and reads "opens only on this machine for now" everywhere else. The package is the one the installer verified, not what the app says about itself.
  • npm. The npm-cli.js next to node, or else the npm on PATH when it resolves to npm's own npm-cli.js. Install a Node that ships npm (nodejs.org, mise) or put npm on PATH.

Connect directory

Terminal control permits one controller per host and stable agent ID, and four controllers in total. A socket is pinged every 25 seconds and terminated if the previous ping is unanswered at the next check. Disconnect, error, failed setup and server teardown share idempotent cleanup. The controller lock and connection slot are held for cleanup for at most ten seconds. A late finalizer receives an interruption request and remains owned by the hub; shutdown waits for an uninterruptible finalizer to finish. The TerminalReleaseTimeout warning contains only its tag, host, stable agent ID and elapsed milliseconds. An ordinary second attachment to a live controller is refused with code 4423 and agent already controlled. An ordinary reconnect to a closing controller waits for that same bounded cleanup, showing Connecting. Closing the waiting socket cancels its attachment; an explicit takeover during the wait supersedes it without bypassing cleanup.

Explicit takeover=1 on /v1/connect/session or the owning host's /v1/connect/terminal replaces that agent's controller. Malformed takeover values are rejected. The old socket closes with the shared TerminalTakenOverClose identity (4424, taken_over), stops accepting input, and releases its scope before the replacement opens. Replacement reuses the agent's existing cap slot. Concurrent requests admit only the newest surviving takeover after the shared cleanup barrier; an aborted waiter cannot open a session, and old cleanup cannot remove the new reservation. Clients show Connecting throughout the cleanup wait.

Before a remote takeover, the hub probes the owning host's tailnet GET /v1/connect/terminal/capabilities. This originless authenticated route authorizes only the configured approval hub node and returns { "takeover": true }, decoded with TerminalCapabilities. Every takeover needs a fresh probe; ordinary attachments do not probe. The read is bounded to 1 KiB and five seconds and does not follow redirects. Absent, false, malformed, redirected or 404 capability responses return the controlled state before any remote WebSocket upgrade. Transport or server failures use the unavailable-host path. The hub never retries takeover as an ordinary attachment, so older hosts cannot silently ignore the intent. Typed takeover close code and reason pass unchanged through the relay.

The authenticated GET /v1/connect/agents response includes optional hosts on its first page, read from config.machines. This lists the first 256 configured machine names in configuration order, even when agents or usage readings do not list a host. Additional configured machines remain configured but are omitted from this hint so the page stays within its schema bound. Later pages omit that metadata. Older clients can ignore it, and newer clients omit the empty-directory host hint when an older hub does not supply it.

Approval request disclosure

The authenticated approval browser receives a sanitized projection of each request from the persisted JobRecord. Pending, approved, rejected, and expired records retain the same typed request fields in browser-visible history. Safe fields include the request kind and title, operation mode and channel, normalized repository or revision coordinates, agent session, actor and worker identities, and lifecycle timestamps. Repository and revision values are redacted for credentials and unsafe URI query parameters before they cross the HTTP boundary; internal prompts and messages are represented by [redacted internal prompt].

Approval nonces, job hashes, command results and errors, credentials, subscription details, raw terminal or internal prompts never appear in the authenticated browser projection or any unauthenticated/public response. Other authenticated host activity may appear through its own sanitized projection; it is not part of this approval-request disclosure. The projection is not a substitute for the persisted JobRecord; approval decisions still require the authenticated listener and server-side nonce/hash proof.

pushAllowedOrigins is the exact HTTPS-origin allowlist for browser push services. Subscription endpoints are checked before persistence and again before delivery; credentials, IP literals, localhost, and origins outside that list are rejected. Add self-hosted push services explicitly by their public origin.

Blocked agent notifications

Local inventory is read before peer discovery. A discovery failure is logged and marks configured peers incomplete while local notifications continue. Delivery runs across at most four subscriptions at once, preserving episode order within each subscription.

The canonical hub also uses those subscriptions when a complete Herdr read reports an agent as blocked, needs-you, waiting or error, or its latest Voice post is a blocker. A later Voice post clears the Voice blocker; a live blocked state keeps the episode open independently. Partial reads and outages cannot start or end an episode. Missing agents close only after two complete absence reads spanning five minutes, with fresh absence evidence required after restart.

Each subscription receives one notification per episode and one reminder 15 minutes after the initial delivery attempt, if the agent is still blocked. There are no further reminders. The title is <name> needs you; the body contains the generated name/state sentence and, when present, only the latest Voice blocker's first line, bounded to 240 characters by Schema. The serialized payload carries the authenticated client-visible host, stable agent ID, name, blocked state, that first line, an optional approval job ID, and an optional Voice blocker eventId bounded to 256 characters. It contains no terminal output, scrollback, transcript, raw approval/question prompt, subscription keys or credentials. Voice reporters must keep secrets out of their reported text. Notification tags are per host and agent, so a repeat replaces that agent's earlier notification. Blocked notifications request renotify: true so reminders and new episodes alert again when replacing an existing notification, subject to browser and operating-system settings. A tap opens /connect/?host=…&agent=…&open=stage; a Voice blocker adds &event=… with its authenticated client-visible post identifier. That identifier is persisted in the episode payload and may cross the authenticated notification and Connect URL boundaries; it is not a credential or a raw provider locator. Connect currently ignores the event selector; a later posts view can scroll to the matching post. A known approval job uses the existing approval deep link. The click handler navigates a controlled same-origin window first, then focuses the returned client. Missing or uncontrolled windows and rejected or null navigation open the deep link in a new window while the click's window-interaction grant remains available. Focus consumes that grant, so a focus failure after successful navigation is surfaced without attempting another window interaction.

The canonical Connect stage shows notification settings when pushes are enabled on this device. Its Mute toggle saves only the selected agent; quiet hours save when both From and Until are valid and focus leaves the time editor, on Enter, or when the stage backdrop is tapped or Close is activated. Escape cancels an uncommitted draft. Clear removes quiet hours. Enter while Clear is queued reaffirms the visible time pair after that Clear; blur and dismissal do not restore it. The initial time zone is the browser's IANA zone; saved settings keep their zone. Pending saves mark the editor busy and keep controls usable. Edits are queued in order. Rapid mute clicks toggle the latest intended state, and quiet-hour reversions compare against the latest queued value. Rejected saves show an error while retaining the last saved mute and times. When pushes are off, the controls are replaced by a link to turn them on in Approvals. An unavailable device or settings check shows an error. The standalone stage checks its existing browser subscription, configured public key and server registration without enrolling the device. An older or missing subscription key shows the device-off state so Approvals can reconcile it.

Per-agent mutes and quiet hours are also available through authenticated settings routes. GET /v1/push/blocked-settings reads the saved settings. PATCH replaces full-snapshot PUT with a single intent: { _tag: "SetAgentMute", agent: { host, agentId }, muted } or { _tag: "SetQuietHours", quietHours }, where quietHours is a validated time pair and zone, or null to clear it. The store merges each intent atomically into the current settings and returns the complete result, so stale devices cannot overwrite unrelated preferences. They apply to the authenticated user's subscriptions across devices; defaults are no mutes and no quiet hours. Quiet hours require a validated IANA timeZone in the quiet-hour settings. The hub calculates the current minute in that zone, including daylight-saving changes. Quiet hours include the start and exclude the end, and may span midnight; equal times suppress all day. A visible open stage reports an owner-scoped heartbeat every 15 seconds to /v1/push/stage-heartbeat. A heartbeat for an open episode acknowledges it for that user across subscriptions, suppressing both its initial notification and reminder after the user leaves or the hub restarts. A later episode requires a new acknowledgement. Closing or hiding the stage clears presence; a lost heartbeat expires after 45 seconds. Presence and settings are accepted only on the authenticated canonical listener, with exact-origin mutation checks. Another user's presence or preferences cannot suppress your notifications.

Connect activity, archive, relationship and post stores share the approval application's private SQLite file with notification settings and Work. Connect batch connections use a 100 ms native busy timeout; request-facing approval-app and blocked-push connections use a zero native timeout and return their typed store failures immediately under contention; Connect writes retry asynchronously for about five seconds so a contended writer cannot block terminal heartbeats for the whole retry budget. An exhausted report write answers 503 ConnectStoreBusyError with its operation and retry budget, without the private SQLite cause. Optional archive passes in live directory reads stop after the first failed attempt and invalidate absence evidence for skipped hosts, bounding writer contention to one native wait across the fleet. Parent-name and voice/self-state enrichment each have a 100 ms deadline; latest-post reads perform no pruning writes. These failures remain isolated warnings and do not consume the directory deadline with primary-write retries. The five-second write deadline includes local transaction-permit queueing, and SQLITE_LOCKED uses the same retry policy as SQLITE_BUSY. Report clients use connectReportResponseTimeoutMillis: the reporter-inventory budget plus the primary-write budget plus a one-second response margin, currently twelve seconds per attempt. Remote reporter inventory reads allow the write budget plus a one-second transport margin, while ordinary directory peer reads retain their 1.5-second deadline. Only the narrow safe inventory-busy response is forwarded; unrelated peer failures remain generic. Relay post and self-state clients decode exhausted busy responses as ConnectStoreBusyError, preserving operation and timeout fields without retrying the exhausted post or copying the server-private cause. Optional hue allocation and permission finalization share a wait of half the peer-directory deadline. A server-scoped, single-flight enrichment owner uses a separate SQLite connection, so a pending hue finalizer neither delays the uncoloured directory fallback nor queues later mandatory activity observations. Shutdown interrupts the worker, joins for at most two seconds, then closes its connection; an uninterruptible overrun logs HueFinalizerShutdownTimeout and accepts no new work. A blocked-notification pass stops admitting new store work after its first episode-store or subscription-store failure, drains already-reserved provider sends and records their definite outcomes, then ends the pass rather than repeating native lock waits for every delivery; provider failures remain isolated. Work, Code references and Fleet jobs retain their own connection settings. WAL, FULL synchronous mode, owner-only database and sidecar permissions, and initialization-before-WAL migration order are preserved.

Episodes, settings, presence, owner-scoped acknowledgements and delivery reservations persist in the hub's owner-only SQLite database. The episode key includes host, stable agent ID and episode start. Reservations include the private subscription revision and initial/reminder slot; raw subscription coordinates never enter the notification payload. Reservation precedes Web Push, so a restart or ambiguous transport failure cannot duplicate an accepted notification. A definite non-2xx rejection releases its slot for the next pass, up to three attempts, then records failure. A 404/410 or endpoint-policy rejection removes the subscription and its blocked send rows. Removed or replaced subscription revisions lose their send rows on the next worker pass. Closed episodes and their payload, acknowledgements and send rows expire after seven days; open episodes retain their deduplication state. Only host, agent ID and the latest episode start survive closed-episode pruning to keep starts monotonic. Suppression consumes no slot.

The canonical hub renders the Relay module shell. Fleet contains four tabs: Approvals, Connect, Work and Usage. Approvals contains only local decisions and remote approval handoffs. Connect embeds the package terminal and authoritative relationship tree, and is the hub's one agent list: each agent's state, its work, and its terminal. The hub has no coordinator chat: it has no /v1/chat route, and its page polls no chat. A coordinator chat job queued before the upgrade still runs through Fleet's runCoordinatorChat, and its reply stays on its job record. Work renders the durable @knpkv/herdr-work projection and job history; it does not list agents. The built PWA assets keep offline installation, push subscription management, notification click routing, and the same authenticated browser routes.

The hub installs as Relay: the manifest's name and short_name, the masthead and the page titles say Relay ("Relay", "Connect in Relay", "Work in Relay", "{host} on Relay"), while id, start_url and scope stay / so an earlier install carries over. The module shell and installed app use the approved M2 Flat-plus Chef logo. Its circular body uses pixel-specific 32px desktop and 24px phone drawings, with instance-local clip identifiers. The simplified 16px favicon is served at /assets/relay-favicon.svg; the scalable app tile remains /assets/relay-icon.svg, also available at the legacy /assets/approval-icon.svg. PNG install icons are 192 and 512 pixels, with a 512 maskable and 180 apple-touch icon using opaque full-bleed backgrounds. The 96px white-on-transparent notification badge comes from the forced-colour outline. Its system colours are resolved before rasterisation. CI has no rasteriser, so the PNGs are committed under icons/ with source SVG and PNG hashes in icons/icons.json. After changing the approved drawings or icon layout, run pnpm --filter @knpkv/herdr-hub render:icons, which needs rsvg-convert; the icon test fails until the committed PNGs match. hostd reads each PNG when it is asked for, so a missing one answers 404 (the manifest's SVG icon covers it) and never stops hostd; installing it later needs no restart. The LAN Work listener serves none of them.

Relay in the hub

The canonical listener mounts Relay at /v1/relay/* (events, session, backends, messages, cancel, decisions), the routes @knpkv/relay-product/client reads with makeRelayClient({ base: "/v1/relay" }). It holds one conversation, { product: "herdr", kind: "fleet", id: <hub host> }; another ref is 400. Every route passes the listener's Tailscale identity check, and every POST its origin check, before Relay starts. A decision lets a waiting tool call run, so decisions also needs a node in approvalNodes, like a job's approve or reject: fleet hosts run agents under the same login and can set any Origin. The local listener refuses every decision with 403, since loopback is the hub's own agents; the tailnet, approval, LAN Work and local listeners don't serve them. Relay can only read: list_agents (up to 50, most recently active first), list_pending_approvals (read from each host's 64 most recent waiting jobs, listed oldest first, at most 25), get_job (this hub's jobs and other hosts' pending ones among those 64; state only, never output) and get_work_board (up to 30 goals from the Work service's now snapshot; total includes the older goals a large board leaves out but not goals finished more than a day ago, and notShown counts every goal missing from the answer). Free text is cut to 160 characters and each answer to 24 KiB. It cannot approve, decline, prompt an agent or submit a job; it points the person at the Approvals tab. Turns run on the hub host's own Claude Code or Codex CLI login.

On the canonical page the static Chef is Relay's launcher. Its expanded button styling indicates that the panel is open. Status words in the panel and the existing closed-panel live region convey what Relay is doing ("Working…", "Relay needs you", "Relay replied"), announced only while the panel is closed, and only when what Relay is doing changes: a run is announced once as it starts working, not at each tool it reads with. Once the panel is open, the transcript announces each run's end. The panel opens with the launcher or Ctrl/⌘+J, as an overlay on wide screens and full screen under 768px, and Escape closes it when focus is inside Relay. While Connect shows, its terminal keeps Ctrl/⌘+J, so the launcher advertises no shortcut there. A failed run says why in Relay's own words, never the backend's message.

hostd loads Relay with a dynamic import on the first Relay request. Sessions live in <stateDirectory>/relay/sessions.sqlite (owner-only directory and file). When Relay can't run (a broken install, another hostd holding the store, a linked directory) every Relay route answers 503 RelayUnavailableError with the fix while the rest of the hub keeps working, and the next request tries again.

fleetctl work attach-pr HOST ATTACH_JSON posts a narrow attachment request to POST /v1/work/attach-pr using the same local Work listener or authenticated canonical hub as work record. The request contains goalId, expectedGoalEventId, reviewUrl, exact SHA head, branch, canonical repository as owner/name, occurredAt, and eventId. It accepts no full or partial goal. Work atomically checks the latest event, stored branch/repository, active state, and existing review URL while deriving the checkpoint from stored fields. Concurrent owner or terminal-state updates fail with HTTP 409 and WorkPullRequestAttachConflict; fleetctl prints that cause tag and exits nonzero. An identical URL already attached succeeds without writing, even when the expected event is old, and returns the latest stored checkpoint. Browser-origin requests are rejected.

fleetctl work attach-pr HOST '{"goalId":"goal-example","expectedGoalEventId":"created","reviewUrl":"https://github.com/example/project/pull/7","head":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","branch":"feat/example","repository":"example/project","occurredAt":2000,"eventId":"pr-attached"}'

fleetctl work record HOST CHECKPOINT_JSON decodes one WorkGoalCheckpoint locally, then posts it to the target host's fixed POST /v1/work/checkpoints endpoint. With crossHost: false, Work uses a Work-only listener on the configured loopback workBindAddress (127.0.0.0/8, 127.0.0.1 by default) and port. The listener has no authentication of its own: configuration rejects non-loopback bind addresses and the listener refuses any non-loopback peer. Its routes are the schema-decoded GET /v1/work snapshot, originless typed checkpoint POST, and originless typed PR attachment POST. Wildcard bind addresses are rejected, and requests must use the exact configured listener authority so an unconfigured DNS alias cannot read the snapshot. With crossHost: true, Work commands target only the canonical approval hub, which serves the read-only snapshot, typed checkpoint route, and typed PR attachment route through its authenticated tailnet/TLS surface. An omitted fleetctl work snapshot host targets the canonical hub in cross-host mode and the local host otherwise. Checkpoint writes reject browser-origin requests and accept no job, command, file, or partial-goal shape. A checkpoint containing an approval target requires cross-host approval to be enabled, the host to be configured, and the approval page to resolve through the configured hub or an online Tailscale peer; its URL origin must exactly match that resolved page before persistence. Disabled cross-host approval, unknown or offline hosts, Tailscale failures, malformed approval pages, and origin mismatches return typed validation or operation failures without recording a new checkpoint. An exact persisted replay remains idempotent when its approval page is temporarily unavailable; changed content with the same event ID or goal timestamp returns WorkCheckpointConflictError.

Set the optional agentUsageLimitsCommand in the fleet configuration (for example ["/home/me/.local/bin/agent-usage", "limits"]) to show this host's Claude and Codex limits in Connect. The command must print agent-usage's LimitsNow line; agent-usage limits reads it from the running agent-usage over its owner-only control socket, so no provider credential or session cookie passes through hostd, and balances and spend never leave the host this way. hostd runs it once at start, then serves its last read and reruns it in the background once that read is 30 seconds old, so a slow run never delays an answer. GET /v1/connect/limits returns that read on every listener that serves the dashboard (local, tailnet, the approval listener for approvalNodes, and the hub); on the canonical hub it also lists every peer's read, fetched from each peer's tailnet GET /v1/connect/limits/local, which only the approval hub's node may call. A host without the command, a failing or slow command, a newer agent-usage format (unsupported_version), or output that is not agent-usage's answer is reported as Unavailable with its own reason. Its detail is one of a fixed set of sentences hostd chooses: agent-usage's own stderr and a spawn error can name host paths (the control socket under the user's home, the executable), and a peer's reading crosses the tailnet, so that text is logged on the host and never sent. agent-usage and hostd are pinned separately, so within the same format a snapshot this version cannot read (a newer source or reason) is left out and counted in skipped rather than blanking the host (a failing command is reported by the matching fixed sentence, such as "is not running on this host"). A peer the hub could not ask is listed under failures. If the hub cannot list the fleet at all, it answers with its own read and peersListed: false. An unknown window keeps its reason and is never reported as zero use.

The same command gives the Usage tab its history: hostd swaps its final limits for usage (a command that does not end in limits has no usage, and says so) and runs agent-usage usage --range <24h|7d|30d> --time-zone <zone>. The answer is agent-usage's UsageNow: tokens per period, agent and model, and the limit series. agent-usage leaves cost, balances, Bookings (tickets and repos), sessions and paths out at the source, so none of them crosses the tailnet. GET /v1/connect/usage?range=7d&timeZone=Europe/Amsterdam answers like /v1/connect/limits, every peer included on the hub through its tailnet GET /v1/connect/usage/local with the same query. A range other than 24h, 7d or 30d, or a zone that is not IANA-shaped (letters, digits, _ + - /), is refused with 400 before any command runs. Each range and zone is read once (concurrent first asks share that read), served from its last read and refreshed in the background after five minutes; at most 16 pairs are kept, the least recently asked dropped first, and at most two agent-usage reads run at once. The hub waits 25 seconds for a peer, longer than the peer's own 20-second cold read, so a first ask gets the read rather than a timeout.

Set the optional lanWork object in the fleet configuration to opt into a LAN Work listener (or pass the equivalent HttpServerOptions.lanWork when embedding the server). It exposes /pair, /, and GET /v1/work, plus the static Work stylesheet at /assets/index.css and configured .woff2 fonts. The server prints a lowercase 64-character code after the listener is ready; the code is accepted once in a POST body, expires after five minutes, and is never placed in a URL. The paired session is an HttpOnly cookie. Approvals, Connect, secrets, agent control, and job submission remain unavailable on this listener. Top-level GET / and GET /pair navigations may omit Origin; the pairing POST /pair and authenticated GET /v1/work API require the exact listener origin and reject missing or different origins before reading or authorizing the request.

LAN Work deliberately serves plain HTTP and is suitable only for a trusted LAN segment. It does not provide transport confidentiality; put an authenticated TLS proxy or equivalent private network boundary in front of it before exposing pairing codes or session cookies across an untrusted link. Provider locators remain server-private; LAN Work exposes only normalized client-visible records or recorded-target status.

Relay modules

Primary module links open the descriptor's default page with no previous object selection or filter. The module section stays mounted while inactive. Page-local state survives a page switch only when the pages render a shared owner component, as Fleet does; distinct page components remount. The canonical hub migrates a historical destination only after its source page is retired and its replacement page is installed.

@knpkv/herdr-hub/modules is the browser-safe module contract. Product packages export their page components through a browser entry such as ./views or ./surface. The shell supplies those components to makeModuleViews; every page declared by the shared ModuleDescriptor needs an implementation. The module id is its hash namespace, and page ids are its route segments. moduleHref(descriptor, page, search) checks page ownership in TypeScript. readModuleRoute(url, descriptors) decodes the registered hash grammar with Schema, including a bare #module alias for its default page. Unknown hashes return ModuleRouteNotFound; the shell shows a not-found page and retains the requested URL until the reader chooses its landing link.

canonicalModuleUrl converts existing ?tab=approvals|connect|work|usage links into #fleet/<tab> while retaining object selectors such as goal, window, approvalHost and approvalJob. An existing hash takes precedence. The function returns a URL without navigating; the shell applies it with history.replaceState before its first paint. It accepts an explicit landing destination for an embedding shell. It leaves the document path, authentication routes, push links and service-worker scope intact. The canonical shell uses these routes now. Tab clicks replace the current history entry; module changes and purposeful cross-links push an entry, so Back restores the previous target. A first-load document anchor such as /#main is retained while the landing page renders. During a session, ordinary anchors retain the current page. The canonical shell maps same-origin /connect/?agent=…&host=…&open=stage handoffs to Connect home when installed, otherwise legacy Fleet/Connect; direct visits to /connect/ retain the standalone surface.

@knpkv/herdr-hub/modules/server supplies the separate Effect contract. A product exports a ServerModule with the same descriptor and a layer providing ModuleHandlers. Acquire stable services in that layer; route handlers must have no remaining service requirements. makeModuleLayer validates descriptor agreement and API path uniqueness, builds each product layer within the application's scope, and provides ModuleRegistry. moduleReadRoute and moduleWriteRoute require response schemas; mutation constructors also require a body schema. Handlers stay in a private registry; copied route metadata cannot replace schema decoding. Products and the mount must share one runtime instance of this server entry. A route constructed by a second bundled copy fails closed with ModuleRegistrationError reason invalid_route, just like an unrecognized route identity. Response encoding and body decoding failures remain typed ModuleRouteFailure values. Module handlers return sanitized authenticated client-visible data; raw provider coordinates, credentials, account identifiers and host paths remain server-private and must never enter that response, except the Sandboxes account id, which stays on that module's authenticated card and run page, the requester login on a pending sandbox request, which crosses only the authenticated GET /v1/modules/sandboxes/requests, and a run's log tail and stored failure line, which cross only GET /v1/modules/sandboxes/run-log to the owner.

The shared descriptor's requires field only describes UI capabilities. It grants no authority. The HTTP owner's readJson callback returns schema-validated JSON through the existing content-type and body-budget checks. It passes its listener's authorization and origin checks to dispatchModuleRoute, which authorizes before reading bodies or running handlers and checks the origin for every mutation. Descriptor listeners restrict mounting further. For /v1/modules/*, the HTTP owner must answer 404 when dispatch returns null; it must not pass that namespace to another handler. The Work peer listener retains its existing restricted API and does not mount modules. The canonical hub mounts these routes with the same authorization as /v1/dashboard. Other listeners return 404 for the Connect summary namespace. ModuleListener permits serve, approval, tailnet and LAN; product descriptors cannot register the Work peer listener. HttpServerOptions.modules appends product layers through this same validation and lifetime boundary. Existing Fleet and Relay endpoints retain their handlers.

Route constructors now require an explicit query codec: moduleReadRoute(path, query, success, handle) and moduleWriteRoute(method, path, query, body, success, handle). Build it with Schema.fromURLSearchParams; handlers receive decoded selectors rather than raw search parameters. Excess keys are rejected with 400 invalid_query. Use Schema.fromURLSearchParams(Schema.Record(Schema.String, Schema.Never)) for a route accepting no selectors. An empty Schema.Struct({}) accepts extra keys, so it does not express that boundary. Numeric selectors requiring finite values use Schema.FiniteFromString. Existing callers must supply the query codec when updating.

Browser shell mounting

SuperappShell is exported from @knpkv/herdr-hub/views. Supply validated ModuleViews, an explicit registered landing destination, the current host, configured host count, notification content and the existing HubRelayConversation. Tailscale authentication remains on the hub listener. The shell adds no browser login. The noncanonical dashboard retains its existing view.

Each page function receives (search, mount). mount.active says whether its module is visible; inactive modules retain their last page at the same module-keyed position. mount.navigate(moduleHref(descriptor, page, search), "push" | "replace") validates a same-document destination before changing history. Its optional third argument defaults to "page" focus; null retains focus during in-page selector updates and "search" targets Connect search. An invalid destination displays a route notice and adds no history entry. Existing page implementations accepting only search remain assignable. A null focus update also retains any pending arrival focus through an immediate query repair; it does not start a new focus request. SuperappShell.mobileModules lists installed module ids shown directly in the phone bar, defaults to all installed modules, and leaves the rest behind All modules with its current-module name. Products must stop conditional subscriptions when inactive, and use the same root component for their pages when state must survive page changes. Fleet builds every page from one root and keeps Approvals mounted.

FleetShell retains its legacy behavior when no new props are supplied. Optional navigation supplies the selected tab and its change callback; the parent then owns URL/history, link interception and global shortcuts. embedded defaults to false; true removes the Fleet header, launcher and shortcuts dialog so the parent owns them once. active defaults to true; false unmounts Connect, Work and Usage while keeping Approvals and its decision state. The shell retains Ctrl/⌘+K agent search and Fleet's g sequences. One parent Relay launcher and conversation survive every module change; the active Connect terminal retains Ctrl/⌘+J. Existing RelayDock compatibility exports remain intact.

HubRelay.launcherLabels optionally supplies desktop and phone text. The shell uses "Ask Relay" on desktop and "Relay" on the full-screen phone presentation, with "Ask Relay" as the accessible name in both. Omitting the prop retains the existing "Relay" label.

The shell opts into HubRelay.launcherMark="chef", a static decorative mark at 32px desktop and 24px phone. The default creature remains independent of launcherLabels and keeps the legacy launcher. HubRelay supplies the shared Creature with the fixed generic identity relay / assistant to its panel header and live Relay turn: pending confirmation needs you, a running tool thinks, streaming text answers, and a quiet focused composer listens. Historical turns stay idle. Existing status words remain authoritative; listening adds plain text inside the panel and adds no live region. Closing the panel clears listening.

For target panels opened through mount.navigate(href, "push", null), call mount.closeSelection(parentHref) on Close or Escape. Code uses it to clear its target while retaining queue selectors. The shell uses Back only when the current entry records that exact canonical parent and this shell lifetime's nonce; direct links, reloads and remounts replace instead. Query-key order and a bare module hash do not change parent identity. Filters remain replace navigations. The closing modal restores focus after cleanup; other Back/Forward navigations retain the shell's page-focus policy. Every shell history write preserves foreign object-state keys, and replace retains the selection's origin. External module-mount hosts must supply this new callback.

Connect home and flat navigation

The canonical hub opens Connect at #connect/overview. Its flat sidebar lists Connect, Fleet, Code, Host resources and Stacks, in that order, with installed apps placed among them (Usage after Code). The phone bar keeps Connect, Fleet and Code, plus apps that ask for it (Usage does); Host resources and Stacks are available through More. More also contains settings and any other installed module outside the phone bar; it has no placeholders or Appearance control. primaryModules optionally groups the sidebar's primary destinations and is typed against the registered views. The required session prop supplies access (tailscale or loopback) and connection state. Connected shows the access label and green dot; stale carries observedAt and shows the retained reading time while reconnecting; unavailable means there is no reading. The dashboard composition always has its bootstrap and therefore emits stale, rather than unavailable, after a failed refresh. terminalOwnsKeys defaults to false; only an active connected Connect terminal reserves Ask Relay summon keys. Inactive module portals must follow mount.active while their owners stay alive.

Canonical Fleet uses fleetHubModule, retaining only Approvals and Work. The standalone fleetModule keeps all four legacy pages. Connect home uses the existing Connect atoms and owner for the directory, stage and terminal. Ready and Idle share the Ready group; unknown states and remembered offline-host agents appear in Unknown with caution words. Recently done carries no claim about completion today. The approvals strip subscribes to the existing directory snapshot and polls the same bounded approval collector separately; approvals and agents have distinct counts.

Historical #today, #today/overview, #fleet/connect and ?tab=connect destinations migrate to Connect home only when the old page is absent and Connect is installed. #fleet/usage and ?tab=usage then migrate to Usage By tool, retaining selectors. A source that remains registered stays reachable. The public todayModule and TodayView exports remain available for other embedders, but Today is absent from both canonical hub registries, and its old API returns 404.

Host resources

#resources/overview opens read-only observations for the host serving this hub. GET /v1/host/resources uses the same listener authorization as /v1/connect/usage, sends Cache-Control: no-store, and never aggregates peers. The exported HostResources schema and HostResourcesView describe the wire snapshot and presentation. Each section is a Reading or an Unavailable with a reason, so a failed source does not erase other readings.

hostd samples Linux memory, CPU/load, filesystem capacity, gate holders, heavy jobs, preview listeners and RSS by pane every ten seconds. Memory and CPU histories retain at most 360 timestamped samples in memory for one hour; restarting hostd starts fresh history. Memory uses MemAvailable, with exported thresholds of 8 GiB for heavy start and 3 GiB for abort. The browser polls only while this module is active and visible. Its own clock marks a sample stale after more than 30 seconds. A failed refresh retains the last values with an offline label; no previous sample produces the no-reading state.

Set the optional fleet field heavyGate.lockDirectory to an absolute directory containing heavy-gate.lock, heavy-gate-2.lock and, when present, full-gate-waiting. There is no default. Omission makes gate and heavy-jobs sections Unavailable with reason: "not_configured". For example:

{ "heavyGate": { "lockDirectory": "/run/user/1000/heavy-gate" } }

Gate inspection is read-only. A lock record in a descriptor's own fdinfo proves retention after exact resolved-path and fstat device/inode checks, including after the acquiring process exits. Global kernel records only corroborate ownership; PID 0 and an absent global record do not erase a verified holder. Each slot reports Held, Free or Unknown, all proven retaining PIDs and its primary topmost retaining root. Exited processes and ordinary pipe, socket or anonymous descriptors do not prevent a free observation. Newly appeared PIDs are scanned directly; a retainer without process metadata, unreadable ownership evidence or an unresolved kernel lock yields Unknown with reason: "ownership_unknown". A missing or unreadable configured directory makes gate and jobs unavailable; absent lock files inside a readable directory remain free. Unknown slots show no holder facts; Heavy jobs contains only proven roots. Heavy jobs lists every distinct proven root once, with disjoint process/RSS membership, including roots sharing a slot.