Repository navigation
feat(webui): one client owns a Mini App at a time - #263
Open
Fectivnfy112357 wants to merge 3 commits into
Open
Fectivnfy112357 wants to merge 3 commits into
Fectivnfy112357 wants to merge 3 commits into
Conversation
This was referenced Oct 26, 2026
First slice of #254. The desktop client writes no ownership record, so for anything already running — including a Mini App the desktop client started — the only signal is the Mini App's own derived port. This adds the probe that reads it. `probeMiniAppPortOwnership` answers with three outcomes rather than a boolean, because the runtime reacts differently to each: `free` (the port is this client's to take), `guarded` (a Mini App's guard rejected the anonymous request, so another owner is running this Mini App and this client must refuse), and `foreign` (something else holds the port and does not answer like the guard, so another port is fine). The answer is the guard's, not the socket's: a port that accepts but answers 200 is `foreign`, not `guarded`. That is the distinction that keeps a stray listener from being mistaken for a Mini App, and it is what the ownership record's liveness check cannot cover on its own. Proven against real servers: a closed port reports free, the guarded fixture reports guarded, and a plain `node:http` server in a child process reports foreign. Still to do for #254: the ownership record with an OS-released liveness proof, the refusal surfaced in the listing and the picker, per-Mini-App granularity across two clients, and refusing before spawn. Verified: pnpm typecheck; pnpm run test:capabilities (237 files, 5827 passed, 18 skipped); pnpm check:source after the inventory write.
…point Part of #254. ADR 0020 required a liveness proof the operating system releases when the owner dies rather than a timestamp, and did not name one. This picks it and records the choice in the ADR's ownership section: a socket file on POSIX, a named pipe on Windows, named from the plugin id the way the OAuth lease endpoint is named from its data directory. Binding is the ownership. A second bind fails while the owner lives and the operating system releases it when the owner dies, so a record beside it states who holds the Mini App and since when, while the endpoint remains the truth. Nothing is unlinked before the first bind attempt — doing that would steal a live owner's endpoint, which is why this does not copy the OAuth lease server's path cleanup. The case that matters is the crashed owner: it leaves a socket file with nothing listening on it. A failed bind is therefore told apart from a leftover by connecting — a refused connection proves the file is stale, and only then is it removed and the bind retried once. An endpoint that still answers is somebody's, and the acquisition reports the recorded holder instead. Proven: acquire and re-acquire after release; a second client is refused with the holder's name; a socket file left by a dead owner is reclaimed; two different Mini Apps are owned at once; the endpoint is named from the plugin data directory rather than a global path. Still to do for #254: wire the acquisition into `prepareRuntime` before the port loop so the refusal precedes spawn, and surface the held state in the listing and the picker. Verified: pnpm typecheck; pnpm run test:capabilities (237 files, 5832 passed, 18 skipped); pnpm check:source after the inventory write.
Part of #254. Ownership is now taken in `prepareRuntime`, before the port loop and before any spawn, so a second client cannot reach the point of starting a process over the same plugin data. The refusal is a `MiniAppNodeRuntimeError` with code `OWNED_ELSEWHERE` whose text names the holder (`client` and `pid` from the record), so the listing has something readable to report rather than a silent failure. The acquisition is released where the rest of the preparation is cleaned up: on stop, on a fatal abort, and on any failure between taking it and returning a runtime — so a failed start does not leave the Mini App locked to this client. Proven through the scripted-child harness: with one client holding the Mini App, a second preparation for the same plugin data directory rejects with `OWNED_ELSEWHERE` and the second child receives no init message at all, which is what "before spawn" means in observable terms. Still to do for #254: surface the held state in the WebUI listing and picker (that is the client half), and decide whether the desktop client's presence needs a record the port probe cannot see — it writes none, so today it is detected only by its port answering like the guard. Verified: pnpm typecheck; pnpm run test:capabilities (237 files, 5833 passed, 18 skipped); pnpm check:source.
Fectivnfy112357
force-pushed
the
feat/miniapp-loopback-guard
branch
from
October 26, 2026 08:33
3332247 to
40cadee
Compare
Fectivnfy112357
force-pushed
the
feat/miniapp-ownership-probe
branch
from
October 26, 2026 08:33
e3cc29e to
fb56967
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #254. Stacked on #262 (
feat/miniapp-loopback-guard), which is stacked on #261. Merge top-down;GitHub retargets each PR as its base merges.
Change
Two clients share one data directory (ADR 0006), and a Mini App's port is derived from its plugin id, so
without a rule both compute the same first port and the second starts a second process over the same
plugin data. This PR decides ownership rather than arbitration: one client owns a Mini App at a time, per
Mini App.
file on POSIX, a named pipe on Windows, named from the plugin id. Binding is the ownership: a second
bind fails while the owner lives, and the operating system releases it when the owner dies. A record
beside it states which client holds it and since when, so
record present, nobody listeningis stale bydefinition rather than by timestamp.
told apart from a leftover by connecting — a refused connection proves it is stale, and only then is it
removed and the bind retried once. Nothing is unlinked before the first bind attempt, which is why this
does not copy the OAuth lease server's path cleanup.
prepareRuntimetakes ownership before the port loop and before anyspawn, and a held endpoint raises
OWNED_ELSEWHEREnaming the holder(
Mini App <id> is already running in <client> (pid <pid>)). The acquisition is released on stop, on afatal abort, and on any failure between taking it and returning a runtime.
record — is detected by
probeMiniAppPortOwnership, which answersfree | guarded | foreignfrom theguard's own answer rather than a bare connect, so a stray listener is never mistaken for a Mini App.
ADR 0020's ownership section now records the mechanism and the reason the endpoint is never unlinked
first.
Validation
pnpm typecheck— passedpnpm run test:capabilities— 237 files, 5833 passed, 18 skippedpnpm check:sourceafternode scripts/source-inventory.mjs --write— 5457 files, passedtest/unit/miniapp/owner-lease.test.ts(5 cases) and the ownership cases insidetest/unit/miniapp/loopback-guard.test.ts(3 cases) andnode-runtime-credential.test.ts(1 case)pnpm verifyin full; browser and release-package steps are left to CINot in this PR
Reporting the refusal in the plugin catalogue's apps area and the picker. That moved to #256 with your
agreement — the WebUI's Mini App service is not enabled yet, and ADR 0020 requires the service cell, the
presenter and the view to land as one change. #256 receives the readable refusal this PR produces.
Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable.Maintainer handoff
Publication scope or license changes: none.
Shared-source port: not needed — the endpoint naming follows this repository's own
@mavis/oauth-lease-protocolconvention rather than copying internal code.