Scaffold Loki Chrome CultMesh daemon
This commit is contained in:
commit
c6188d4228
9 changed files with 887 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
state/*.tmp
|
||||
state/*.log
|
||||
state/runtime/
|
||||
59
README.md
Normal file
59
README.md
Normal 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
44
docs/architecture.md
Normal 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
22
package.json
Normal 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
422
src/loki-daemon.mjs
Normal 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;
|
||||
});
|
||||
31
state/loki.chrome_snapshot.cc
Normal file
31
state/loki.chrome_snapshot.cc
Normal 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
115
state/loki.eve_surface.cc
Normal 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": []
|
||||
}
|
||||
109
state/loki.provider_advertisement.cc
Normal file
109
state/loki.provider_advertisement.cc
Normal 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": []
|
||||
}
|
||||
81
test/fixture-cdp-smoke.mjs
Normal file
81
test/fixture-cdp-smoke.mjs
Normal 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}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in a new issue