Skip to content

feat(server): list open sessions on the landing page - #310

Open
flpdorea wants to merge 6 commits into
kunchenguid:mainfrom
flpdorea:fm/lavish-axi-session-list
Open

feat(server): list open sessions on the landing page#310
flpdorea wants to merge 6 commits into
kunchenguid:mainfrom
flpdorea:fm/lavish-axi-session-list

Conversation

@flpdorea

@flpdorea flpdorea commented Sep 1, 2026

Copy link
Copy Markdown

Closes #308

Security note - reviewed and explicitly accepted, not auto-fixed

The no-mistakes review step flagged that a landing-page session link does more than reveal a file name: visiting /session/:key (now one click away from an unauthenticated root page, where before it required already knowing that exact URL) immediately calls issueReviewerHandoff and silently supersedes whatever browser tab currently holds that review, with no confirmation step. This is not new behavior introduced by this PR - /session/:key already worked this way for anyone who already had the URL - but the landing page does meaningfully lower the bar by making every open session's URL discoverable with zero prior information, within the existing "unauthenticated server, trust the network" threat model this repo's README already documents (see the Network binding bullet).

This was raised to the project owner as an explicit ask-user gate (not resolved by the agent) and the decision was: ship as-is, as a discoverability change within an already-accepted threat model rather than a new class of vulnerability, with this note added specifically so the maintainer sees it during human review too. The full original finding is preserved verbatim in the collapsed Review section below.

What Changed

  • The / landing route now reads visible sessions from the store and renders each as a row (status dot, truncated file path, "Open" link) instead of the static placeholder card; the empty-list case is unchanged.
  • Extracted filterVisibleSessions in src/session-store.js (drops status === "ended" sessions) and reused it from both src/cli.js's visibleSessions() and the new server route, so the CLI's home output and the landing page can't diverge on what counts as "open".
  • The landing page auto-refreshes every 15 seconds via a <meta http-equiv="refresh"> tag, but only when the session list is non-empty, so an idle empty page never reloads out from under a viewer.
  • The store read is wrapped to fail open: a state.json read error falls back to an empty session list instead of a 500, since / is unauthenticated and must stay available.
  • README documents the new landing page session list behavior.

Risk Assessment

✅ Low: This fix round only added one accurate documentation line to README.md with no functional or source code changes; the prior security finding was already reviewed and explicitly accepted by the user in round 1.

Testing

Installed missing dependencies (chokidar etc. via pnpm install, which the worktree lacked) then ran the three targeted test groups covering this change end-to-end: the landing page correctly renders the empty-state placeholder with no refresh tag when no sessions are open, lists open sessions with escaped file paths/status dots/working session links and a 15s refresh tag when populated, excludes ended sessions while keeping order, HTML-escapes a maliciously crafted file path, and the session link resolves correctly cross-host under simulated Tailscale binding (validating the relative-link design decision). The extracted filterVisibleSessions helper is also unit-tested directly. All 7 relevant tests passed; no code or test changes were needed, and the working tree is clean (node_modules/dist installed for testing are gitignored, not tracked).

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → 1 auto-fixed, 1 escalated and accepted as-is
  • ⚠️ src/server.js:530 - The new / route is unauthenticated (protected only by the Host allowlist, not by session-key possession) and now lists every open session's real filesystem path plus a one-click /session/:key link. Visiting that link immediately calls issueReviewerHandoff, which mints a fresh reviewer token and supersedes whatever browser tab currently holds the review - with no confirmation step. Previously an attacker/curious user needed to already know (or receive) the exact artifact path to compute or be given the session key; now anyone who can reach the bound interface (loopback-sharing users, or any device on a Tailscale-bound tailnet per the documented non-loopback warning) can enumerate every open review and hijack any of them in one click, with no opt-out env var. The commit message documents this as a deliberate accepted default, but that acceptance appears to have been made unilaterally rather than confirmed with the project owner, and this materially lowers the bar for reviewer-session takeover within the existing 'unauthenticated server, trust the network' threat model.
  • ℹ️ README.md - The landing page session list is a new user-facing feature (file paths, status dots, refresh behavior, session links) but README.md's user-facing contract was not updated to describe it, contrary to AGENTS.md's documentation-ownership rule that README owns features and their behavior.

