Add Loki Chrome extension bridge

This commit is contained in:
Meta Vens 2026-06-09 16:12:31 +01:00
commit ae87719d61
13 changed files with 863 additions and 70 deletions

View file

@ -1,16 +1,17 @@
# Loki
Loki is a CultMesh-oriented daemon for observing local Chrome tabs through an
explicit Chrome DevTools Protocol endpoint.
Loki is a CultMesh-oriented daemon for observing local Chrome tabs through a
drop-in Chrome extension or an explicit Chrome DevTools Protocol endpoint.
It does not see ordinary locked Chrome tabs by magic. Chrome must be launched
with a remote debugging port, and the daemon publishes that reachability as
state instead of pretending.
It does not see ordinary locked Chrome tabs by magic. The easy path is loading
the unpacked extension in `extension/`; the developer path is launching Chrome
with a remote debugging port. The daemon publishes reachability as state instead
of pretending.
## Authority Map
- Owner: Loki owns Chrome DevTools observation and the derived operator surface.
- Inputs: `http://127.0.0.1:9222/json/version`,
- Inputs: Chrome extension reports, `http://127.0.0.1:9222/json/version`,
`http://127.0.0.1:9222/json/list`, and daemon configuration.
- Outputs: typed `.cc` witness documents, an Eve provider advertisement, and a
`gamecult.eve.surface.v1` surface projection.
@ -18,12 +19,32 @@ state instead of pretending.
operator text are derived from the Chrome snapshot.
- Forbidden writers: renderers, dashboards, ad hoc scripts, and browser windows
do not own Loki state.
- Shared paths: one `observeChrome()` path feeds witness, advertisement, and
Eve projection.
- Shared paths: extension and CDP observations are normalized into one snapshot
before witness, advertisement, and Eve projection.
- Cut line: command execution is not implemented until it has an auditable
command envelope and explicit consent boundary.
## Start Chrome For Observation
## Drop Into Chrome
Run Loki:
```powershell
cd E:\Projects\Loki
npm run start
```
Then install the extension:
1. Open `chrome://extensions`.
2. Enable `Developer mode`.
3. Choose `Load unpacked`.
4. Select `E:\Projects\Loki\extension`.
The extension posts tab metadata to
`http://127.0.0.1:17666/ingest/chrome-extension`. It does not read page DOM,
cookies, or page content.
## CDP Observation
Close Chrome or use a separate profile, then launch:
@ -33,13 +54,6 @@ Close Chrome or use a separate profile, then launch:
--user-data-dir="$env:TEMP\loki-chrome-profile"
```
Then run Loki:
```powershell
cd E:\Projects\Loki
npm run start
```
For a one-shot smoke:
```powershell

View file

@ -3,19 +3,25 @@
## Objective
Expose local Chrome tab state as provider-owned, typed service state for
CultMesh/Eve consumers.
CultMesh/Eve consumers while keeping Chrome installation easy for the operator.
## Current Mechanism
Chrome exposes DevTools metadata on `127.0.0.1:{port}` when started with
`--remote-debugging-port`. Loki polls the version and tab-list endpoints,
normalizes the result, and writes three documents from the same snapshot.
The Chrome extension observes tab metadata with explicit Chrome permissions and
posts reports to Loki's localhost ingest endpoint. Chrome can also expose
DevTools metadata on `127.0.0.1:{port}` when started with
`--remote-debugging-port`. Loki normalizes both sources into one snapshot and
writes three documents from that snapshot.
```mermaid
flowchart LR
Chrome["Chrome CDP endpoint"] --> Observe["observeChrome"]
Config["Loki config"] --> Observe
Observe --> Snapshot["chrome_snapshot.cc"]
Extension["Chrome extension"] --> Ingest["extension ingest"]
Chrome["Chrome CDP endpoint"] --> Observe["observe CDP"]
Config["Loki config"] --> Ingest
Config --> Observe
Ingest --> Combine["combined snapshot"]
Observe --> Combine
Combine --> Snapshot["chrome_snapshot.cc"]
Snapshot --> Provider["provider_advertisement.cc"]
Snapshot --> Surface["eve_surface.cc"]
```
@ -23,6 +29,7 @@ flowchart LR
## Invariants
- CDP reachability is observed state, not assumed capability.
- Extension reachability is observed state, not assumed capability.
- Chrome owns page reality; Loki owns only the observation snapshot.
- `.cc` witnesses are the durable local state surface.
- Eve/CultUI output is derived from the same snapshot as the witness.

23
extension/manifest.json Normal file
View file

