-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST #442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -711,6 +711,46 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| .flag-close:hover, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .flag-close:focus-visible { color: var(--ink); outline: 2px solid var(--border); outline-offset: 1px; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: none; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 0 24px 10px 24px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--bg); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flex: 0 0 auto; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: relative; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| z-index: 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth.open { display: block; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth-panel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: grid; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| grid-template-columns: minmax(0, 1fr) minmax(180px, 320px) auto; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| gap: 10px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: min(960px, 100%); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| max-width: 960px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 10px 14px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-left: 3px solid var(--accent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--bg-subtle); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-family: var(--font-ui); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-size: 12px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth-title { font-weight: 600; color: var(--ink); margin-bottom: 2px; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth-desc { color: var(--ink-muted); line-height: 1.4; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth-desc code { font-family: var(--font-mono); font-size: 10px; color: var(--ink); } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth input { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| min-width: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 7px 9px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: var(--bg); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: var(--ink); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-family: var(--font-mono); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-size: 12px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @media (max-width: 900px) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-auth-panel { grid-template-columns: 1fr; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Viewer footer */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .viewer-footer { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| margin-top: 48px; padding: 16px 0 24px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -961,6 +1001,7 @@ <h1>agentmemory</h1> | |||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="flag-banners" class="flag-banners"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="viewer-auth" class="viewer-auth"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="view-dashboard" class="view active"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="view-graph" class="view"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1063,6 +1104,7 @@ <h1>agentmemory</h1> | |||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var TAB_IDS = ['dashboard', 'graph', 'memories', 'timeline', 'sessions', 'lessons', 'actions', 'crystals', 'audit', 'activity', 'profile', 'replay']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var VIEWER_TOKEN_STORAGE_KEY = 'agentmemory-viewer-token'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var state = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| activeTab: 'dashboard', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1159,17 +1201,54 @@ <h1>agentmemory</h1> | |||||||||||||||||||||||||||||||||||||||||||||||||||
| try { el.setSelectionRange(focus.start, focus.end); } catch (e) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getViewerToken() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { return sessionStorage.getItem(VIEWER_TOKEN_STORAGE_KEY) || ''; } catch (_) { return ''; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function setViewerToken(token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (token) sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, token); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else sessionStorage.removeItem(VIEWER_TOKEN_STORAGE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function showViewerAuthPrompt() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var host = document.getElementById('viewer-auth'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!host) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| host.classList.add('open'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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>' + | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1217
to
+1224
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| '</div>'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var input = document.getElementById('viewer-auth-token'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (input && typeof input.focus === 'function') input.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function hideViewerAuthPrompt() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var host = document.getElementById('viewer-auth'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!host) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| host.classList.remove('open'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| host.innerHTML = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function api(path, opts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var url = REST + '/agentmemory/' + path; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var viewerToken = getViewerToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (viewerToken && !headers.Authorization && !headers.authorization) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers.Authorization = 'Bearer ' + viewerToken; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var fetchOpts = Object.assign({}, opts || {}, { headers: headers }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var res = await fetch(url, fetchOpts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (res.status === 401) showViewerAuthPrompt(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('[viewer] API ' + (fetchOpts.method || 'GET') + ' ' + path + ' returned ' + res.status); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| hideViewerAuthPrompt(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await res.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('[viewer] API error on ' + path + ':', err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3710,6 +3789,19 @@ <h1>agentmemory</h1> | |||||||||||||||||||||||||||||||||||||||||||||||||||
| if (memoryId) confirmDeleteMemory(memoryId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3792
to
+3803
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retry the viewer config fetch after unlocking.
🔁 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (action === 'timeline-filter') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTlTypeFilter(target.getAttribute('data-type-filter') || ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix wording typo in allowlist description.
Line 90 uses “preseeds”; replace with “pre-seeds” (or “prepopulates”) for clearer docs text.
✏️ Proposed doc tweak
🤖 Prompt for AI Agents