🔧 Fix: docs(readme): document new landing page session list (applied to the first finding only; the second was escalated to the project owner and approved as-is - see the Security note above)
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • node --test --test-name-pattern &#34;landing&#34; test/server.test.js (5 tests: empty placeholder, single session listing, multiple sessions excluding ended, HTML-escaping of malicious file paths, health/landing responsiveness)
  • node --test --test-name-pattern &#34;Tailscale mode binds&#34; test/server.test.js (verifies the session link on the landing page is relative/host-independent and resolves correctly when fetched via the MagicDNS host, per issue #216)
  • node --test --test-name-pattern &#34;filterVisibleSessions&#34; test/session-store.test.js (unit test for the extracted shared filter used by both the CLI home output and the new landing page)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

visibleSessions() in cli.js filtered out ended sessions with a private
one-liner. The upcoming server-side landing list needs the exact same
filter, and a second copy of `status !== "ended"` is exactly the kind
of thing that quietly drifts between the two surfaces over time.

Export it once from session-store.js so the CLI's home output and the
server's landing page can never silently diverge on what "open" means.
GET / always rendered the same static "Lavish Editor is running" card,
regardless of how many sessions were actually open - useful only if you
already have the CLI to hand. On a phone via the tailnet root there is
no CLI at all, and even on desktop it's convenient to just open the
root URL and see what's live.

The route now reads the same session inventory the CLI already prints
(filterVisibleSessions() over store.listSessions()) and renders a
minimal read-only list when any are open: a status dot, the file path
with the home directory elided the same way the chrome overflow menu
already does (displayPathParts), and a link. The empty state is
byte-identical to today's placeholder.

Decisions worth recording:
- Read failures fail open to the placeholder rather than 500ing - this
  route is unauthenticated and always reachable, so it must never hang
  or error on a bad state.json read.
