Skip to content

fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST#442

Open
wyh0626 wants to merge 2 commits into
rohitg00:mainfrom
wyh0626:fix/434-fly-viewer-bind
Open

fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST#442
wyh0626 wants to merge 2 commits into
rohitg00:mainfrom
wyh0626:fix/434-fly-viewer-bind

Conversation

@wyh0626
Copy link
Copy Markdown
Contributor

@wyh0626 wyh0626 commented May 17, 2026

Summary

Closes #434. startViewerServer hardcodes server.listen(port, "127.0.0.1"). On Fly, traffic from fly proxy enters via the fly-local-6pn WireGuard interface, not loopback, so the kernel RSTs the connection. That's the curl: (56) Recv failure: Connection reset by peer the issue reports.

Fix

A two-line helper:

export function resolveViewerHost(): string {
  return process.env.AGENTMEMORY_VIEWER_HOST?.trim() || "127.0.0.1";
}

startViewerServer calls it once and uses the result. The default stays 127.0.0.1, so every local install is unaffected and the v0.9.12 plaintext-bearer guard (which assumes bearer tokens only ride loopback) still holds.

The Fly image opts in by setting AGENTMEMORY_VIEWER_HOST=:: in deploy/fly/Dockerfile. That makes the viewer listen on fly-local-6pn (IPv6) as well as loopback. fly.toml is untouched: port 3113 is still not declared as a Fly service, so the viewer remains reachable only through fly proxy (mTLS over WireGuard).

Verify

Unit:

npx vitest run test/viewer-host.test.ts
# 6/6 pass

End-to-end on Fly. I built the patched image locally with npm pack + a tarball install in the Dockerfile, deployed to a Singapore machine, then ran the exact reproducer from the issue:

fly proxy 3113:3113 --app "$APP"

curl -I http://localhost:3113/
# HTTP/1.1 404 Not Found
# (before fix: curl: (56) Recv failure: Connection reset by peer)

curl -s http://localhost:3113/ | head -c 80
# <!DOCTYPE html>...<title>agentmemory viewer</title>

curl -s http://localhost:3113/agentmemory/livez
# {"service":"agentmemory","status":"ok"}

HEAD on / returns 404 because the viewer only registers a GET handler there; GET serves the dashboard HTML. The signal is that the TCP+HTTP handshake completes at all, no RST. App and volume torn down after.

Tests

6 cases in test/viewer-host.test.ts:

  • resolveViewerHost defaults to 127.0.0.1 when the env var is unset
  • same when it's an empty string
  • returns the configured value when set
  • trims surrounding whitespace
  • startViewerServer actually binds to 127.0.0.1 by default
  • startViewerServer binds to ::1 when AGENTMEMORY_VIEWER_HOST=::1

Summary by CodeRabbit

  • New Features

    • Configurable viewer network binding for non-loopback deployments and gated API access requiring a Bearer token; static assets remain publicly viewable
    • In-browser auth prompt with session-persisted token automatically attached to API requests
  • Documentation

    • Expanded Fly.io deployment guide with networking, allowed-hosts, auth requirements and secret-retrieval fallback
  • Tests

    • Added comprehensive tests for host binding, inbound auth gating, token storage, and runtime behaviors

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR makes the viewer bind host configurable (via AGENTMEMORY_VIEWER_HOST), enforces inbound Bearer auth and Host-header allowlisting for non-loopback binds, updates the frontend to prompt/save tokens on 401, and adds Fly entrypoint/docs to enable fly proxy access by binding to the WireGuard interface and seeding allowed hosts.

Changes

Viewer host binding and authentication