@ -0,0 +1,23 @@
{
"manifest_version": 3,
"name": "Loki Chrome Bridge",
"version": "0.1.0",
"description": "Publishes Chrome tab metadata to the local Loki CultMesh daemon.",
"permissions": [
"alarms",
"storage",
"tabs"
],
"host_permissions": [
"http://127.0.0.1:17666/*",
"http://localhost:17666/*"
],
"background": {
"service_worker": "service-worker.js",
"type": "module"
},
"action": {
"default_title": "Loki Chrome Bridge",
"default_popup": "popup.html"
}
}

91
extension/popup.css Normal file
View file

@ -0,0 +1,91 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 300px;
font: 13px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #17201b;
background: #f7faf8;
}
main {
padding: 12px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
h1 {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.state {
border: 1px solid #a9b8ae;
border-radius: 999px;
padding: 2px 8px;
color: #445149;
background: #ffffff;
}
.state.ok {
border-color: #58a36f;
color: #1d6b35;
background: #eef8f0;
}
.state.bad {
border-color: #c2803a;
color: #7a4a13;
background: #fff7ea;
}
dl {
display: grid;
gap: 8px;
margin: 0 0 12px;
}
dl div {
display: grid;
grid-template-columns: 72px 1fr;
gap: 8px;
}
dt {
color: #5a665f;
}
dd {
margin: 0;
overflow-wrap: anywhere;
}
.error {
margin: 0 0 12px;
color: #8d3d20;
}
button {
width: 100%;
border: 1px solid #2b6b47;
border-radius: 6px;
padding: 7px 10px;
color: #ffffff;
background: #2f7a50;
font: inherit;
cursor: pointer;
}
button:disabled {
cursor: wait;
opacity: 0.65;
}

37
extension/popup.html Normal file
View file

@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Loki Chrome Bridge</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main>
<header>
<h1>Loki</h1>
<span id="state" class="state">checking</span>
</header>
<dl>
<div>
<dt>Tabs</dt>
<dd id="tabs">-</dd>
</div>
<div>
<dt>Last sync</dt>
<dd id="last-sync">-</dd>
</div>
<div>
<dt>Endpoint</dt>
<dd id="endpoint">-</dd>
</div>
</dl>
<p id="error" class="error" hidden></p>
<button id="sync" type="button">Sync now</button>
</main>
<script src="popup.js" type="module"></script>
</body>
</html>

42
extension/popup.js Normal file
View file

@ -0,0 +1,42 @@
const stateEl = document.querySelector("#state");
const tabsEl = document.querySelector("#tabs");
const lastSyncEl = document.querySelector("#last-sync");
const endpointEl = document.querySelector("#endpoint");
const errorEl = document.querySelector("#error");
const syncButton = document.querySelector("#sync");
syncButton.addEventListener("click", async () => {
syncButton.disabled = true;
await chrome.runtime.sendMessage({ type: "loki.sync-now" });
await render();
syncButton.disabled = false;
});
await render();
async function render() {
const result = await chrome.runtime.sendMessage({ type: "loki.get-status" });
const status = result.status;
endpointEl.textContent = result.endpoint;
if (!status) {
stateEl.textContent = "waiting";
stateEl.className = "state";
tabsEl.textContent = "-";
lastSyncEl.textContent = "-";
errorEl.hidden = true;
return;
}
stateEl.textContent = status.ok ? "connected" : "offline";
stateEl.className = `state ${status.ok ? "ok" : "bad"}`;
tabsEl.textContent = String(status.tabCount ?? 0);
lastSyncEl.textContent = status.lastSyncAt ? new Date(status.lastSyncAt).toLocaleTimeString() : "-";
if (status.error) {
errorEl.textContent = status.error;
errorEl.hidden = false;
} else {
errorEl.hidden = true;
}
}

146
extension/service-worker.js Normal file
View file

@ -0,0 +1,146 @@
const DEFAULT_ENDPOINT = "http://127.0.0.1:17666/ingest/chrome-extension";
const STATUS_KEY = "lokiStatus";
const CONFIG_KEY = "lokiConfig";
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create("loki-sync", { periodInMinutes: 1 });
syncTabs("installed");
});
chrome.runtime.onStartup.addListener(() => {
chrome.alarms.create("loki-sync", { periodInMinutes: 1 });
syncTabs("startup");
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "loki-sync") syncTabs("alarm");
});
chrome.tabs.onCreated.addListener(() => syncTabs("tab-created"));
chrome.tabs.onUpdated.addListener(() => syncTabs("tab-updated"));
chrome.tabs.onRemoved.addListener(() => syncTabs("tab-removed"));
chrome.tabs.onActivated.addListener(() => syncTabs("tab-activated"));
chrome.tabs.onMoved.addListener(() => syncTabs("tab-moved"));
chrome.tabs.onAttached.addListener(() => syncTabs("tab-attached"));
chrome.tabs.onDetached.addListener(() => syncTabs("tab-detached"));
chrome.tabs.onHighlighted.addListener(() => syncTabs("tab-highlighted"));
chrome.windows.onFocusChanged.addListener(() => syncTabs("window-focus"));
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === "loki.sync-now") {
syncTabs("manual").then(sendResponse);
return true;
}
if (message?.type === "loki.get-status") {
getStatus().then(sendResponse);
return true;
}
if (message?.type === "loki.set-endpoint") {
setEndpoint(message.endpoint).then(sendResponse);
return true;
}
return false;
});
async function syncTabs(reason) {
const statusStartedAt = new Date().toISOString();
try {
const endpoint = await getEndpoint();
const tabs = await chrome.tabs.query({});
const report = {
schema: "gamecult.loki.chrome_extension_report.v0",
extensionVersion: chrome.runtime.getManifest().version,
browser: navigator.userAgent,
reportedAt: statusStartedAt,
reason,
tabs: tabs.map(projectTab),
summary: {
tabCount: tabs.length,
activeTabCount: tabs.filter((tab) => tab.active).length,
},
redaction: {
pageContent: "not-read",
cookies: "not-read",
tabMetadata: "title-url-favicon-window-state",
},
};
const response = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(report),
});
const responseBody = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(responseBody.error || `HTTP ${response.status}`);
}
const status = {
ok: true,
endpoint,
reason,
lastSyncAt: new Date().toISOString(),
tabCount: tabs.length,
activeSource: responseBody.activeSource ?? "unknown",
error: null,
};
await chrome.storage.local.set({ [STATUS_KEY]: status });
return status;
} catch (error) {
const endpoint = await getEndpoint();
const status = {
ok: false,
endpoint,
reason,
lastSyncAt: new Date().toISOString(),
tabCount: 0,
activeSource: "none",
error: error instanceof Error ? error.message : String(error),
};
await chrome.storage.local.set({ [STATUS_KEY]: status });
return status;
}
}
function projectTab(tab) {
return {
id: tab.id,
windowId: tab.windowId,
index: tab.index,
active: tab.active,
highlighted: tab.highlighted,
pinned: tab.pinned,
audible: tab.audible ?? false,
muted: Boolean(tab.mutedInfo?.muted),
incognito: tab.incognito,
title: tab.title ?? null,
url: tab.url ?? null,
favIconUrl: tab.favIconUrl ?? null,
};
}
async function getStatus() {
const values = await chrome.storage.local.get([STATUS_KEY, CONFIG_KEY]);
return {
endpoint: values[CONFIG_KEY]?.endpoint ?? DEFAULT_ENDPOINT,
status: values[STATUS_KEY] ?? null,
};
}
async function getEndpoint() {
const values = await chrome.storage.local.get(CONFIG_KEY);
return values[CONFIG_KEY]?.endpoint ?? DEFAULT_ENDPOINT;
}
async function setEndpoint(endpoint) {
const cleanEndpoint = String(endpoint || "").trim();
if (!/^https?:\/\/(127\.0\.0\.1|localhost):\d+\/ingest\/chrome-extension$/.test(cleanEndpoint)) {
return {
ok: false,
error: "Endpoint must be http://127.0.0.1:<port>/ingest/chrome-extension or localhost.",
};
}
await chrome.storage.local.set({ [CONFIG_KEY]: { endpoint: cleanEndpoint } });
return syncTabs("endpoint-updated");
}

