Add active tab page snapshots

This commit is contained in:
Meta Vens 2026-06-13 19:46:10 +01:00
commit b20fb98c54
8 changed files with 332 additions and 26 deletions

View file

@ -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`

View file

@ -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.

View file

@ -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;
} }

View file

@ -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>

View file

@ -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`;
}

View file

@ -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({});

View file

@ -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;

View file

@ -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 {