Require a token for writes and quarantine what nobody vouches for
Sixteen mutating routes now refuse a caller who cannot present the token the server started with; queries stay open, because a question is not a write. The environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix convention — the shorter name I reached for first returned 403 where the smoke expected 401, and the server printed the reason. Words from a stranger are quarantined on the way in: an HTTP upload is third_party by definition, and a sentence carrying an instruction-shaped phrase is quarantined whoever wrote it. A quarantined atom stays findable and stays out of the compiled context until a second, live, unquarantined source agrees with its rarest words. Corroboration is computed at read time because it is a property of the corpus now, not of the moment a memory was written; it is term-level, and the docstring says so. forget now erases the source row and the compiled contexts beside the atoms, so a forgotten memory leaves nothing that can still speak. The semantic cache key carries a store revision, which fixes a real staleness: a context compiled before new evidence arrived kept its old quarantine decision forever. bench.audit --product still pointed at evals.py, which the p1-6 split moved into backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification rule had quietly stopped auditing the product; it now walks the package, and a test pins that the package exists and holds no re-roll. Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2 bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31 harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two servers and the MCP child.
This commit is contained in:
1 parent
8b974e6626
commit
9145a23f18
32 files changed
+750
-58
No files matched your search
+23
-8
@@ -1,7 +1,7 @@
|
||||
import { Activity, Archive, Database, Gauge, RefreshCw, Server } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getAdapters, getStatus } from './api';
|
||||
import { getAdapters, getStatus, readToken, writeToken } from './api';
|
||||
import { AdapterPanel } from './components/AdapterPanel';
|
||||
import { CartridgePanel } from './components/CartridgePanel';
|
||||
import { CompilerPanel } from './components/CompilerPanel';
|
||||
@@ -17,6 +17,7 @@ export function App() {
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||
const [adapters, setAdapters] = useState<AdapterStatus[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [token, setToken] = useState(readToken);
|
||||
|
||||
async function refresh() {
|
||||
const [nextStatus, nextAdapters] = await Promise.all([getStatus(), getAdapters()]);
|
||||
@@ -63,13 +64,27 @@ export function App() {
|
||||
<p className="eyebrow">Ely-Eye v4.0</p>
|
||||
<h1>Local VLM Context OS</h1>
|
||||
</div>
|
||||
<button
|
||||
className="icon-button"
|
||||
title="Refresh status"
|
||||
onClick={() => refreshQuietly()}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
</button>
|
||||
<div className="topbar-actions">
|
||||
<input
|
||||
className="token-input"
|
||||
type="password"
|
||||
value={token}
|
||||
placeholder="写入令牌"
|
||||
aria-label="写入令牌"
|
||||
title="服务器设置了 ELY_EYE_AUTH_TOKEN 时,写入需要它"
|
||||
onChange={(event) => {
|
||||
setToken(event.target.value);
|
||||
writeToken(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="icon-button"
|
||||
title="Refresh status"
|
||||
onClick={() => refreshQuietly()}
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="error-line">{error}</div>}
|
||||
|
||||
+21
-1
@@ -16,8 +16,28 @@ import type {
|
||||
SystemStatus,
|
||||
} from './types';
|
||||
|
||||
const TOKEN_KEY = 'ely_eye_token';
|
||||
|
||||
/** The write token lives in the browser, not in the bundle: a served page must not ship the owner's secret. */
|
||||
export function readToken(): string {
|
||||
return window.localStorage.getItem(TOKEN_KEY) ?? '';
|
||||
}
|
||||
|
||||
export function writeToken(token: string): void {
|
||||
if (token) {
|
||||
window.localStorage.setItem(TOKEN_KEY, token);
|
||||
} else {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, init);
|
||||
const headers = new Headers(init?.headers);
|
||||
const token = readToken();
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
const response = await fetch(url, { ...init, headers });
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(typeof payload.detail === 'string' ? payload.detail : JSON.stringify(payload.detail));
|
||||
|
||||
Reference in new issue
Block a user