Layer / File(s) Summary
Host resolution and loopback classification
src/viewer/server.ts, test/viewer-host.test.ts
resolveViewerHost() reads AGENTMEMORY_VIEWER_HOST with trimming and defaults to 127.0.0.1; isLoopbackHost() classifies loopback aliases. Unit tests cover defaults, trimming, and loopback detection.
Bearer auth and error contract
src/viewer/server.ts, test/viewer-host.test.ts
requireInboundBearer() parses and validates Bearer tokens with timing-safe compare; ViewerConfigError signals startup configuration failures. Tests verify parsing, scheme handling, and token checks.
Host allowlist validation
src/viewer/server.ts, test/viewer-host.test.ts
buildAllowedHosts() now takes the resolved bind host and seeds CORS-derived loopback defaults only when binding to loopback; non-loopback binds require explicit VIEWER_ALLOWED_HOSTS. Tests check inclusion/exclusion and empty-override behavior.
Server startup and per-request authentication
src/viewer/server.ts, test/viewer-host.test.ts
startViewerServer resolves bind host, enforces AGENTMEMORY_SECRET and non-empty VIEWER_ALLOWED_HOSTS for non-loopback binds, computes allowlist with the resolved host, binds accordingly, enforces per-request Bearer auth (401 + WWW-Authenticate when missing/invalid), and disables port-retry for non-loopback binds. Integration tests validate modes, auth gating, static asset access, logs, and EADDRINUSE non-retry behavior.
Frontend authentication UI and token management
src/viewer/index.html, test/viewer-session-id.test.ts
Adds #viewer-auth HTML/CSS and VIEWER_TOKEN_STORAGE_KEY; api() attaches stored Authorization: Bearer <token> when present, shows the auth prompt on 401, hides it on success, and save-viewer-token stores the token and reloads the active tab. Tests add a sessionStorage mock and verify token attachment and 401 prompt behavior.
Fly deployment configuration and documentation
deploy/fly/entrypoint.sh, deploy/fly/README.md
Entrypoint detects Fly runtime and sets AGENTMEMORY_VIEWER_HOST=:: and seeds VIEWER_ALLOWED_HOSTS to match fly proxy Host headers when running on Fly; README documents the Fly-specific binding, allowlist/auth rules, 401 UI behavior, static asset exemption, a security warning, and adds a /data/.hmac fallback to capture the HMAC secret.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • rohitg00/agentmemory#69: Adds Bearer token validation to viewer; this PR integrates that into mandatory inbound auth for non-loopback binds.
  • rohitg00/agentmemory#405: Modifies port retry/EADDRINUSE handling in startViewerServer; this PR gates retries based on inbound-auth status.
  • rohitg00/agentmemory#294: Refactors buildAllowedHosts and Host-header validation; this PR extends that work with bind-aware defaults and Fly-specific allowlist seeding.

Poem

🐰 I hopped through meshes, ports, and host,
Saved secrets where the WireGuard's crossed,
A token in session, a prompt on the breeze,
Loopback keeps calm while the proxy agrees,
Securely bound, the viewer greets with ease.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses the root cause of #434 (viewer bound only to loopback preventing fly proxy access) by adding configurable binding. However, per reviewer feedback, it lacks required security safeguards: inbound bearer auth enforcement for non-loopback binds, host allowlist tightening, and Fly-only runtime guards. Implement inbound bearer token validation for non-loopback binds, require explicit VIEWER_ALLOWED_HOSTS when non-loopback, move AGENTMEMORY_VIEWER_HOST set to entrypoint guard (Fly detection), and update README with security warnings.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST' accurately summarizes the main change: enabling configurable viewer binding through an environment variable.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing configurable viewer binding and security controls. Documentation, server logic, tests, entrypoint configuration, and UI auth prompt changes all support the core objective of secure non-loopback binding.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wyh0626 wyh0626 marked this pull request as ready for review May 17, 2026 08:57
@rohitg00
Copy link
Copy Markdown
Owner

Hey @wyh0626 — appreciate the contribution, but blocking on a security regression before this can land.

The viewer has no inbound auth — its only defense beyond loopback bind is the Host header allowlist in buildAllowedHosts(). Binding to :: (or 0.0.0.0) exposes a privileged proxy that holds the agentmemory REST bearer token in process.env.AGENTMEMORY_SECRET and forwards arbitrary requests upstream. Any LAN peer that spoofs Host: localhost:3113 lands the same trust level as a local user.

