Add active tab page snapshots
This commit is contained in:
parent
fa3b9c7778
commit
b20fb98c54
8 changed files with 332 additions and 26 deletions
|
|
@ -44,6 +44,12 @@ The extension posts tab metadata to
|
||||||
`http://127.0.0.1:17666/ingest/chrome-extension`. Its normal sync does not read
|
`http://127.0.0.1:17666/ingest/chrome-extension`. Its normal sync does not read
|
||||||
page DOM, cookies, or page content.
|
page DOM, cookies, or page content.
|
||||||
|
|
||||||
|
Use `Snapshot active tab` in the popup when Codex needs help reading a page that
|
||||||
|
only your authenticated Chrome session can see. That action captures only the
|
||||||
|
current active tab, records visible text, headings, links, and visible form
|
||||||
|
controls, and writes `state/loki.chrome_page_snapshot.cc`. It does not read
|
||||||
|
cookies, response bodies, password values, or hidden inputs.
|
||||||
|
|
||||||
The popup also exposes `Debug sweep`. Chrome will warn that Loki can debug
|
The popup also exposes `Debug sweep`. Chrome will warn that Loki can debug
|
||||||
pages because the extension uses `chrome.debugger`. A sweep attaches to each
|
pages because the extension uses `chrome.debugger`. A sweep attaches to each
|
||||||
debuggable tab, probes Chrome Debugger Protocol domains, records frame/runtime/
|
debuggable tab, probes Chrome Debugger Protocol domains, records frame/runtime/
|
||||||
|
|
@ -72,6 +78,7 @@ Loki writes:
|
||||||
|
|
||||||
- `state/loki.chrome_snapshot.cc`
|
- `state/loki.chrome_snapshot.cc`
|
||||||
- `state/loki.chrome_debug_probe.cc`
|
- `state/loki.chrome_debug_probe.cc`
|
||||||
|
- `state/loki.chrome_page_snapshot.cc`
|
||||||
- `state/loki.provider_advertisement.cc`
|
- `state/loki.provider_advertisement.cc`
|
||||||
- `state/loki.eve_surface.cc`
|
- `state/loki.eve_surface.cc`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ CultMesh/Eve consumers while keeping Chrome installation easy for the operator.
|
||||||
## Current Mechanism
|
## Current Mechanism
|
||||||
|
|
||||||
The Chrome extension observes tab metadata with explicit Chrome permissions and
|
The Chrome extension observes tab metadata with explicit Chrome permissions and
|
||||||
posts reports to Loki's localhost ingest endpoint. On command, it also uses
|
posts reports to Loki's localhost ingest endpoint. On explicit operator command
|
||||||
`chrome.debugger` to attach to debuggable tabs, probe Chrome Debugger Protocol
|
from the popup, it can capture the active tab's visible page text, headings,
|
||||||
domains, and detach. Chrome can also expose DevTools metadata on
|
links, and visible form controls for Codex-assisted navigation. On command, it
|
||||||
|
also uses `chrome.debugger` to attach to debuggable tabs, probe Chrome Debugger
|
||||||
|
Protocol domains, and detach. Chrome can also expose DevTools metadata on
|
||||||
`127.0.0.1:{port}` when started with `--remote-debugging-port`. Loki normalizes
|
`127.0.0.1:{port}` when started with `--remote-debugging-port`. Loki normalizes
|
||||||
these sources into one snapshot and writes typed witness documents from that
|
these sources into one snapshot and writes typed witness documents from that
|
||||||
state.
|
state.
|
||||||
|
|
@ -18,6 +20,7 @@ state.
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
Extension["Chrome extension"] --> Ingest["extension ingest"]
|
Extension["Chrome extension"] --> Ingest["extension ingest"]
|
||||||
|
Snapshot["active page snapshot"] --> Ingest
|
||||||
Debugger["chrome.debugger sweep"] --> Ingest
|
Debugger["chrome.debugger sweep"] --> Ingest
|
||||||
Chrome["Chrome CDP endpoint"] --> Observe["observe CDP"]
|
Chrome["Chrome CDP endpoint"] --> Observe["observe CDP"]
|
||||||
Config["Loki config"] --> Ingest
|
Config["Loki config"] --> Ingest
|
||||||
|
|
@ -35,6 +38,8 @@ flowchart LR
|
||||||
- CDP reachability is observed state, not assumed capability.
|
- CDP reachability is observed state, not assumed capability.
|
||||||
- Extension reachability is observed state, not assumed capability.
|
- Extension reachability is observed state, not assumed capability.
|
||||||
- Debug sweeps are explicit observations, not continuous hidden attachment.
|
- Debug sweeps are explicit observations, not continuous hidden attachment.
|
||||||
|
- Page snapshots are explicit active-tab observations, not continuous page
|
||||||
|
surveillance.
|
||||||
- Chrome owns page reality; Loki owns only the observation snapshot.
|
- Chrome owns page reality; Loki owns only the observation snapshot.
|
||||||
- `.cc` witnesses are the durable local state surface.
|
- `.cc` witnesses are the durable local state surface.
|
||||||
- Eve/CultUI output is derived from the same snapshot as the witness.
|
- Eve/CultUI output is derived from the same snapshot as the witness.
|
||||||
|
|
@ -45,12 +50,12 @@ flowchart LR
|
||||||
1. Add CultCacheTS-backed binary `.cc` persistence behind the current writer.
|
1. Add CultCacheTS-backed binary `.cc` persistence behind the current writer.
|
||||||
2. Add CultNet/CultMesh publication of the same documents.
|
2. Add CultNet/CultMesh publication of the same documents.
|
||||||
3. Add command handling only through `gamecult.eve.command.v1`.
|
3. Add command handling only through `gamecult.eve.command.v1`.
|
||||||
4. Add consentful CDP actions, starting with read-only page metadata and
|
4. Add operator-approved screenshot capture.
|
||||||
operator-approved screenshot capture.
|
|
||||||
|
|
||||||
## Command Boundary
|
## Command Boundary
|
||||||
|
|
||||||
Loki currently advertises no executable tab commands. This is deliberate. A
|
Loki currently advertises no executable tab commands. Page snapshot capture is
|
||||||
daemon that can evaluate JavaScript inside live user tabs is holding a sharp
|
extension-owned and popup-triggered: the operator clicks once, the extension
|
||||||
tool; it needs an explicit command document, audit log, origin, target tab id,
|
captures the active tab, posts the typed observation, and detaches. A future
|
||||||
and denial path before it should act.
|
daemon-originated command path still needs an explicit command document, audit
|
||||||
|
log, origin, target tab id, and denial path before it should act.
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ dl {
|
||||||
|
|
||||||
dl div {
|
dl div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 72px 1fr;
|
grid-template-columns: 88px 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,16 @@
|
||||||
<dt>Debug</dt>
|
<dt>Debug</dt>
|
||||||
<dd id="debug">-</dd>
|
<dd id="debug">-</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Page</dt>
|
||||||
|
<dd id="page-snapshot">-</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<p id="error" class="error" hidden></p>
|
<p id="error" class="error" hidden></p>
|
||||||
|
|
||||||
<button id="sync" type="button">Sync now</button>
|
<button id="sync" type="button">Sync now</button>
|
||||||
|
<button id="snapshot-active-tab" type="button">Snapshot active tab</button>
|
||||||
<button id="debug-sweep" type="button">Debug sweep</button>
|
<button id="debug-sweep" type="button">Debug sweep</button>
|
||||||
</main>
|
</main>
|
||||||
<script src="popup.js" type="module"></script>
|
<script src="popup.js" type="module"></script>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ const tabsEl = document.querySelector("#tabs");
|
||||||
const lastSyncEl = document.querySelector("#last-sync");
|
const lastSyncEl = document.querySelector("#last-sync");
|
||||||
const endpointEl = document.querySelector("#endpoint");
|
const endpointEl = document.querySelector("#endpoint");
|
||||||
const debugEl = document.querySelector("#debug");
|
const debugEl = document.querySelector("#debug");
|
||||||
|
const pageSnapshotEl = document.querySelector("#page-snapshot");
|
||||||
const errorEl = document.querySelector("#error");
|
const errorEl = document.querySelector("#error");
|
||||||
const syncButton = document.querySelector("#sync");
|
const syncButton = document.querySelector("#sync");
|
||||||
|
const snapshotActiveTabButton = document.querySelector("#snapshot-active-tab");
|
||||||
const debugSweepButton = document.querySelector("#debug-sweep");
|
const debugSweepButton = document.querySelector("#debug-sweep");
|
||||||
|
|
||||||
syncButton.addEventListener("click", async () => {
|
syncButton.addEventListener("click", async () => {
|
||||||
|
|
@ -21,6 +23,13 @@ debugSweepButton.addEventListener("click", async () => {
|
||||||
debugSweepButton.disabled = false;
|
debugSweepButton.disabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
snapshotActiveTabButton.addEventListener("click", async () => {
|
||||||
|
snapshotActiveTabButton.disabled = true;
|
||||||
|
await chrome.runtime.sendMessage({ type: "loki.snapshot-active-tab" });
|
||||||
|
await render();
|
||||||
|
snapshotActiveTabButton.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
await render();
|
await render();
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
|
|
@ -33,6 +42,7 @@ async function render() {
|
||||||
stateEl.className = "state";
|
stateEl.className = "state";
|
||||||
tabsEl.textContent = "-";
|
tabsEl.textContent = "-";
|
||||||
debugEl.textContent = "-";
|
debugEl.textContent = "-";
|
||||||
|
pageSnapshotEl.textContent = "-";
|
||||||
lastSyncEl.textContent = "-";
|
lastSyncEl.textContent = "-";
|
||||||
errorEl.hidden = true;
|
errorEl.hidden = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -42,6 +52,7 @@ async function render() {
|
||||||
stateEl.className = `state ${status.ok ? "ok" : "bad"}`;
|
stateEl.className = `state ${status.ok ? "ok" : "bad"}`;
|
||||||
tabsEl.textContent = String(status.tabCount ?? 0);
|
tabsEl.textContent = String(status.tabCount ?? 0);
|
||||||
debugEl.textContent = formatDebugProbe(status.debugProbe);
|
debugEl.textContent = formatDebugProbe(status.debugProbe);
|
||||||
|
pageSnapshotEl.textContent = formatPageSnapshot(status.pageSnapshot);
|
||||||
lastSyncEl.textContent = status.lastSyncAt ? new Date(status.lastSyncAt).toLocaleTimeString() : "-";
|
lastSyncEl.textContent = status.lastSyncAt ? new Date(status.lastSyncAt).toLocaleTimeString() : "-";
|
||||||
|
|
||||||
if (status.error) {
|
if (status.error) {
|
||||||
|
|
@ -57,3 +68,11 @@ function formatDebugProbe(debugProbe) {
|
||||||
const summary = debugProbe.summary ?? {};
|
const summary = debugProbe.summary ?? {};
|
||||||
return `${summary.attachedTabCount ?? 0}/${summary.attemptedTabCount ?? 0} attached`;
|
return `${summary.attachedTabCount ?? 0}/${summary.attemptedTabCount ?? 0} attached`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPageSnapshot(pageSnapshot) {
|
||||||
|
if (!pageSnapshot) return "not captured";
|
||||||
|
if (pageSnapshot.error) return pageSnapshot.error;
|
||||||
|
const textLength = pageSnapshot.content?.visibleTextLength ?? 0;
|
||||||
|
const controlCount = pageSnapshot.forms?.controlCount ?? 0;
|
||||||
|
return `${textLength} chars, ${controlCount} controls`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
debugSweep().then(sendResponse);
|
debugSweep().then(sendResponse);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (message?.type === "loki.snapshot-active-tab") {
|
||||||
|
snapshotActiveTab().then(sendResponse);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (message?.type === "loki.get-status") {
|
if (message?.type === "loki.get-status") {
|
||||||
getStatus().then(sendResponse);
|
getStatus().then(sendResponse);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -63,10 +67,11 @@ async function syncTabs(reason, extra = {}) {
|
||||||
activeTabCount: tabs.filter((tab) => tab.active).length,
|
activeTabCount: tabs.filter((tab) => tab.active).length,
|
||||||
},
|
},
|
||||||
redaction: {
|
redaction: {
|
||||||
pageContent: "not-read",
|
pageContent: extra.pageSnapshot ? "explicit-active-tab-visible-text" : "not-read",
|
||||||
cookies: "not-read",
|
cookies: "not-read",
|
||||||
tabMetadata: "title-url-favicon-window-state",
|
tabMetadata: "title-url-favicon-window-state",
|
||||||
debugProbe: extra.debugProbe ? "runtime-frame-performance-security-metadata" : "not-run",
|
debugProbe: extra.debugProbe ? "runtime-frame-performance-security-metadata" : "not-run",
|
||||||
|
pageSnapshot: extra.pageSnapshot ? extra.pageSnapshot.redaction : "not-run",
|
||||||
},
|
},
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
|
|
@ -89,6 +94,7 @@ async function syncTabs(reason, extra = {}) {
|
||||||
lastSyncAt: new Date().toISOString(),
|
lastSyncAt: new Date().toISOString(),
|
||||||
tabCount: tabs.length,
|
tabCount: tabs.length,
|
||||||
debugProbe: responseBody.debugProbe ?? null,
|
debugProbe: responseBody.debugProbe ?? null,
|
||||||
|
pageSnapshot: responseBody.pageSnapshot ?? null,
|
||||||
activeSource: responseBody.activeSource ?? "unknown",
|
activeSource: responseBody.activeSource ?? "unknown",
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
@ -103,6 +109,7 @@ async function syncTabs(reason, extra = {}) {
|
||||||
lastSyncAt: new Date().toISOString(),
|
lastSyncAt: new Date().toISOString(),
|
||||||
tabCount: 0,
|
tabCount: 0,
|
||||||
debugProbe: null,
|
debugProbe: null,
|
||||||
|
pageSnapshot: null,
|
||||||
activeSource: "none",
|
activeSource: "none",
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
};
|
};
|
||||||
|
|
@ -111,6 +118,164 @@ async function syncTabs(reason, extra = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function snapshotActiveTab() {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
if (!tab || !Number.isInteger(tab.id)) {
|
||||||
|
return syncTabs("active-page-snapshot", {
|
||||||
|
pageSnapshot: failedPageSnapshot(startedAt, "No active tab is available."),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageSnapshot = await capturePageSnapshot(tab, startedAt);
|
||||||
|
return syncTabs("active-page-snapshot", { pageSnapshot });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function capturePageSnapshot(tab, startedAt) {
|
||||||
|
const target = { tabId: tab.id };
|
||||||
|
const pageSnapshot = {
|
||||||
|
schema: "gamecult.loki.chrome_page_snapshot.v0",
|
||||||
|
capturedAt: startedAt,
|
||||||
|
completedAt: null,
|
||||||
|
tab: projectTab(tab),
|
||||||
|
attached: false,
|
||||||
|
attachError: null,
|
||||||
|
error: null,
|
||||||
|
content: null,
|
||||||
|
forms: null,
|
||||||
|
redaction: {
|
||||||
|
trigger: "operator-popup-click",
|
||||||
|
scope: "active-tab-only",
|
||||||
|
pageContent: "visible-text-and-page-structure",
|
||||||
|
cookies: "not-read",
|
||||||
|
passwordValues: "redacted",
|
||||||
|
hiddenInputs: "omitted",
|
||||||
|
responseBodies: "not-read",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chromeDebuggerAttach(target, "1.3");
|
||||||
|
pageSnapshot.attached = true;
|
||||||
|
await chromeDebuggerSendCommand(target, "Runtime.enable");
|
||||||
|
const result = await chromeDebuggerSendCommand(target, "Runtime.evaluate", {
|
||||||
|
expression: pageSnapshotExpression(),
|
||||||
|
returnByValue: true,
|
||||||
|
awaitPromise: true,
|
||||||
|
});
|
||||||
|
const value = result?.result?.value;
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
throw new Error("Page snapshot did not return an object.");
|
||||||
|
}
|
||||||
|
pageSnapshot.content = value.content ?? null;
|
||||||
|
pageSnapshot.forms = value.forms ?? null;
|
||||||
|
} catch (error) {
|
||||||
|
pageSnapshot.error = error instanceof Error ? error.message : String(error);
|
||||||
|
if (!pageSnapshot.attached) pageSnapshot.attachError = pageSnapshot.error;
|
||||||
|
} finally {
|
||||||
|
pageSnapshot.completedAt = new Date().toISOString();
|
||||||
|
if (pageSnapshot.attached) {
|
||||||
|
await chromeDebuggerDetach(target).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function failedPageSnapshot(startedAt, error) {
|
||||||
|
return {
|
||||||
|
schema: "gamecult.loki.chrome_page_snapshot.v0",
|
||||||
|
capturedAt: startedAt,
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
tab: null,
|
||||||
|
attached: false,
|
||||||
|
attachError: error,
|
||||||
|
error,
|
||||||
|
content: null,
|
||||||
|
forms: null,
|
||||||
|
redaction: {
|
||||||
|
trigger: "operator-popup-click",
|
||||||
|
scope: "active-tab-only",
|
||||||
|
pageContent: "not-read",
|
||||||
|
cookies: "not-read",
|
||||||
|
passwordValues: "not-read",
|
||||||
|
hiddenInputs: "not-read",
|
||||||
|
responseBodies: "not-read",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageSnapshotExpression() {
|
||||||
|
return `(() => {
|
||||||
|
const limit = (value, max) => String(value ?? "").replace(/\\s+/g, " ").trim().slice(0, max);
|
||||||
|
const selectorFor = (element) => {
|
||||||
|
if (!element) return null;
|
||||||
|
if (element.id) return "#" + CSS.escape(element.id);
|
||||||
|
const name = element.getAttribute("name");
|
||||||
|
if (name) return element.tagName.toLowerCase() + "[name=" + JSON.stringify(name) + "]";
|
||||||
|
return element.tagName.toLowerCase();
|
||||||
|
};
|
||||||
|
const labelFor = (control) => {
|
||||||
|
const explicit = control.id ? document.querySelector("label[for=" + JSON.stringify(control.id) + "]") : null;
|
||||||
|
const implicit = control.closest("label");
|
||||||
|
const aria = control.getAttribute("aria-label") || control.getAttribute("title") || control.getAttribute("placeholder");
|
||||||
|
return limit(explicit?.innerText || implicit?.innerText || aria || "", 300);
|
||||||
|
};
|
||||||
|
const controls = Array.from(document.querySelectorAll("input, select, textarea, button"))
|
||||||
|
.filter((control) => {
|
||||||
|
const type = (control.getAttribute("type") || control.tagName).toLowerCase();
|
||||||
|
return type !== "hidden" && type !== "password";
|
||||||
|
})
|
||||||
|
.slice(0, 300)
|
||||||
|
.map((control) => {
|
||||||
|
const tag = control.tagName.toLowerCase();
|
||||||
|
const type = (control.getAttribute("type") || tag).toLowerCase();
|
||||||
|
const option = tag === "select" ? control.selectedOptions?.[0] : null;
|
||||||
|
const value = type === "checkbox" || type === "radio"
|
||||||
|
? String(Boolean(control.checked))
|
||||||
|
: tag === "select"
|
||||||
|
? limit(option?.innerText || option?.label || "", 500)
|
||||||
|
: limit(control.value || "", 500);
|
||||||
|
return {
|
||||||
|
tag,
|
||||||
|
type,
|
||||||
|
label: labelFor(control),
|
||||||
|
selector: selectorFor(control),
|
||||||
|
name: control.getAttribute("name"),
|
||||||
|
id: control.id || null,
|
||||||
|
disabled: Boolean(control.disabled),
|
||||||
|
required: Boolean(control.required),
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const links = Array.from(document.links).slice(0, 200).map((link) => ({
|
||||||
|
text: limit(link.innerText || link.textContent || "", 300),
|
||||||
|
href: link.href,
|
||||||
|
}));
|
||||||
|
const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6")).slice(0, 80).map((heading) => ({
|
||||||
|
level: heading.tagName.toLowerCase(),
|
||||||
|
text: limit(heading.innerText || heading.textContent || "", 500),
|
||||||
|
}));
|
||||||
|
const visibleText = limit(document.body?.innerText || "", 60000);
|
||||||
|
return {
|
||||||
|
content: {
|
||||||
|
href: location.href,
|
||||||
|
title: document.title,
|
||||||
|
readyState: document.readyState,
|
||||||
|
language: document.documentElement.lang || null,
|
||||||
|
visibleText,
|
||||||
|
visibleTextLength: visibleText.length,
|
||||||
|
headings,
|
||||||
|
links,
|
||||||
|
},
|
||||||
|
forms: {
|
||||||
|
controlCount: controls.length,
|
||||||
|
controls,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
async function debugSweep() {
|
async function debugSweep() {
|
||||||
const startedAt = new Date().toISOString();
|
const startedAt = new Date().toISOString();
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await chrome.tabs.query({});
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ function normalizeExtensionObservation(payload, config) {
|
||||||
maxAgeMs: config.intervalMs * 3,
|
maxAgeMs: config.intervalMs * 3,
|
||||||
},
|
},
|
||||||
debugProbe: normalizeDebugProbe(payload.debugProbe),
|
debugProbe: normalizeDebugProbe(payload.debugProbe),
|
||||||
|
pageSnapshot: normalizePageSnapshot(payload.pageSnapshot),
|
||||||
tabs,
|
tabs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +167,16 @@ function normalizeDebugProbe(debugProbe) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePageSnapshot(pageSnapshot) {
|
||||||
|
if (!pageSnapshot || pageSnapshot.schema !== "gamecult.loki.chrome_page_snapshot.v0") return null;
|
||||||
|
return {
|
||||||
|
...pageSnapshot,
|
||||||
|
providerId,
|
||||||
|
serviceId,
|
||||||
|
ingestedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeExtensionTab(tab) {
|
function normalizeExtensionTab(tab) {
|
||||||
return {
|
return {
|
||||||
id: String(tab.id ?? ""),
|
id: String(tab.id ?? ""),
|
||||||
|
|
@ -234,6 +245,7 @@ function buildSnapshot(config, cdpObservation, extensionObservation) {
|
||||||
cdp,
|
cdp,
|
||||||
extension,
|
extension,
|
||||||
debugProbe: extensionObservation?.debugProbe ?? null,
|
debugProbe: extensionObservation?.debugProbe ?? null,
|
||||||
|
pageSnapshot: extensionObservation?.pageSnapshot ?? null,
|
||||||
tabs,
|
tabs,
|
||||||
summary: {
|
summary: {
|
||||||
tabCount: tabs.length,
|
tabCount: tabs.length,
|
||||||
|
|
@ -241,14 +253,17 @@ function buildSnapshot(config, cdpObservation, extensionObservation) {
|
||||||
activeTabCount: tabs.filter((tab) => tab.active).length,
|
activeTabCount: tabs.filter((tab) => tab.active).length,
|
||||||
debugProbeAttachedTabCount: extensionObservation?.debugProbe?.summary?.attachedTabCount ?? 0,
|
debugProbeAttachedTabCount: extensionObservation?.debugProbe?.summary?.attachedTabCount ?? 0,
|
||||||
debugProbeAttemptedTabCount: extensionObservation?.debugProbe?.summary?.attemptedTabCount ?? 0,
|
debugProbeAttemptedTabCount: extensionObservation?.debugProbe?.summary?.attemptedTabCount ?? 0,
|
||||||
|
pageSnapshotVisibleTextLength: extensionObservation?.pageSnapshot?.content?.visibleTextLength ?? 0,
|
||||||
|
pageSnapshotFormControlCount: extensionObservation?.pageSnapshot?.forms?.controlCount ?? 0,
|
||||||
},
|
},
|
||||||
redaction: {
|
redaction: {
|
||||||
urlQuery: activeObservation ? "preserved" : "not-observed",
|
urlQuery: activeObservation ? "preserved" : "not-observed",
|
||||||
titles: activeObservation ? "preserved" : "not-observed",
|
titles: activeObservation ? "preserved" : "not-observed",
|
||||||
pageContent: "not-read",
|
pageContent: extensionObservation?.pageSnapshot ? "explicit-active-tab-visible-text" : "not-read",
|
||||||
cookies: "not-read",
|
cookies: "not-read",
|
||||||
source: activeObservation?.source ?? "none",
|
source: activeObservation?.source ?? "none",
|
||||||
debugProbe: extensionObservation?.debugProbe ? extensionObservation.debugProbe.redaction : "not-run",
|
debugProbe: extensionObservation?.debugProbe ? extensionObservation.debugProbe.redaction : "not-run",
|
||||||
|
pageSnapshot: extensionObservation?.pageSnapshot ? extensionObservation.pageSnapshot.redaction : "not-run",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -287,18 +302,24 @@ function unreachableSnapshot(config, error) {
|
||||||
error: "no extension report received",
|
error: "no extension report received",
|
||||||
},
|
},
|
||||||
tabs: [],
|
tabs: [],
|
||||||
summary: {
|
pageSnapshot: null,
|
||||||
tabCount: 0,
|
debugProbe: null,
|
||||||
inspectableTabCount: 0,
|
summary: {
|
||||||
activeTabCount: 0,
|
tabCount: 0,
|
||||||
},
|
inspectableTabCount: 0,
|
||||||
redaction: {
|
activeTabCount: 0,
|
||||||
urlQuery: "not-observed",
|
pageSnapshotVisibleTextLength: 0,
|
||||||
titles: "not-observed",
|
pageSnapshotFormControlCount: 0,
|
||||||
pageContent: "not-read",
|
},
|
||||||
cookies: "not-read",
|
redaction: {
|
||||||
source: "none",
|
urlQuery: "not-observed",
|
||||||
},
|
titles: "not-observed",
|
||||||
|
pageContent: "not-read",
|
||||||
|
cookies: "not-read",
|
||||||
|
source: "none",
|
||||||
|
pageSnapshot: "not-run",
|
||||||
|
debugProbe: "not-run",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,6 +377,14 @@ function buildProviderAdvertisement(snapshot) {
|
||||||
cultMeshAddress: "asgard.localhost.loki.chrome/state/debug-probe",
|
cultMeshAddress: "asgard.localhost.loki.chrome/state/debug-probe",
|
||||||
portable: true,
|
portable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
schema: "gamecult.loki.chrome_page_snapshot.v0",
|
||||||
|
owner: providerId,
|
||||||
|
authority: "operator-approved-observation",
|
||||||
|
storage: "cultcache-cc",
|
||||||
|
cultMeshAddress: "asgard.localhost.loki.chrome/state/page-snapshot",
|
||||||
|
portable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
schema: "gamecult.eve.surface.v1",
|
schema: "gamecult.eve.surface.v1",
|
||||||
owner: providerId,
|
owner: providerId,
|
||||||
|
|
@ -399,6 +428,17 @@ function buildProviderAdvertisement(snapshot) {
|
||||||
updatedAt: snapshot.debugProbe?.ingestedAt ?? null,
|
updatedAt: snapshot.debugProbe?.ingestedAt ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "loki.chrome.page_snapshot",
|
||||||
|
kind: "cc-export",
|
||||||
|
path: "state/loki.chrome_page_snapshot.cc",
|
||||||
|
schemas: ["gamecult.loki.chrome_page_snapshot.v0"],
|
||||||
|
redaction: snapshot.pageSnapshot?.redaction ?? "not-run",
|
||||||
|
freshness: {
|
||||||
|
state: snapshot.pageSnapshot ? "fresh" : "missing",
|
||||||
|
updatedAt: snapshot.pageSnapshot?.ingestedAt ?? null,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
surfaces: [
|
surfaces: [
|
||||||
{
|
{
|
||||||
|
|
@ -424,8 +464,8 @@ function buildProviderAdvertisement(snapshot) {
|
||||||
{
|
{
|
||||||
kind: "chrome-extension-ingest",
|
kind: "chrome-extension-ingest",
|
||||||
address: snapshot.extension.ingestEndpoint,
|
address: snapshot.extension.ingestEndpoint,
|
||||||
carries: ["chrome-extension-tab-metadata"],
|
carries: ["chrome-extension-tab-metadata", "explicit-active-page-snapshot"],
|
||||||
note: "Preferred drop-in Chrome path; extension observes tab metadata and can run explicit chrome.debugger sweeps.",
|
note: "Preferred drop-in Chrome path; extension observes tab metadata and can run explicit operator-triggered page snapshots and chrome.debugger sweeps.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "local-cultcache-witness",
|
kind: "local-cultcache-witness",
|
||||||
|
|
@ -435,6 +475,7 @@ function buildProviderAdvertisement(snapshot) {
|
||||||
"state/loki.provider_advertisement.cc",
|
"state/loki.provider_advertisement.cc",
|
||||||
"state/loki.eve_surface.cc",
|
"state/loki.eve_surface.cc",
|
||||||
"state/loki.chrome_debug_probe.cc",
|
"state/loki.chrome_debug_probe.cc",
|
||||||
|
"state/loki.chrome_page_snapshot.cc",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -496,6 +537,14 @@ function buildEveSurface(snapshot) {
|
||||||
: "not run",
|
: "not run",
|
||||||
snapshot.debugProbe ? "ok" : "muted",
|
snapshot.debugProbe ? "ok" : "muted",
|
||||||
),
|
),
|
||||||
|
metric(
|
||||||
|
"page-snapshot",
|
||||||
|
"Page snapshot",
|
||||||
|
snapshot.pageSnapshot
|
||||||
|
? `${snapshot.summary.pageSnapshotVisibleTextLength} chars`
|
||||||
|
: "not captured",
|
||||||
|
snapshot.pageSnapshot ? "ok" : "muted",
|
||||||
|
),
|
||||||
{
|
{
|
||||||
id: "loki.chrome.extension-endpoint",
|
id: "loki.chrome.extension-endpoint",
|
||||||
kind: "inspector.kv",
|
kind: "inspector.kv",
|
||||||
|
|
@ -614,6 +663,9 @@ async function publishSnapshot(config, snapshot) {
|
||||||
snapshot.debugProbe
|
snapshot.debugProbe
|
||||||
? writeTypedDocument(join(config.stateDir, "loki.chrome_debug_probe.cc"), snapshot.debugProbe)
|
? writeTypedDocument(join(config.stateDir, "loki.chrome_debug_probe.cc"), snapshot.debugProbe)
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
|
snapshot.pageSnapshot
|
||||||
|
? writeTypedDocument(join(config.stateDir, "loki.chrome_page_snapshot.cc"), snapshot.pageSnapshot)
|
||||||
|
: Promise.resolve(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { advertisement, surface };
|
return { advertisement, surface };
|
||||||
|
|
@ -674,6 +726,7 @@ function startExtensionIngestServer(config) {
|
||||||
activeSource: snapshot.activeSource,
|
activeSource: snapshot.activeSource,
|
||||||
tabs: snapshot.summary.tabCount,
|
tabs: snapshot.summary.tabCount,
|
||||||
debugProbe: snapshot.debugProbe ? snapshot.debugProbe : null,
|
debugProbe: snapshot.debugProbe ? snapshot.debugProbe : null,
|
||||||
|
pageSnapshot: snapshot.pageSnapshot ? snapshot.pageSnapshot : null,
|
||||||
observedAt: snapshot.observedAt,
|
observedAt: snapshot.observedAt,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,51 @@ try {
|
||||||
runtimeEvaluation: "fixture",
|
runtimeEvaluation: "fixture",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pageSnapshot: {
|
||||||
|
schema: "gamecult.loki.chrome_page_snapshot.v0",
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
tab: {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
attached: true,
|
||||||
|
attachError: null,
|
||||||
|
error: null,
|
||||||
|
content: {
|
||||||
|
href: "https://sitfiscal.portaldasfinancas.gov.pt/geral/siteMap",
|
||||||
|
title: "Portal das Financas",
|
||||||
|
readyState: "complete",
|
||||||
|
language: "pt",
|
||||||
|
visibleText: "Entregar declaracao IRS Consultar pagamentos",
|
||||||
|
visibleTextLength: 43,
|
||||||
|
headings: [{ level: "h1", text: "Portal das Financas" }],
|
||||||
|
links: [{ text: "Consultar pagamentos", href: "https://sitfiscal.portaldasfinancas.gov.pt/pagamentos" }],
|
||||||
|
},
|
||||||
|
forms: {
|
||||||
|
controlCount: 1,
|
||||||
|
controls: [{ tag: "button", type: "button", label: "", selector: "button", name: null, id: null, disabled: false, required: false, value: "" }],
|
||||||
|
},
|
||||||
|
redaction: {
|
||||||
|
trigger: "operator-popup-click",
|
||||||
|
scope: "active-tab-only",
|
||||||
|
pageContent: "visible-text-and-page-structure",
|
||||||
|
cookies: "not-read",
|
||||||
|
passwordValues: "redacted",
|
||||||
|
hiddenInputs: "omitted",
|
||||||
|
responseBodies: "not-read",
|
||||||
|
},
|
||||||
|
},
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
id: 100,
|
id: 100,
|
||||||
|
|
@ -99,6 +144,7 @@ try {
|
||||||
|
|
||||||
const snapshotText = await readFile(join(stateDir, "loki.chrome_snapshot.cc"), "utf8");
|
const snapshotText = await readFile(join(stateDir, "loki.chrome_snapshot.cc"), "utf8");
|
||||||
const debugProbeText = await readFile(join(stateDir, "loki.chrome_debug_probe.cc"), "utf8");
|
const debugProbeText = await readFile(join(stateDir, "loki.chrome_debug_probe.cc"), "utf8");
|
||||||
|
const pageSnapshotText = await readFile(join(stateDir, "loki.chrome_page_snapshot.cc"), "utf8");
|
||||||
if (!snapshotText.includes('"activeSource": "chrome-extension"')) {
|
if (!snapshotText.includes('"activeSource": "chrome-extension"')) {
|
||||||
throw new Error("snapshot did not choose chrome-extension as active source");
|
throw new Error("snapshot did not choose chrome-extension as active source");
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +154,12 @@ try {
|
||||||
if (!debugProbeText.includes('"schema": "gamecult.loki.chrome_debug_probe.v0"')) {
|
if (!debugProbeText.includes('"schema": "gamecult.loki.chrome_debug_probe.v0"')) {
|
||||||
throw new Error("debug probe witness was not written");
|
throw new Error("debug probe witness was not written");
|
||||||
}
|
}
|
||||||
|
if (!pageSnapshotText.includes('"schema": "gamecult.loki.chrome_page_snapshot.v0"')) {
|
||||||
|
throw new Error("page snapshot witness was not written");
|
||||||
|
}
|
||||||
|
if (!pageSnapshotText.includes("Entregar declaracao IRS")) {
|
||||||
|
throw new Error("page snapshot did not preserve visible page text");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("extension ingest smoke passed");
|
console.log("extension ingest smoke passed");
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
Reference in a new issue