- Each link is built fresh as a relative /session/{key} at render time,
  never the session's stored `url` field. That field is captured once
  at POST /api/sessions time against whatever host resolved then, and
  would go stale relative to whichever host actually served this page
  once the tailnet root (kunchenguid#216) exists alongside plain loopback.
- No opt-out env var. This is a deliberate new-default change, accepted
  as such, so no extra config surface was added for it.
- The populated list carries a 15s <meta http-equiv=refresh> so it
  stays current without a new SSE surface; the empty placeholder never
  carries it, since there is nothing there to go stale and reloading it
  would disrupt someone leaving the tab open for later.
- session.file is a filesystem path an attacker could shape (this route
  has no auth beyond the Host allowlist), so every user-controlled
  string is escaped with the existing escapeHtml().
Only one existing test touched the landing route at all (the Tailscale
dual-listener test), and it only ever saw the empty placeholder. Extend
it to assert the session it already opens shows up in the list with a
working relative link resolved against the MagicDNS host - proving
tailnet-root compatibility rather than merely asserting it.

New coverage:
- empty state stays byte-identical to today's placeholder, no refresh tag
- a single open session renders file name, status dot, working link, and
  the refresh tag
- several sessions list correctly and an ended one is excluded
- a file path containing markup-significant characters is escaped, not
  rendered raw - this route has no auth in front of it
- filterVisibleSessions as a pure unit test: keeps open/feedback,
  drops ended, preserves order
@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "test(server): keep XSS payload slashes o..." | Re-trigger Greptile

@flpdorea

flpdorea commented Sep 1, 2026

Copy link
Copy Markdown
Author

Adding a couple of screenshots here to make this easier to review — here's what it looks like running locally:

Empty state (no sessions open):
empty-state

Populated state (2 sessions open):
populated-state

Ran the full test suite locally as well and everything's green. Happy to adjust anything if you'd like it done differently!

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Diff-reviewed head 007e76d1 (Closes #308). First-time fork CI / Guard / Require no-mistakes approved after that review (runs 33548055260, 33548055270, 33548055274). No workflow-file changes. Attestation matches head. Greptile already ran; not pinging.

This is a captain-decision hold, not waiting on the author. Do not merge from this pass.

Why hold

  1. contract-class: new-default. Unconfigured GET / now lists open sessions (file path, status, one-click /session/:key) instead of today's static placeholder. Commit message and body are explicit: no opt-out env. new-default does not auto-merge.
  2. Security (flag). The landing page is still only Host-allowlisted (no session-key gate). Listing every open session's path plus a relative /session/:key link lowers the bar from "already have the URL" to "anyone who can reach the bound interface can enumerate and open every live review." On main, GET /session/:key already calls issueReviewerHandoff (fresh reviewer token / chrome load) with no confirm step — that behavior is not new, but discoverability is. Fits the README Network-binding "unauthenticated server, trust the network" model, and still needs an explicit captain word because it is a real discoverability change (especially once feat: serve live sessions to other Tailscale devices via Tailscale Serve #216's tailnet root is in play). The PR's no-mistakes "ask-user → ship as-is" note is not a captain yes from this triage.

VISION.md (per rule)

  • The artifact stays the author's — aligns. Landing/chrome root only; no extra injection into the saved HTML; direct-open of the file itself unchanged.
  • Interaction beats prose — aligns. Thin pointer board into live review sessions, not a prose substitute for the artifact.
  • An artifact's design is chosen, never defaulted into — aligns. Does not restyle artifacts or change design guidance.
  • Nothing interrupts the human — aligns for the review loop itself (landing is outside /session/:key). Note: populated list uses a 15s meta refresh only on /; empty placeholder does not. Fail-open on state.json read matches fail-open guidance.
  • Every token is spent on purpose — aligns. Human-facing HTML; no new per-run agent token surface. CLI home still uses the same filter.
  • The instructions are the product — aligns. README owns the new landing contract (one line added). Shared filterVisibleSessions keeps CLI and landing from drifting.
  • Scope — aligns when kept read-only / this-server-only (matches Feature: read-only session list at the server root (local + tailnet), companion to #215/#216 #308 out-of-scope). Cross-project grouping or manage-from-browser would re-open feat - Single dashboard for all lavish sessions #243. One-person / many-screens listing is in VISION; the enumeration/handoff discoverability call is the captain product/security decision above, not a second-person collab feature.

No auto-merge. No rebase. #216 left alone (do-not-re-flag). Flagging Firstmate for the security + new-default decision.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Follow-up on CI after the fork approvals: Guard and Require no-mistakes are green. Ubuntu and macOS build-and-test are green. windows-latest is red on the new landing XSS test (landing page escapes a session file path containing markup-significant characters): mkdir of a directory literally named <script>alert(1)</script>&"' fails with ENOENT on Windows (invalid path characters). That is a test-fixture issue, not a product bug in the escaping path. Please fix the Windows fixture (use a Windows-legal path that still exercises escapeHtml) and push.

Until that is green, part of this PR is waiting on the author. Separately, the captain-decision hold in the previous comment still stands for new-default + the session-enumeration / handoff discoverability call — that is not waiting on you, and it is not cleared by greening CI.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate.

Captain accepted the new-default landing (security discoverability within the trust-the-network model). Do not treat that as merge approval yet.

Still blocking merge:

  1. windows-latestlanding page escapes a session file path containing markup-significant characters fails with ENOENT mkdir of a path containing <script>… (invalid on Windows). Please fix the fixture so it stays a cross-platform escapeHtml regression without mkdir'ing Windows-forbidden characters (rewrite the stored session path after a normal open, or equivalent). We may also push a small maintainer fix onto this branch.
  2. Live screenshot review — Firstmate still needs empty + populated landing screenshots for the captain before any merge.

CI otherwise: ubuntu/macos + Guard + no-mistakes are green on 007e76d1.

cursoragent and others added 2 commits September 2, 2026 04:41
Windows forbids <>:"|?* in path names, so mkdir of a <script> directory
fails on windows-latest. Open a safe artifact, then rewrite session.file
in state.json to keep the escapeHtml regression without an illegal path.

Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
path.join treats `/` in `</script>` as a separator on Windows, so the
stored session.file became `...<\script>...` and the title-attribute
escape assertion failed. Concatenate with path.sep so the literal
payload is preserved on every OS.

Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Recheck of #310 head 84c0715 (closes #308). Do not merge from this pass.

CI (fully green)

  • build-and-test windows-latest — SUCCESS
  • build-and-test ubuntu-latest — SUCCESS
  • build-and-test macos-latest — SUCCESS
  • Guard (Generated files must not be hand-edited) — SUCCESS
  • Greptile Review — SUCCESS (last reviewed 84c0715, confidence 5/5)
  • mergeable: MERGEABLE · mergeStateStatus: CLEAN · no auto-merge

contract-class: new-default (captain already accepted; no opt-out env). Not re-flagging captain.

Hold reason (unchanged policy)
Still holding for Firstmate explicit yes after live screenshot review (empty + populated landing). Screenshots remain valid: post-007e76d1 commits (647a780, 84c0715) are Windows XSS test-fixture only; product landing HTML unchanged. Live PNGs from head 84c0715 were already delivered to Firstmate earlier this pass cycle.

Since last stamp 2026-09-01T21:25:00Z held-captain-and-ci-red

  • Captain accepted new-default / trust-the-network discoverability
  • Windows fixture fixed and pushed; CI newly fully green
  • Screenshots on file with Firstmate; explicit yes still outstanding
  • No merge or rebase in flight (none aborted)

Outcome: held-awaiting-firstmate-yes. Firstmate: CI is clear — please relay explicit yes (or no) after screenshot review before any merge.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate.

Captain skipped the merge card. Skip is not yes. This remains a captain-decision hold — not waiting on the author.

CI is fully green on 84c0715 and landing screenshots stand. Do not merge from this pass. Will only merge on an explicit later yes from Firstmate after the captain.

@flpdorea

flpdorea commented Sep 2, 2026

Copy link
Copy Markdown
Author

Independently re-verified at the actual current head (84c0715), not just relaying the fixture-only claim:

git diff --stat 007e76d1..84c0715 touches only test/server.test.js — zero product source changed. Ran the two demo servers fresh off 84c0715 (empty state, and a populated state with 2 sample sessions) and re-screenshotted both. They match what's already posted on this PR pixel-for-pixel: status dot + truncated path + "Open →" link per row, 15s meta refresh only when populated, none when empty.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Recheck after author comment (independent screenshot re-verify). Tip is still 84c0715 — no product tip move. CI remains green; MERGEABLE / CLEAN.

Standing captain-decision hold (FM-LA-310). Captain skipped the merge card earlier; skip is not yes. This is not waiting on the author. Do not merge, rebase, or re-flag from this pass. Will only merge on an explicit later yes from Firstmate after the captain.

VISION (per-rule): Artifact stays the author's — aligns (landing chrome, not artifact mutation). Interaction beats prose — aligns (session list is the pointing surface for open reviews). Design chosen — aligns (same visual shell). Nothing interrupts — aligns (read-only list + meta refresh when populated). Every token on purpose — aligns. Instructions are the product — aligns. Scope — aligns (one person / local+tailnet server root; not multi-user collab).

contract-class: new-default (unconfigured root landing gains a live session list; captain already accepted the product call earlier — merge still needs explicit yes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: read-only session list at the server root (local + tailnet), companion to #215/#216

3 participants