Specifically:

  1. deploy/fly/Dockerfile bakes AGENTMEMORY_VIEWER_HOST=:: — any user who pulls this image and runs docker run -p 3113:3113 outside the documented fly setup silently exposes the proxy. The ENV needs to move to an entrypoint that asserts the fly-only context (or a runtime guard).

  2. No Host-allowlist tightening when bind is non-loopback. Either auto-derive VIEWER_ALLOWED_HOSTS from AGENTMEMORY_VIEWER_HOST + the configured fly app domain, or refuse to start unless VIEWER_ALLOWED_HOSTS is explicitly set.

  3. No inbound auth. Even with the allowlist tightened, the viewer needs to require AGENTMEMORY_SECRET on its own inbound path when bound to non-loopback (not just on the upstream REST call). Reuse the existing HMAC pattern from src/auth.ts — same Bearer header, timing-safe compare.

  4. README needs an explicit warning that AGENTMEMORY_VIEWER_HOST=0.0.0.0 exposes a bearer-authorized proxy without inbound auth by default.

This PR also currently conflicts with main on src/viewer/server.ts. Rebase + add the 4 guards above and I'll review again.

@wyh0626 wyh0626 force-pushed the fix/434-fly-viewer-bind branch from 4b35165 to 8ad0794 Compare May 20, 2026 15:41
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

@wyh0626 is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/viewer/server.ts (1)

17-30: 💤 Low value

Consider caching the favicon buffer at module load time.

loadViewerFavicon() performs synchronous disk I/O on every /favicon.svg request. Since the favicon is static, loading it once at module initialization would avoid repeated disk reads.

♻️ Optional: cache favicon at module level
+// Cache favicon at module load — it's static and avoids repeated disk I/O.
+const VIEWER_FAVICON: Buffer | null = loadViewerFavicon();
+
 function loadViewerFavicon(): Buffer | null {
   const base = dirname(fileURLToPath(import.meta.url));
   // ... rest unchanged
 }