View file

@ -7,7 +7,7 @@
"scripts": {
"start": "node src/loki-daemon.mjs",
"smoke": "node src/loki-daemon.mjs --once",
"test": "node test/fixture-cdp-smoke.mjs"
"test": "node test/fixture-cdp-smoke.mjs && node test/extension-ingest-smoke.mjs"
},
"engines": {
"node": ">=20"

View file

@ -1,3 +1,4 @@
import { createServer } from "node:http";
import { mkdir, rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@ -11,6 +12,8 @@ function parseArgs(argv) {
const config = {
host: process.env.LOKI_CHROME_HOST ?? "127.0.0.1",
port: Number(process.env.LOKI_CHROME_PORT ?? 9222),
extensionHost: process.env.LOKI_EXTENSION_HOST ?? "127.0.0.1",
extensionPort: Number(process.env.LOKI_EXTENSION_PORT ?? 17666),
intervalMs: Number(process.env.LOKI_INTERVAL_MS ?? 5000),
stateDir: process.env.LOKI_STATE_DIR ?? defaultStateDir,
once: false,
@ -21,6 +24,8 @@ function parseArgs(argv) {
if (arg === "--once") config.once = true;
else if (arg === "--host") config.host = argv[++index] ?? config.host;
else if (arg === "--port") config.port = Number(argv[++index] ?? config.port);
else if (arg === "--extension-host") config.extensionHost = argv[++index] ?? config.extensionHost;
else if (arg === "--extension-port") config.extensionPort = Number(argv[++index] ?? config.extensionPort);
else if (arg === "--interval-ms") config.intervalMs = Number(argv[++index] ?? config.intervalMs);
else if (arg === "--state-dir") config.stateDir = argv[++index] ?? config.stateDir;
else if (arg === "--help") {
@ -35,6 +40,9 @@ function parseArgs(argv) {
if (!Number.isFinite(config.intervalMs) || config.intervalMs < 1000) {
throw new Error(`Invalid --interval-ms: ${config.intervalMs}`);
}
if (!Number.isFinite(config.extensionPort) || config.extensionPort <= 0) {
throw new Error(`Invalid --extension-port: ${config.extensionPort}`);
}
return config;
}
@ -43,6 +51,7 @@ function printHelp() {
Usage:
node src/loki-daemon.mjs [--once] [--host 127.0.0.1] [--port 9222]
[--extension-host 127.0.0.1] [--extension-port 17666]
[--interval-ms 5000] [--state-dir state]
`);
}
@ -79,15 +88,14 @@ async function observeChrome(config) {
: [];
return {
schema: "gamecult.loki.chrome_snapshot.v0",
providerId,
serviceId,
source: "cdp",
observedAt,
freshness: {
state: "fresh",
lastSeenAt: observedAt,
maxAgeMs: config.intervalMs * 3,
},
authority: "external-authority-projection",
cdp: {
endpoint: baseUrl,
reachable: true,
@ -97,22 +105,10 @@ async function observeChrome(config) {
webSocketDebuggerUrl: stringOrNull(version.webSocketDebuggerUrl),
},
tabs,
summary: {
tabCount: tabs.length,
inspectableTabCount: tabs.filter((tab) => tab.webSocketDebuggerUrl).length,
},
redaction: {
urlQuery: "preserved",
titles: "preserved",
pageContent: "not-read",
cookies: "not-read",
},
};
} catch (error) {
return {
schema: "gamecult.loki.chrome_snapshot.v0",
providerId,
serviceId,
source: "cdp",
observedAt,
freshness: {
state: "unreachable",
@ -124,19 +120,171 @@ async function observeChrome(config) {
reachable: false,
error: error instanceof Error ? error.message : String(error),
},
tabs: [],
};
}
}
function normalizeExtensionObservation(payload, config) {
const observedAt = new Date().toISOString();
if (!payload || payload.schema !== "gamecult.loki.chrome_extension_report.v0") {
throw new Error("Unsupported extension report schema");
}
const tabs = Array.isArray(payload.tabs)
? payload.tabs.map(normalizeExtensionTab).sort((left, right) => Number(right.active) - Number(left.active) || left.index - right.index)
: [];
return {
source: "chrome-extension",
observedAt,
extension: {
reachable: true,
extensionVersion: stringOrNull(payload.extensionVersion),
browser: stringOrNull(payload.browser),
receivedAt: observedAt,
lastReportAt: stringOrNull(payload.reportedAt),
ingestEndpoint: `http://${config.extensionHost}:${config.extensionPort}/ingest/chrome-extension`,
},
freshness: {
state: "fresh",
lastSeenAt: observedAt,
maxAgeMs: config.intervalMs * 3,
},
tabs,
};
}
function normalizeExtensionTab(tab) {
return {
id: String(tab.id ?? ""),
windowId: numberOrNull(tab.windowId),
index: Number.isFinite(tab.index) ? tab.index : 0,
active: Boolean(tab.active),
highlighted: Boolean(tab.highlighted),
pinned: Boolean(tab.pinned),
audible: Boolean(tab.audible),
muted: Boolean(tab.muted),
incognito: Boolean(tab.incognito),
title: stringOrNull(tab.title),
url: stringOrNull(tab.url),
type: "page",
faviconUrl: stringOrNull(tab.favIconUrl),
devtoolsFrontendUrl: null,
webSocketDebuggerUrl: null,
attached: false,
canInspect: false,
source: "chrome-extension",
};
}
function buildSnapshot(config, cdpObservation, extensionObservation) {
const observedAt = new Date().toISOString();
const activeObservation = chooseActiveObservation(cdpObservation, extensionObservation);
const tabs = activeObservation?.tabs ?? [];
const cdp = cdpObservation?.cdp ?? {
endpoint: `http://${config.host}:${config.port}`,
reachable: false,
error: "not-observed",
};
const extension = extensionObservation?.extension ?? {
reachable: false,
ingestEndpoint: `http://${config.extensionHost}:${config.extensionPort}/ingest/chrome-extension`,
error: "no extension report received",
};
const sourceStates = {
cdp: cdpObservation?.freshness?.state ?? "missing",
chromeExtension: extensionObservation?.freshness?.state ?? "missing",
};
const freshnessState = activeObservation ? "fresh" : "unreachable";
return {
schema: "gamecult.loki.chrome_snapshot.v0",
providerId,
serviceId,
observedAt,
activeSource: activeObservation?.source ?? "none",
freshness: {
state: freshnessState,
lastSeenAt: activeObservation?.observedAt ?? null,
maxAgeMs: config.intervalMs * 3,
},
sources: {
cdp: {
state: sourceStates.cdp,
observedAt: cdpObservation?.observedAt ?? null,
},
chromeExtension: {
state: sourceStates.chromeExtension,
observedAt: extensionObservation?.observedAt ?? null,
},
},
cdp,
extension,
tabs,
summary: {
tabCount: tabs.length,
inspectableTabCount: tabs.filter((tab) => tab.webSocketDebuggerUrl).length,
activeTabCount: tabs.filter((tab) => tab.active).length,
},
redaction: {
urlQuery: activeObservation ? "preserved" : "not-observed",
titles: activeObservation ? "preserved" : "not-observed",
pageContent: "not-read",
cookies: "not-read",
source: activeObservation?.source ?? "none",
},
};
}
function chooseActiveObservation(cdpObservation, extensionObservation) {
if (extensionObservation?.freshness?.state === "fresh") return extensionObservation;
if (cdpObservation?.freshness?.state === "fresh") return cdpObservation;
return null;
}
function unreachableSnapshot(config, error) {
const observedAt = new Date().toISOString();
return {
schema: "gamecult.loki.chrome_snapshot.v0",
providerId,
serviceId,
observedAt,
activeSource: "none",
freshness: {
state: "unreachable",
lastSeenAt: null,
maxAgeMs: config.intervalMs * 3,
},
sources: {
cdp: { state: "unreachable", observedAt },
chromeExtension: { state: "missing", observedAt: null },
},
cdp: {
endpoint: `http://${config.host}:${config.port}`,
reachable: false,
error,
},
extension: {
reachable: false,
ingestEndpoint: `http://${config.extensionHost}:${config.extensionPort}/ingest/chrome-extension`,
error: "no extension report received",
},
tabs: [],
summary: {
tabCount: 0,
inspectableTabCount: 0,
activeTabCount: 0,
},
redaction: {
urlQuery: "not-observed",
titles: "not-observed",
pageContent: "not-read",
cookies: "not-read",
source: "none",
},
};
}
}
function normalizeTab(target) {
@ -177,6 +325,14 @@ function buildProviderAdvertisement(snapshot) {
cultMeshAddress: "asgard.localhost.loki.chrome/state/snapshot",
portable: true,
},
{
schema: "gamecult.loki.chrome_extension_report.v0",
owner: providerId,
authority: "external-authority-projection",
storage: "ingest-only",
cultMeshAddress: "asgard.localhost.loki.chrome/ingest/chrome-extension",
portable: true,
},
{
schema: "gamecult.eve.surface.v1",
owner: providerId,
@ -231,6 +387,12 @@ function buildProviderAdvertisement(snapshot) {
carries: ["chrome-target-metadata"],
note: "Requires user-launched Chrome with --remote-debugging-port.",
},
{
kind: "chrome-extension-ingest",
address: snapshot.extension.ingestEndpoint,
carries: ["chrome-extension-tab-metadata"],
note: "Preferred drop-in Chrome path; extension observes tab metadata and Loki owns persistence.",
},
{
kind: "local-cultcache-witness",
address: "state/",
@ -248,7 +410,7 @@ function buildProviderAdvertisement(snapshot) {
}
function buildEveSurface(snapshot) {
const statusTone = snapshot.cdp.reachable ? "ok" : "warning";
const statusTone = snapshot.freshness.state === "fresh" ? "ok" : "warning";
const tabRows = snapshot.tabs.slice(0, 20).map((tab) => ({
id: `tab-${stableId(tab.id)}`,
kind: "inspector.kv",
@ -285,22 +447,33 @@ function buildEveSurface(snapshot) {
{
id: "loki.chrome.summary",
kind: "panel",
props: { title: "Chrome DevTools", tone: statusTone },
props: { title: "Chrome Observation", tone: statusTone },
children: [
metric("reachable", "Reachable", snapshot.cdp.reachable ? "yes" : "no", statusTone),
metric("reachable", "Reachable", snapshot.freshness.state === "fresh" ? "yes" : "no", statusTone),
metric("source", "Source", snapshot.activeSource, statusTone),
metric("tabs", "Tabs", String(snapshot.summary.tabCount), "neutral"),
metric("inspectable", "Inspectable", String(snapshot.summary.inspectableTabCount), "neutral"),
{
id: "loki.chrome.endpoint",
id: "loki.chrome.extension-endpoint",
kind: "inspector.kv",
props: {
label: "Endpoint",
value: snapshot.cdp.endpoint,
tone: statusTone,
label: "Extension ingest",
value: snapshot.extension.ingestEndpoint,
tone: snapshot.extension.reachable ? "ok" : "muted",
},
children: [],
},
snapshot.cdp.error
{
id: "loki.chrome.cdp-endpoint",
kind: "inspector.kv",
props: {
label: "CDP endpoint",
value: snapshot.cdp.endpoint,
tone: snapshot.cdp.reachable ? "ok" : "muted",
},
children: [],
},
snapshot.cdp.error && !snapshot.extension.reachable
? {
id: "loki.chrome.error",
kind: "text",
@ -308,11 +481,11 @@ function buildEveSurface(snapshot) {
children: [],
}
: {
id: "loki.chrome.browser",
id: "loki.chrome.source-state",
kind: "inspector.kv",
props: {
label: "Browser",
value: snapshot.cdp.browser ?? "unknown",
label: "Source state",
value: `extension=${snapshot.sources.chromeExtension.state}; cdp=${snapshot.sources.cdp.state}`,
tone: "neutral",
},
children: [],
@ -368,6 +541,10 @@ function stringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberOrNull(value) {
return Number.isFinite(value) ? value : null;
}
async function writeTypedDocument(filePath, document) {
await mkdir(dirname(filePath), { recursive: true });
const tempPath = `${filePath}.${process.pid}.tmp`;
@ -397,11 +574,107 @@ async function publishSnapshot(config, snapshot) {
}
async function tick(config) {
const snapshot = await observeChrome(config);
const cdpObservation = await observeChrome(config);
const snapshot = buildSnapshot(config, cdpObservation, runtime.extensionObservation);
await publishSnapshot(config, snapshot);
const state = snapshot.freshness.state;
const count = snapshot.summary.tabCount;
console.log(`${snapshot.observedAt} ${state} tabs=${count} endpoint=${snapshot.cdp.endpoint}`);
runtime.lastSnapshot = snapshot;
console.log(`${snapshot.observedAt} ${state} source=${snapshot.activeSource} tabs=${count} cdp=${snapshot.sources.cdp.state} extension=${snapshot.sources.chromeExtension.state}`);
}
const runtime = {
extensionObservation: null,
lastSnapshot: null,
};
function startExtensionIngestServer(config) {
const server = createServer(async (request, response) => {
setCorsHeaders(response);
if (request.method === "OPTIONS") {
response.writeHead(204);
response.end();
return;
}
try {
if (request.method === "GET" && request.url === "/health") {
writeJson(response, 200, {
ok: true,
providerId,
serviceId,
ingest: `http://${config.extensionHost}:${config.extensionPort}/ingest/chrome-extension`,
lastSnapshotAt: runtime.lastSnapshot?.observedAt ?? null,
});
return;
}
if (request.method === "GET" && request.url === "/state") {
writeJson(response, 200, runtime.lastSnapshot ?? unreachableSnapshot(config, "no snapshot published yet"));
return;
}
if (request.method === "POST" && request.url === "/ingest/chrome-extension") {
const payload = await readJsonBody(request);
runtime.extensionObservation = normalizeExtensionObservation(payload, config);
const cdpObservation = await observeChrome(config);
const snapshot = buildSnapshot(config, cdpObservation, runtime.extensionObservation);
await publishSnapshot(config, snapshot);
runtime.lastSnapshot = snapshot;
writeJson(response, 202, {
accepted: true,
schema: snapshot.schema,
activeSource: snapshot.activeSource,
tabs: snapshot.summary.tabCount,
observedAt: snapshot.observedAt,
});
return;
}
writeJson(response, 404, { error: "not found" });
} catch (error) {
writeJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
}
});
server.listen(config.extensionPort, config.extensionHost, () => {
console.log(`Loki extension ingest listening on http://${config.extensionHost}:${config.extensionPort}`);
});
return server;
}
function setCorsHeaders(response) {
response.setHeader("access-control-allow-origin", "*");
response.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
response.setHeader("access-control-allow-headers", "content-type");
}
function writeJson(response, statusCode, body) {
response.writeHead(statusCode, { "content-type": "application/json" });
response.end(JSON.stringify(body));
}
function readJsonBody(request) {
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (body.length > 1024 * 1024) {
request.destroy();
reject(new Error("request body too large"));
}
});
request.on("end", () => {
try {
resolve(JSON.parse(body || "{}"));
} catch (error) {
reject(error);
}
});
request.on("error", reject);
});
}
async function main() {
@ -409,6 +682,7 @@ async function main() {
await tick(config);
if (config.once) return;
startExtensionIngestServer(config);
setInterval(() => {
tick(config).catch((error) => {
console.error(error);

View file

@ -1,31 +1,49 @@
# cultcache-text-witness v0
# schema: gamecult.loki.chrome_snapshot.v0
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
# updatedAt: 2026-06-09T15:11:06.124Z
{
"schema": "gamecult.loki.chrome_snapshot.v0",
"providerId": "loki.chrome",
"serviceId": "loki.chrome_daemon",
"observedAt": "2026-06-09T14:02:52.753Z",
"observedAt": "2026-06-09T15:11:06.124Z",
"activeSource": "none",
"freshness": {
"state": "unreachable",
"lastSeenAt": null,
"maxAgeMs": 15000
},
"sources": {
"cdp": {
"state": "unreachable",
"observedAt": "2026-06-09T15:11:06.096Z"
},
"chromeExtension": {
"state": "missing",
"observedAt": null
}
},
"cdp": {
"endpoint": "http://127.0.0.1:9222",
"reachable": false,
"error": "fetch failed"
},
"extension": {
"reachable": false,
"ingestEndpoint": "http://127.0.0.1:17666/ingest/chrome-extension",
"error": "no extension report received"
},
"tabs": [],
"summary": {
"tabCount": 0,
"inspectableTabCount": 0
"inspectableTabCount": 0,
"activeTabCount": 0
},
"redaction": {
"urlQuery": "not-observed",
"titles": "not-observed",
"pageContent": "not-read",
"cookies": "not-read"
"cookies": "not-read",
"source": "none"
}
}

View file

@ -1,15 +1,15 @@
# cultcache-text-witness v0
# schema: gamecult.eve.surface.v1
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
# updatedAt: 2026-06-09T15:11:06.124Z
{
"type": "surface-state",
"schema": "gamecult.eve.surface.v1",
"providerId": "loki.chrome",
"providerKind": "service.operator",
"title": "Loki Chrome",
"version": 1781013772753,
"updatedAt": "2026-06-09T14:02:52.753Z",
"version": 1781017866124,
"updatedAt": "2026-06-09T15:11:06.124Z",
"surface": {
"root": {
"id": "loki.chrome.root",
@ -27,7 +27,7 @@
"id": "loki.chrome.summary",
"kind": "panel",
"props": {
"title": "Chrome DevTools",
"title": "Chrome Observation",
"tone": "warning"
},
"children": [
@ -41,6 +41,16 @@
},
"children": []
},
{
"id": "loki.chrome.metric.source",
"kind": "metric",
"props": {
"label": "Source",
"value": "none",
"tone": "warning"
},
"children": []
},
{
"id": "loki.chrome.metric.tabs",
"kind": "metric",
@ -62,12 +72,22 @@
"children": []
},
{
"id": "loki.chrome.endpoint",
"id": "loki.chrome.extension-endpoint",
"kind": "inspector.kv",
"props": {
"label": "Endpoint",
"label": "Extension ingest",
"value": "http://127.0.0.1:17666/ingest/chrome-extension",
"tone": "muted"
},
"children": []
},
{
"id": "loki.chrome.cdp-endpoint",
"kind": "inspector.kv",
"props": {
"label": "CDP endpoint",
"value": "http://127.0.0.1:9222",
"tone": "warning"
"tone": "muted"
},
"children": []
},

View file

@ -1,7 +1,7 @@
# cultcache-text-witness v0
# schema: gamecult.eve.provider_advertisement.v1
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
# updatedAt: 2026-06-09T15:11:06.124Z
{
"schema": "gamecult.eve.provider_advertisement.v1",
"providerId": "loki.chrome",
@ -13,7 +13,7 @@
"cultMeshAddress": "asgard.localhost.loki.chrome/eve/gui",
"title": "Loki Chrome",
"kind": "service.operator",
"updatedAt": "2026-06-09T14:02:52.753Z",
"updatedAt": "2026-06-09T15:11:06.124Z",
"freshness": {
"state": "unreachable",
"lastSeenAt": null,
@ -28,6 +28,14 @@
"cultMeshAddress": "asgard.localhost.loki.chrome/state/snapshot",
"portable": true
},
{
"schema": "gamecult.loki.chrome_extension_report.v0",
"owner": "loki.chrome",
"authority": "external-authority-projection",
"storage": "ingest-only",
"cultMeshAddress": "asgard.localhost.loki.chrome/ingest/chrome-extension",
"portable": true
},
{
"schema": "gamecult.eve.surface.v1",
"owner": "loki.chrome",
@ -49,11 +57,12 @@
"urlQuery": "not-observed",
"titles": "not-observed",
"pageContent": "not-read",
"cookies": "not-read"
"cookies": "not-read",
"source": "none"
},
"freshness": {
"state": "unreachable",
"updatedAt": "2026-06-09T14:02:52.753Z"
"updatedAt": "2026-06-09T15:11:06.124Z"
}
},
{
@ -66,7 +75,7 @@
"redaction": "derived-operator-surface",
"freshness": {
"state": "unreachable",
"updatedAt": "2026-06-09T14:02:52.753Z"
"updatedAt": "2026-06-09T15:11:06.124Z"
}
}
],
@ -93,6 +102,14 @@
],
"note": "Requires user-launched Chrome with --remote-debugging-port."
},
{
"kind": "chrome-extension-ingest",
"address": "http://127.0.0.1:17666/ingest/chrome-extension",
"carries": [
"chrome-extension-tab-metadata"
],
"note": "Preferred drop-in Chrome path; extension observes tab metadata and Loki owns persistence."
},
{
"kind": "local-cultcache-witness",
"address": "state/",

View file

@ -0,0 +1,104 @@
import { createServer } from "node:http";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
const stateDir = await mkdtemp(join(tmpdir(), "loki-extension-smoke-"));
const extensionPort = await reservePort();
const cdpPort = await reservePort();
const daemon = spawn(process.execPath, [
"src/loki-daemon.mjs",
"--port",
String(cdpPort),
"--extension-port",
String(extensionPort),
"--interval-ms",
"60000",
"--state-dir",
stateDir,
], {
cwd: new URL("..", import.meta.url),
stdio: "pipe",
});
let stderr = "";
daemon.stderr.on("data", (chunk) => {
stderr += chunk;
});
try {
await waitForHealth(extensionPort);
const response = await fetch(`http://127.0.0.1:${extensionPort}/ingest/chrome-extension`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
schema: "gamecult.loki.chrome_extension_report.v0",
extensionVersion: "fixture",
browser: "Chrome fixture",
reportedAt: new Date().toISOString(),
reason: "smoke",
tabs: [
{
id: 100,
windowId: 1,
index: 0,
active: true,
highlighted: true,
pinned: false,
audible: false,
muted: false,
incognito: false,
title: "Portal das Financas",
url: "https://sitfiscal.portaldasfinancas.gov.pt/geral/siteMap",
favIconUrl: null,
},
],
}),
});
if (!response.ok) {
throw new Error(`ingest failed: HTTP ${response.status} ${await response.text()}`);
}
const snapshotText = await readFile(join(stateDir, "loki.chrome_snapshot.cc"), "utf8");
if (!snapshotText.includes('"activeSource": "chrome-extension"')) {
throw new Error("snapshot did not choose chrome-extension as active source");
}
if (!snapshotText.includes("Portal das Financas")) {
throw new Error("snapshot did not include extension tab");
}
console.log("extension ingest smoke passed");
} finally {
daemon.kill();
await rm(stateDir, { recursive: true, force: true });
}
async function waitForHealth(port) {
const deadline = Date.now() + 5000;
while (Date.now() < deadline) {
try {
const response = await fetch(`http://127.0.0.1:${port}/health`);
if (response.ok) return;
} catch {
await delay(100);
}
}
throw new Error(`daemon did not become healthy: ${stderr}`);
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function reservePort() {
return new Promise((resolve, reject) => {
const server = createServer();
server.on("error", reject);
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
}