Scaffold Loki Chrome CultMesh daemon

This commit is contained in:
Meta Vens 2026-06-09 15:04:37 +01:00
commit c6188d4228
9 changed files with 887 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
state/*.tmp
state/*.log
state/runtime/

59
README.md Normal file
View file

@ -0,0 +1,59 @@
# Loki
Loki is a CultMesh-oriented daemon for observing local Chrome tabs through 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.
## Authority Map
- Owner: Loki owns Chrome DevTools observation and the derived operator surface.
- Inputs: `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.
- Derived state: Eve surface health, tab counts, selected tab hints, and
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.
- Cut line: command execution is not implemented until it has an auditable
command envelope and explicit consent boundary.
## Start Chrome For Observation
Close Chrome or use a separate profile, then launch:
```powershell
& "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9222 `
--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
npm run smoke
```
## State Files
Loki writes:
- `state/loki.chrome_snapshot.cc`
- `state/loki.provider_advertisement.cc`
- `state/loki.eve_surface.cc`
These first `.cc` documents are typed text witnesses. They preserve schema
identity, provider authority, and redaction posture while the daemon awaits a
direct CultCacheTS/CultNetTS backing store integration.

44
docs/architecture.md Normal file
View file

@ -0,0 +1,44 @@
# Loki Architecture
## Objective
Expose local Chrome tab state as provider-owned, typed service state for
CultMesh/Eve consumers.
## 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.
```mermaid
flowchart LR
Chrome["Chrome CDP endpoint"] --> Observe["observeChrome"]
Config["Loki config"] --> Observe
Observe --> Snapshot["chrome_snapshot.cc"]
Snapshot --> Provider["provider_advertisement.cc"]
Snapshot --> Surface["eve_surface.cc"]
```
## Invariants
- CDP 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.
- No renderer or dashboard can override the daemon's observation state.
## Intended Change Path
1. Add CultCacheTS-backed binary `.cc` persistence behind the current writer.
2. Add CultNet/CultMesh publication of the same documents.
3. Add command handling only through `gamecult.eve.command.v1`.
4. Add consentful CDP actions, starting with read-only page metadata and
operator-approved screenshot capture.
## Command Boundary
Loki currently advertises no executable tab commands. This is deliberate. A
daemon that can evaluate JavaScript inside live user tabs is holding a sharp
tool; it needs an explicit command document, audit log, origin, target tab id,
and denial path before it should act.

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "@gamecult/loki",
"version": "0.1.0",
"private": true,
"description": "CultMesh-oriented Chrome DevTools observer daemon.",
"type": "module",
"scripts": {
"start": "node src/loki-daemon.mjs",
"smoke": "node src/loki-daemon.mjs --once",
"test": "node test/fixture-cdp-smoke.mjs"
},
"engines": {
"node": ">=20"
},
"keywords": [
"gamecult",
"cultmesh",
"cultcache",
"chrome-devtools-protocol"
],
"license": "UNLICENSED"
}

422
src/loki-daemon.mjs Normal file
View file

@ -0,0 +1,422 @@
import { mkdir, rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const providerId = "loki.chrome";
const serviceId = "loki.chrome_daemon";
const defaultStateDir = join(repoRoot, "state");
function parseArgs(argv) {
const config = {
host: process.env.LOKI_CHROME_HOST ?? "127.0.0.1",
port: Number(process.env.LOKI_CHROME_PORT ?? 9222),
intervalMs: Number(process.env.LOKI_INTERVAL_MS ?? 5000),
stateDir: process.env.LOKI_STATE_DIR ?? defaultStateDir,
once: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
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 === "--interval-ms") config.intervalMs = Number(argv[++index] ?? config.intervalMs);
else if (arg === "--state-dir") config.stateDir = argv[++index] ?? config.stateDir;
else if (arg === "--help") {
printHelp();
process.exit(0);
}
}
if (!Number.isFinite(config.port) || config.port <= 0) {
throw new Error(`Invalid --port: ${config.port}`);
}
if (!Number.isFinite(config.intervalMs) || config.intervalMs < 1000) {
throw new Error(`Invalid --interval-ms: ${config.intervalMs}`);
}
return config;
}
function printHelp() {
console.log(`Loki Chrome CultMesh daemon
Usage:
node src/loki-daemon.mjs [--once] [--host 127.0.0.1] [--port 9222]
[--interval-ms 5000] [--state-dir state]
`);
}
async function fetchJson(url, timeoutMs = 2500) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
return await response.json();
} finally {
clearTimeout(timeout);
}
}
async function observeChrome(config) {
const baseUrl = `http://${config.host}:${config.port}`;
const observedAt = new Date().toISOString();
try {
const [version, targets] = await Promise.all([
fetchJson(`${baseUrl}/json/version`),
fetchJson(`${baseUrl}/json/list`),
]);
const tabs = Array.isArray(targets)
? targets
.filter((target) => target.type === "page")
.map(normalizeTab)
.sort((left, right) => left.title.localeCompare(right.title) || left.id.localeCompare(right.id))
: [];
return {
schema: "gamecult.loki.chrome_snapshot.v0",
providerId,
serviceId,
observedAt,
freshness: {
state: "fresh",
lastSeenAt: observedAt,
maxAgeMs: config.intervalMs * 3,
},
cdp: {
endpoint: baseUrl,
reachable: true,
browser: stringOrNull(version.Browser),
protocolVersion: stringOrNull(version["Protocol-Version"]),
userAgent: stringOrNull(version["User-Agent"]),
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,
observedAt,
freshness: {
state: "unreachable",
lastSeenAt: null,
maxAgeMs: config.intervalMs * 3,
},
cdp: {
endpoint: baseUrl,
reachable: false,
error: error instanceof Error ? error.message : String(error),
},
tabs: [],
summary: {
tabCount: 0,
inspectableTabCount: 0,
},
redaction: {
urlQuery: "not-observed",
titles: "not-observed",
pageContent: "not-read",
cookies: "not-read",
},
};
}
}
function normalizeTab(target) {
return {
id: String(target.id ?? ""),
title: stringOrNull(target.title),
url: stringOrNull(target.url),
type: stringOrNull(target.type),
faviconUrl: stringOrNull(target.faviconUrl),
devtoolsFrontendUrl: stringOrNull(target.devtoolsFrontendUrl),
webSocketDebuggerUrl: stringOrNull(target.webSocketDebuggerUrl),
attached: Boolean(target.attached),
canInspect: Boolean(target.webSocketDebuggerUrl),
};
}
function buildProviderAdvertisement(snapshot) {
const updatedAt = snapshot.observedAt;
return {
schema: "gamecult.eve.provider_advertisement.v1",
providerId,
serviceId,
verseId: "loki.local",
rootVerse: "asgard",
canonicalService: "asgard.loki.chrome",
locatedService: "asgard.localhost.loki.chrome",
cultMeshAddress: "asgard.localhost.loki.chrome/eve/gui",
title: "Loki Chrome",
kind: "service.operator",
updatedAt,
freshness: snapshot.freshness,
schemas: [
{
schema: "gamecult.loki.chrome_snapshot.v0",
owner: providerId,
authority: "observed",
storage: "cultcache-cc",
cultMeshAddress: "asgard.localhost.loki.chrome/state/snapshot",
portable: true,
},
{
schema: "gamecult.eve.surface.v1",
owner: providerId,
authority: "derived",
storage: "cultcache-cc",
cultMeshAddress: "asgard.localhost.loki.chrome/eve/gui",
portable: true,
},
],
witnesses: [
{
id: "loki.chrome.snapshot",
kind: "cc-export",
path: "state/loki.chrome_snapshot.cc",
schemas: ["gamecult.loki.chrome_snapshot.v0"],
redaction: snapshot.redaction,
freshness: {
state: snapshot.freshness.state,
updatedAt,
},
},
{
id: "loki.chrome.eve_surface",
kind: "cc-export",
path: "state/loki.eve_surface.cc",
schemas: ["gamecult.eve.surface.v1"],
redaction: "derived-operator-surface",
freshness: {
state: snapshot.freshness.state,
updatedAt,
},
},
],
surfaces: [
{
surfaceId: "loki.chrome.operator",
schema: "gamecult.eve.surface.v1",
transport: "cultmesh-document",
address: "asgard.localhost.loki.chrome/eve/gui",
tuiAddress: "asgard.localhost.loki.chrome/eve/tui",
audience: "operator",
mode: "read-only",
styleProfile: "loki.operator",
commands: [],
},
],
commands: [],
routes: [
{
kind: "local-cdp",
address: snapshot.cdp.endpoint,
carries: ["chrome-target-metadata"],
note: "Requires user-launched Chrome with --remote-debugging-port.",
},
{
kind: "local-cultcache-witness",
address: "state/",
carries: [
"state/loki.chrome_snapshot.cc",
"state/loki.provider_advertisement.cc",
"state/loki.eve_surface.cc",
],
},
],
nestedVerses: [],
styleCapabilities: [],
contacts: [],
};
}
function buildEveSurface(snapshot) {
const statusTone = snapshot.cdp.reachable ? "ok" : "warning";
const tabRows = snapshot.tabs.slice(0, 20).map((tab) => ({
id: `tab-${stableId(tab.id)}`,
kind: "inspector.kv",
props: {
label: tab.title || "(untitled)",
value: tab.url || "(no url)",
tone: tab.canInspect ? "ok" : "muted",
meta: {
id: tab.id,
attached: tab.attached,
canInspect: tab.canInspect,
},
},
children: [],
}));
return {
type: "surface-state",
schema: "gamecult.eve.surface.v1",
providerId,
providerKind: "service.operator",
title: "Loki Chrome",
version: Date.parse(snapshot.observedAt),
updatedAt: snapshot.observedAt,
surface: {
root: {
id: "loki.chrome.root",
kind: "surface",
props: {
layout: "operator",
freshness: snapshot.freshness,
},
children: [
{
id: "loki.chrome.summary",
kind: "panel",
props: { title: "Chrome DevTools", tone: statusTone },
children: [
metric("reachable", "Reachable", snapshot.cdp.reachable ? "yes" : "no", statusTone),
metric("tabs", "Tabs", String(snapshot.summary.tabCount), "neutral"),
metric("inspectable", "Inspectable", String(snapshot.summary.inspectableTabCount), "neutral"),
{
id: "loki.chrome.endpoint",
kind: "inspector.kv",
props: {
label: "Endpoint",
value: snapshot.cdp.endpoint,
tone: statusTone,
},
children: [],
},
snapshot.cdp.error
? {
id: "loki.chrome.error",
kind: "text",
props: { text: snapshot.cdp.error, tone: "warning" },
children: [],
}
: {
id: "loki.chrome.browser",
kind: "inspector.kv",
props: {
label: "Browser",
value: snapshot.cdp.browser ?? "unknown",
tone: "neutral",
},
children: [],
},
],
},
{
id: "loki.chrome.tabs",
kind: "panel",
props: { title: "Observed Tabs", empty: snapshot.tabs.length === 0 },
children: tabRows.length > 0
? tabRows
: [
{
id: "loki.chrome.no-tabs",
kind: "text",
props: {
text: "No CDP page targets observed.",
tone: "muted",
},
children: [],
},
],
},
],
},
styles: {
tokens: {
density: "operator",
accent: "#39a275",
warning: "#b7791f",
},
},
},
commands: [],
};
}
function metric(id, label, value, tone) {
return {
id: `loki.chrome.metric.${id}`,
kind: "metric",
props: { label, value, tone },
children: [],
};
}
function stableId(value) {
return String(value || "unknown").replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 80);
}
function stringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
async function writeTypedDocument(filePath, document) {
await mkdir(dirname(filePath), { recursive: true });
const tempPath = `${filePath}.${process.pid}.tmp`;
const body = [
`# cultcache-text-witness v0`,
`# schema: ${document.schema}`,
`# provider: ${document.providerId ?? providerId}`,
`# updatedAt: ${document.updatedAt ?? document.observedAt ?? new Date().toISOString()}`,
JSON.stringify(document, null, 2),
"",
].join("\n");
await writeFile(tempPath, body, "utf8");
await rename(tempPath, filePath);
}
async function publishSnapshot(config, snapshot) {
const advertisement = buildProviderAdvertisement(snapshot);
const surface = buildEveSurface(snapshot);
await Promise.all([
writeTypedDocument(join(config.stateDir, "loki.chrome_snapshot.cc"), snapshot),
writeTypedDocument(join(config.stateDir, "loki.provider_advertisement.cc"), advertisement),
writeTypedDocument(join(config.stateDir, "loki.eve_surface.cc"), surface),
]);
return { advertisement, surface };
}
async function tick(config) {
const snapshot = await observeChrome(config);
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}`);
}
async function main() {
const config = parseArgs(process.argv.slice(2));
await tick(config);
if (config.once) return;
setInterval(() => {
tick(config).catch((error) => {
console.error(error);
});
}, config.intervalMs);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View file

@ -0,0 +1,31 @@
# cultcache-text-witness v0
# schema: gamecult.loki.chrome_snapshot.v0
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
{
"schema": "gamecult.loki.chrome_snapshot.v0",
"providerId": "loki.chrome",
"serviceId": "loki.chrome_daemon",
"observedAt": "2026-06-09T14:02:52.753Z",
"freshness": {
"state": "unreachable",
"lastSeenAt": null,
"maxAgeMs": 15000
},
"cdp": {
"endpoint": "http://127.0.0.1:9222",
"reachable": false,
"error": "fetch failed"
},
"tabs": [],
"summary": {
"tabCount": 0,
"inspectableTabCount": 0
},
"redaction": {
"urlQuery": "not-observed",
"titles": "not-observed",
"pageContent": "not-read",
"cookies": "not-read"
}
}

115
state/loki.eve_surface.cc Normal file
View file

@ -0,0 +1,115 @@
# cultcache-text-witness v0
# schema: gamecult.eve.surface.v1
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
{
"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",
"surface": {
"root": {
"id": "loki.chrome.root",
"kind": "surface",
"props": {
"layout": "operator",
"freshness": {
"state": "unreachable",
"lastSeenAt": null,
"maxAgeMs": 15000
}
},
"children": [
{
"id": "loki.chrome.summary",
"kind": "panel",
"props": {
"title": "Chrome DevTools",
"tone": "warning"
},
"children": [
{
"id": "loki.chrome.metric.reachable",
"kind": "metric",
"props": {
"label": "Reachable",
"value": "no",
"tone": "warning"
},
"children": []
},
{
"id": "loki.chrome.metric.tabs",
"kind": "metric",
"props": {
"label": "Tabs",
"value": "0",
"tone": "neutral"
},
"children": []
},
{
"id": "loki.chrome.metric.inspectable",
"kind": "metric",
"props": {
"label": "Inspectable",
"value": "0",
"tone": "neutral"
},
"children": []
},
{
"id": "loki.chrome.endpoint",
"kind": "inspector.kv",
"props": {
"label": "Endpoint",
"value": "http://127.0.0.1:9222",
"tone": "warning"
},
"children": []
},
{
"id": "loki.chrome.error",
"kind": "text",
"props": {
"text": "fetch failed",
"tone": "warning"
},
"children": []
}
]
},
{
"id": "loki.chrome.tabs",
"kind": "panel",
"props": {
"title": "Observed Tabs",
"empty": true
},
"children": [
{
"id": "loki.chrome.no-tabs",
"kind": "text",
"props": {
"text": "No CDP page targets observed.",
"tone": "muted"
},
"children": []
}
]
}
]
},
"styles": {
"tokens": {
"density": "operator",
"accent": "#39a275",
"warning": "#b7791f"
}
}
},
"commands": []
}

View file

@ -0,0 +1,109 @@
# cultcache-text-witness v0
# schema: gamecult.eve.provider_advertisement.v1
# provider: loki.chrome
# updatedAt: 2026-06-09T14:02:52.753Z
{
"schema": "gamecult.eve.provider_advertisement.v1",
"providerId": "loki.chrome",
"serviceId": "loki.chrome_daemon",
"verseId": "loki.local",
"rootVerse": "asgard",
"canonicalService": "asgard.loki.chrome",
"locatedService": "asgard.localhost.loki.chrome",
"cultMeshAddress": "asgard.localhost.loki.chrome/eve/gui",
"title": "Loki Chrome",
"kind": "service.operator",
"updatedAt": "2026-06-09T14:02:52.753Z",
"freshness": {
"state": "unreachable",
"lastSeenAt": null,
"maxAgeMs": 15000
},
"schemas": [
{
"schema": "gamecult.loki.chrome_snapshot.v0",
"owner": "loki.chrome",
"authority": "observed",
"storage": "cultcache-cc",
"cultMeshAddress": "asgard.localhost.loki.chrome/state/snapshot",
"portable": true
},
{
"schema": "gamecult.eve.surface.v1",
"owner": "loki.chrome",
"authority": "derived",
"storage": "cultcache-cc",
"cultMeshAddress": "asgard.localhost.loki.chrome/eve/gui",
"portable": true
}
],
"witnesses": [
{
"id": "loki.chrome.snapshot",
"kind": "cc-export",
"path": "state/loki.chrome_snapshot.cc",
"schemas": [
"gamecult.loki.chrome_snapshot.v0"
],
"redaction": {
"urlQuery": "not-observed",
"titles": "not-observed",
"pageContent": "not-read",
"cookies": "not-read"
},
"freshness": {
"state": "unreachable",
"updatedAt": "2026-06-09T14:02:52.753Z"
}
},
{
"id": "loki.chrome.eve_surface",
"kind": "cc-export",
"path": "state/loki.eve_surface.cc",
"schemas": [
"gamecult.eve.surface.v1"
],
"redaction": "derived-operator-surface",
"freshness": {
"state": "unreachable",
"updatedAt": "2026-06-09T14:02:52.753Z"
}
}
],
"surfaces": [
{
"surfaceId": "loki.chrome.operator",
"schema": "gamecult.eve.surface.v1",
"transport": "cultmesh-document",
"address": "asgard.localhost.loki.chrome/eve/gui",
"tuiAddress": "asgard.localhost.loki.chrome/eve/tui",
"audience": "operator",
"mode": "read-only",
"styleProfile": "loki.operator",
"commands": []
}
],
"commands": [],
"routes": [
{
"kind": "local-cdp",
"address": "http://127.0.0.1:9222",
"carries": [
"chrome-target-metadata"
],
"note": "Requires user-launched Chrome with --remote-debugging-port."
},
{
"kind": "local-cultcache-witness",
"address": "state/",
"carries": [
"state/loki.chrome_snapshot.cc",
"state/loki.provider_advertisement.cc",
"state/loki.eve_surface.cc"
]
}
],
"nestedVerses": [],
"styleCapabilities": [],
"contacts": []
}

View file

@ -0,0 +1,81 @@
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 fixtures = {
"/json/version": {
Browser: "Chrome/fixture",
"Protocol-Version": "1.3",
"User-Agent": "Fixture",
webSocketDebuggerUrl: "ws://127.0.0.1:0/devtools/browser/fixture",
},
"/json/list": [
{
id: "tab-1",
type: "page",
title: "Portal das Financas",
url: "https://sitfiscal.portaldasfinancas.gov.pt/geral/siteMap",
webSocketDebuggerUrl: "ws://127.0.0.1:0/devtools/page/tab-1",
attached: false,
},
],
};
const server = createServer((request, response) => {
const payload = fixtures[request.url];
if (!payload) {
response.writeHead(404);
response.end("not found");
return;
}
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify(payload));
});
const stateDir = await mkdtemp(join(tmpdir(), "loki-smoke-"));
try {
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();
await runDaemonOnce(port, stateDir);
const snapshotText = await readFile(join(stateDir, "loki.chrome_snapshot.cc"), "utf8");
const surfaceText = await readFile(join(stateDir, "loki.eve_surface.cc"), "utf8");
if (!snapshotText.includes('"state": "fresh"')) throw new Error("snapshot did not become fresh");
if (!snapshotText.includes("Portal das Financas")) throw new Error("snapshot did not include fixture tab");
if (!surfaceText.includes('"schema": "gamecult.eve.surface.v1"')) throw new Error("surface schema missing");
console.log("fixture CDP smoke passed");
} finally {
server.close();
await rm(stateDir, { recursive: true, force: true });
}
function runDaemonOnce(port, targetStateDir) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [
"src/loki-daemon.mjs",
"--once",
"--port",
String(port),
"--state-dir",
targetStateDir,
], {
cwd: new URL("..", import.meta.url),
stdio: "pipe",
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("exit", (code) => {
if (code === 0) resolve();
else reject(new Error(`daemon exited ${code}: ${stderr}`));
});
});
}