Then in the request handler:

 if (method === "GET" && pathname === "/favicon.svg") {
-  const favicon = loadViewerFavicon();
+  const favicon = VIEWER_FAVICON;
   if (favicon) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/server.ts` around lines 17 - 30, loadViewerFavicon currently does
synchronous disk I/O on every request; change to load the favicon once at module
initialization and reuse it: call loadViewerFavicon() at module top-level and
store the result in a const (e.g., VIEWER_FAVICON or viewerFaviconBuffer), then
update the request handler that serves /favicon.svg to return that cached buffer
(handle null by returning 404 or falling back). Keep the existing
loadViewerFavicon function but ensure the module-level constant is used instead
of calling the function per-request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/viewer/server.ts`:
- Around line 17-30: loadViewerFavicon currently does synchronous disk I/O on
every request; change to load the favicon once at module initialization and
reuse it: call loadViewerFavicon() at module top-level and store the result in a
const (e.g., VIEWER_FAVICON or viewerFaviconBuffer), then update the request
handler that serves /favicon.svg to return that cached buffer (handle null by
returning 404 or falling back). Keep the existing loadViewerFavicon function but
ensure the module-level constant is used instead of calling the function
per-request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36a33852-fedc-4568-9534-f15321657d21

📥 Commits

Reviewing files that changed from the base of the PR and between 4b35165 and 8ad0794.

📒 Files selected for processing (6)
  • deploy/fly/README.md
  • deploy/fly/entrypoint.sh
  • src/viewer/index.html
  • src/viewer/server.ts
  • test/viewer-host.test.ts
  • test/viewer-session-id.test.ts
✅ Files skipped from review due to trivial changes (1)
  • deploy/fly/README.md

wyh0626 added 2 commits May 20, 2026 23:48
`startViewerServer` hardcoded `server.listen(port, "127.0.0.1")`, so
Fly's `fly-local-6pn` traffic was RST and `fly proxy 3113:3113` reported
"Recv failure: Connection reset by peer" (rohitg00#434).

Bind host is now read from `AGENTMEMORY_VIEWER_HOST` (default
`127.0.0.1`). To keep the viewer's bearer-authorized proxy from
silently exposing itself on non-loopback binds, the same code path now:

  - drops loopback hostnames from the default Host allowlist when bind
    is non-loopback;
  - refuses to start unless both `VIEWER_ALLOWED_HOSTS` and
    `AGENTMEMORY_SECRET` are set in that mode;
  - requires `Authorization: Bearer $AGENTMEMORY_SECRET` (timing-safe
    compare from src/auth.ts) on every /agentmemory/* request; HTML
    and the favicon stay open.

`deploy/fly/Dockerfile` no longer bakes `AGENTMEMORY_VIEWER_HOST=::`.
The entrypoint exports it (and a `VIEWER_ALLOWED_HOSTS` default scoped
to the Host headers `fly proxy` emits) only when `FLY_APP_NAME` or
`FLY_ALLOC_ID` is present, so a `docker run -p 3113:3113` of the image
elsewhere stays loopback-only.

Tests: 29 cases in test/viewer-host.test.ts.
@wyh0626 wyh0626 force-pushed the fix/434-fly-viewer-bind branch from 8ad0794 to f80e9a7 Compare May 20, 2026 15:50
@wyh0626
Copy link
Copy Markdown
Contributor Author

wyh0626 commented May 20, 2026

Updated this PR against the latest main and tightened the Fly viewer path.

Changes made:

  1. Kept the default viewer behavior unchanged

    • The viewer still binds to 127.0.0.1 by default.
    • Local runs, plain Docker, and non-Fly deployments are unaffected unless they explicitly set AGENTMEMORY_VIEWER_HOST to a non-loopback address.
  2. Added explicit non-loopback viewer authentication

    • When AGENTMEMORY_VIEWER_HOST is set to 0.0.0.0 or ::, the viewer now requires AGENTMEMORY_SECRET.
    • API/proxy requests must include Authorization: Bearer <AGENTMEMORY_SECRET>.
    • Static viewer HTML remains loadable so the browser can show the unlock prompt.
  3. Tightened Host allowlisting for non-loopback binds

    • Loopback-derived default hosts are no longer trusted when the viewer binds to a non-loopback address.
    • Non-loopback mode now requires explicit VIEWER_ALLOWED_HOSTS.
  4. Improved operator-facing errors

    • Missing AGENTMEMORY_SECRET now explains how to fix it:
      either unset AGENTMEMORY_VIEWER_HOST to return to the safe loopback bind, or set AGENTMEMORY_SECRET.
    • Missing VIEWER_ALLOWED_HOSTS now explains that non-loopback binds require explicit trusted Host headers and gives a concrete example.
  5. Avoided silent 403s from port fallback

    • In non-loopback mode, the viewer no longer retries on EADDRINUSE.
    • This avoids drifting from 3113 to 3114+ while VIEWER_ALLOWED_HOSTS still contains the old port.
    • Loopback/default mode keeps the previous retry behavior.
  6. Made startup logs clearer

    • Non-loopback viewer startup logs now include the bind host, that inbound Bearer auth is required, and the configured allowed Host headers.
  7. Added browser unlock flow

    • The viewer shows an AGENTMEMORY_SECRET prompt when API calls return 401.
    • The token is stored in sessionStorage only.
  8. Updated Fly docs

    • Documented how to get the generated secret from first-boot logs or /data/.hmac.
    • Documented the viewer auth prompt and Fly proxy flow.

@wyh0626 wyh0626 closed this May 20, 2026
@wyh0626 wyh0626 reopened this May 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/viewer/server.ts (1)

323-340: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log the actual bound port after listen(0).

If requestedPort is 0, currentPort never changes, so the startup messages print http://localhost:0 even though server.address() already exposes the real port. That makes the log misleading for ephemeral-port callers and for these tests, which all start the server with 0.

Proposed fix
   server.on("listening", () => {
     const addr = server.address();
-    boundViewerPort =
+    const actualPort =
       addr && typeof addr === "object" && "port" in addr
         ? addr.port
         : currentPort;
+    boundViewerPort = actualPort;
     viewerSkipped = false;
     if (inboundSecret !== null) {
       const allowedHosts = readAllowedHostsOverride().join(", ");
       console.log(
-        `[agentmemory] Viewer: http://localhost:${currentPort} (bound to ${host}; inbound Bearer required; allowed Host headers: ${allowedHosts})`,
+        `[agentmemory] Viewer: http://localhost:${actualPort} (bound to ${host}; inbound Bearer required; allowed Host headers: ${allowedHosts})`,
       );
       return;
     }
     if (currentPort === requestedPort) {
-      console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
+      console.log(`[agentmemory] Viewer: http://localhost:${actualPort}`);
     } else {
       console.log(
-        `[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`,
+        `[agentmemory] Viewer started on http://localhost:${actualPort} (fallback from ${requestedPort})`,
       );
     }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/server.ts` around lines 323 - 340, The startup logs use
currentPort which stays 0 when listen(0) is used; after you compute
boundViewerPort from server.address() (the existing addr check around
server.address()), propagate that real port into the logging path by either
assigning currentPort = boundViewerPort when boundViewerPort is defined or by
replacing currentPort with boundViewerPort in the console.log calls (including
the inboundSecret branch and both requestedPort comparisons) so the actual
ephemeral port returned by server.address() is printed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/fly/README.md`:
- Line 90: Typo in the README description: replace the word "preseeds" with
"pre-seeds" (or "prepopulates") in the sentence that references
VIEWER_ALLOWED_HOSTS
(`VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113`) so the
allowlist description reads clearly (e.g., "pre-seeds VIEWER_ALLOWED_HOSTS..."
or "prepopulates VIEWER_ALLOWED_HOSTS...").

In `@src/viewer/index.html`:
- Around line 1217-1224: The unlock password input (id="viewer-auth-token")
lacks an accessible name; add one by either adding a visible <label> that
references the input id or by adding an aria-label/aria-labelledby that conveys
"Viewer authorization token" (or similar) so screen readers can announce the
field; update the host.innerHTML where the input is created and ensure any
visually-hidden label uses an appropriate class if you want it hidden, and keep
the button with data-action="save-viewer-token" intact to preserve behavior.
- Around line 3792-3803: After successfully saving the viewer token in the
'save-viewer-token' handler (around setViewerToken, hideViewerAuthPrompt,
loadTab(state.activeTab)), also invoke fetchFlags() to re-fetch config/flags and
then refresh the footer/version and flag banner UI (e.g., call existing
functions like refreshFooterVersion() and refreshFlagBanners(), or implement
small functions to re-render those parts if they don't exist) so the page state
updates immediately without a full reload.

---

Outside diff comments:
In `@src/viewer/server.ts`:
- Around line 323-340: The startup logs use currentPort which stays 0 when
listen(0) is used; after you compute boundViewerPort from server.address() (the
existing addr check around server.address()), propagate that real port into the
logging path by either assigning currentPort = boundViewerPort when
boundViewerPort is defined or by replacing currentPort with boundViewerPort in
the console.log calls (including the inboundSecret branch and both requestedPort
comparisons) so the actual ephemeral port returned by server.address() is
printed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a55f2fe6-d173-4a91-80c2-814f12c2160f

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad0794 and f80e9a7.

📒 Files selected for processing (6)
  • deploy/fly/README.md
  • deploy/fly/entrypoint.sh
  • src/viewer/index.html
  • src/viewer/server.ts
  • test/viewer-host.test.ts
  • test/viewer-session-id.test.ts

Comment thread deploy/fly/README.md
Fly's runtime variables** (`FLY_APP_NAME` / `FLY_ALLOC_ID`). That makes
the viewer listen on the machine's `fly-local-6pn` WireGuard interface
as well as loopback so `fly proxy` can reach it. The same branch
preseeds `VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix wording typo in allowlist description.

Line 90 uses “preseeds”; replace with “pre-seeds” (or “prepopulates”) for clearer docs text.

✏️ Proposed doc tweak
-The same branch preseeds `VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113`,
+The same branch pre-seeds `VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/fly/README.md` at line 90, Typo in the README description: replace the
word "preseeds" with "pre-seeds" (or "prepopulates") in the sentence that
references VIEWER_ALLOWED_HOSTS
(`VIEWER_ALLOWED_HOSTS=localhost:3113,127.0.0.1:3113,[::1]:3113`) so the
allowlist description reads clearly (e.g., "pre-seeds VIEWER_ALLOWED_HOSTS..."
or "prepopulates VIEWER_ALLOWED_HOSTS...").

Comment thread src/viewer/index.html
Comment on lines +1217 to +1224
host.innerHTML =
'<div class="viewer-auth-panel">' +
'<div>' +
'<div class="viewer-auth-title">Viewer authorization required</div>' +
'<div class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
'</div>' +
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" placeholder="AGENTMEMORY_SECRET" />' +
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an accessible name to the unlock field.

This input is the only path through the non-loopback auth gate, but it currently relies on placeholder text alone. Screen readers won't get a reliable label here, which makes the unlock flow much harder to complete.

♿ Proposed fix
-            '<div class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
+            '<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
           '</div>' +
-          '<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" placeholder="AGENTMEMORY_SECRET" />' +
+          '<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTMEMORY_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTMEMORY_SECRET" />' +
           '<button class="btn" data-action="save-viewer-token">Unlock</button>' +
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
host.innerHTML =
'<div class="viewer-auth-panel">' +
'<div>' +
'<div class="viewer-auth-title">Viewer authorization required</div>' +
'<div class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
'</div>' +
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" placeholder="AGENTMEMORY_SECRET" />' +
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
host.innerHTML =
'<div class="viewer-auth-panel">' +
'<div>' +
'<div class="viewer-auth-title">Viewer authorization required</div>' +
'<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
'</div>' +
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTMEMORY_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTMEMORY_SECRET" />' +
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/index.html` around lines 1217 - 1224, The unlock password input
(id="viewer-auth-token") lacks an accessible name; add one by either adding a
visible <label> that references the input id or by adding an
aria-label/aria-labelledby that conveys "Viewer authorization token" (or
similar) so screen readers can announce the field; update the host.innerHTML
where the input is created and ensure any visually-hidden label uses an
appropriate class if you want it hidden, and keep the button with
data-action="save-viewer-token" intact to preserve behavior.

Comment thread src/viewer/index.html
Comment on lines +3792 to +3803
if (action === 'save-viewer-token') {
var tokenInput = document.getElementById('viewer-auth-token');
var token = tokenInput ? tokenInput.value.trim() : '';
if (token) {
setViewerToken(token);
hideViewerAuthPrompt();
if (state[state.activeTab] && typeof state[state.activeTab] === 'object') {
state[state.activeTab].loaded = false;
}
loadTab(state.activeTab);
}
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Retry the viewer config fetch after unlocking.

fetchFlags() only runs during bootstrap. If config/flags is the request that first 401s, this handler unlocks the active tab but leaves the footer version and flag banners stale until the user fully reloads the page.

🔁 Proposed fix
       if (token) {
         setViewerToken(token);
         hideViewerAuthPrompt();
+        fetchFlags();
         if (state[state.activeTab] && typeof state[state.activeTab] === 'object') {
           state[state.activeTab].loaded = false;
         }
         loadTab(state.activeTab);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (action === 'save-viewer-token') {
var tokenInput = document.getElementById('viewer-auth-token');
var token = tokenInput ? tokenInput.value.trim() : '';
if (token) {
setViewerToken(token);
hideViewerAuthPrompt();
if (state[state.activeTab] && typeof state[state.activeTab] === 'object') {
state[state.activeTab].loaded = false;
}
loadTab(state.activeTab);
}
return;
if (action === 'save-viewer-token') {
var tokenInput = document.getElementById('viewer-auth-token');
var token = tokenInput ? tokenInput.value.trim() : '';
if (token) {
setViewerToken(token);
hideViewerAuthPrompt();
fetchFlags();
if (state[state.activeTab] && typeof state[state.activeTab] === 'object') {
state[state.activeTab].loaded = false;
}
loadTab(state.activeTab);
}
return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/index.html` around lines 3792 - 3803, After successfully saving
the viewer token in the 'save-viewer-token' handler (around setViewerToken,
hideViewerAuthPrompt, loadTab(state.activeTab)), also invoke fetchFlags() to
re-fetch config/flags and then refresh the footer/version and flag banner UI
(e.g., call existing functions like refreshFooterVersion() and
refreshFlagBanners(), or implement small functions to re-render those parts if
they don't exist) so the page state updates immediately without a full reload.

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.

Fly deployment proxy doesn't work

2 participants