diff --git a/README.md b/README.md
index 74333c5..307a260 100644
--- a/README.md
+++ b/README.md
@@ -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
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
pages because the extension uses `chrome.debugger`. A sweep attaches to each
debuggable tab, probes Chrome Debugger Protocol domains, records frame/runtime/
@@ -72,6 +78,7 @@ Loki writes:
- `state/loki.chrome_snapshot.cc`
- `state/loki.chrome_debug_probe.cc`
+- `state/loki.chrome_page_snapshot.cc`
- `state/loki.provider_advertisement.cc`
- `state/loki.eve_surface.cc`
diff --git a/docs/architecture.md b/docs/architecture.md
index 0ee2ea6..bff0f40 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -8,9 +8,11 @@ CultMesh/Eve consumers while keeping Chrome installation easy for the operator.
## Current Mechanism
The Chrome extension observes tab metadata with explicit Chrome permissions and
-posts reports to Loki's localhost ingest endpoint. 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
+posts reports to Loki's localhost ingest endpoint. On explicit operator command
+from the popup, it can capture the active tab's visible page text, headings,
+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
these sources into one snapshot and writes typed witness documents from that
state.
@@ -18,6 +20,7 @@ state.
```mermaid
flowchart LR
Extension["Chrome extension"] --> Ingest["extension ingest"]
+ Snapshot["active page snapshot"] --> Ingest
Debugger["chrome.debugger sweep"] --> Ingest
Chrome["Chrome CDP endpoint"] --> Observe["observe CDP"]
Config["Loki config"] --> Ingest
@@ -35,6 +38,8 @@ flowchart LR
- CDP reachability is observed state, not assumed capability.
- Extension reachability is observed state, not assumed capability.
- 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.
- `.cc` witnesses are the durable local state surface.
- 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.
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.
+4. Add 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.
+Loki currently advertises no executable tab commands. Page snapshot capture is
+extension-owned and popup-triggered: the operator clicks once, the extension
+captures the active tab, posts the typed observation, and detaches. A future
+daemon-originated command path still needs an explicit command document, audit
+log, origin, target tab id, and denial path before it should act.
diff --git a/extension/popup.css b/extension/popup.css
index d199fa2..89bbf4a 100644
--- a/extension/popup.css
+++ b/extension/popup.css
@@ -56,7 +56,7 @@ dl {
dl div {
display: grid;
- grid-template-columns: 72px 1fr;
+ grid-template-columns: 88px 1fr;
gap: 8px;
}
diff --git a/extension/popup.html b/extension/popup.html
index a60d8dd..88ca3e4 100644
--- a/extension/popup.html
+++ b/extension/popup.html
@@ -30,11 +30,16 @@
Debug
-
+
+
Page
+ -
+
Sync now
+ Snapshot active tab
Debug sweep
diff --git a/extension/popup.js b/extension/popup.js
index e0f7f1b..0dee208 100644
--- a/extension/popup.js
+++ b/extension/popup.js
@@ -3,8 +3,10 @@ const tabsEl = document.querySelector("#tabs");
const lastSyncEl = document.querySelector("#last-sync");
const endpointEl = document.querySelector("#endpoint");
const debugEl = document.querySelector("#debug");
+const pageSnapshotEl = document.querySelector("#page-snapshot");
const errorEl = document.querySelector("#error");
const syncButton = document.querySelector("#sync");
+const snapshotActiveTabButton = document.querySelector("#snapshot-active-tab");
const debugSweepButton = document.querySelector("#debug-sweep");
syncButton.addEventListener("click", async () => {
@@ -21,6 +23,13 @@ debugSweepButton.addEventListener("click", async () => {
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();
async function render() {
@@ -33,6 +42,7 @@ async function render() {
stateEl.className = "state";
tabsEl.textContent = "-";
debugEl.textContent = "-";
+ pageSnapshotEl.textContent = "-";
lastSyncEl.textContent = "-";
errorEl.hidden = true;
return;
@@ -42,6 +52,7 @@ async function render() {
stateEl.className = `state ${status.ok ? "ok" : "bad"}`;
tabsEl.textContent = String(status.tabCount ?? 0);
debugEl.textContent = formatDebugProbe(status.debugProbe);
+ pageSnapshotEl.textContent = formatPageSnapshot(status.pageSnapshot);
lastSyncEl.textContent = status.lastSyncAt ? new Date(status.lastSyncAt).toLocaleTimeString() : "-";
if (status.error) {
@@ -57,3 +68,11 @@ function formatDebugProbe(debugProbe) {
const summary = debugProbe.summary ?? {};
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`;
+}
diff --git a/extension/service-worker.js b/extension/service-worker.js
index aa594f2..5a9fb86 100644
--- a/extension/service-worker.js
+++ b/extension/service-worker.js
@@ -35,6 +35,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
debugSweep().then(sendResponse);
return true;
}
+ if (message?.type === "loki.snapshot-active-tab") {
+ snapshotActiveTab().then(sendResponse);
+ return true;
+ }
if (message?.type === "loki.get-status") {
getStatus().then(sendResponse);
return true;
@@ -63,10 +67,11 @@ async function syncTabs(reason, extra = {}) {
activeTabCount: tabs.filter((tab) => tab.active).length,
},
redaction: {
- pageContent: "not-read",
+ pageContent: extra.pageSnapshot ? "explicit-active-tab-visible-text" : "not-read",
cookies: "not-read",
tabMetadata: "title-url-favicon-window-state",
debugProbe: extra.debugProbe ? "runtime-frame-performance-security-metadata" : "not-run",
+ pageSnapshot: extra.pageSnapshot ? extra.pageSnapshot.redaction : "not-run",
},
...extra,
};
@@ -89,6 +94,7 @@ async function syncTabs(reason, extra = {}) {
lastSyncAt: new Date().toISOString(),
tabCount: tabs.length,
debugProbe: responseBody.debugProbe ?? null,
+ pageSnapshot: responseBody.pageSnapshot ?? null,
activeSource: responseBody.activeSource ?? "unknown",
error: null,
};
@@ -103,6 +109,7 @@ async function syncTabs(reason, extra = {}) {
lastSyncAt: new Date().toISOString(),
tabCount: 0,
debugProbe: null,
+ pageSnapshot: null,
activeSource: "none",
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() {
const startedAt = new Date().toISOString();
const tabs = await chrome.tabs.query({});
diff --git a/src/loki-daemon.mjs b/src/loki-daemon.mjs
index 2557984..1d6108e 100644
--- a/src/loki-daemon.mjs
+++ b/src/loki-daemon.mjs
@@ -152,6 +152,7 @@ function normalizeExtensionObservation(payload, config) {
maxAgeMs: config.intervalMs * 3,
},
debugProbe: normalizeDebugProbe(payload.debugProbe),
+ pageSnapshot: normalizePageSnapshot(payload.pageSnapshot),
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) {
return {
id: String(tab.id ?? ""),
@@ -234,6 +245,7 @@ function buildSnapshot(config, cdpObservation, extensionObservation) {
cdp,
extension,
debugProbe: extensionObservation?.debugProbe ?? null,
+ pageSnapshot: extensionObservation?.pageSnapshot ?? null,
tabs,
summary: {
tabCount: tabs.length,
@@ -241,14 +253,17 @@ function buildSnapshot(config, cdpObservation, extensionObservation) {
activeTabCount: tabs.filter((tab) => tab.active).length,
debugProbeAttachedTabCount: extensionObservation?.debugProbe?.summary?.attachedTabCount ?? 0,
debugProbeAttemptedTabCount: extensionObservation?.debugProbe?.summary?.attemptedTabCount ?? 0,
+ pageSnapshotVisibleTextLength: extensionObservation?.pageSnapshot?.content?.visibleTextLength ?? 0,
+ pageSnapshotFormControlCount: extensionObservation?.pageSnapshot?.forms?.controlCount ?? 0,
},
redaction: {
urlQuery: 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",
source: activeObservation?.source ?? "none",
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",
},
tabs: [],
- summary: {
- tabCount: 0,
- inspectableTabCount: 0,
- activeTabCount: 0,
- },
- redaction: {
- urlQuery: "not-observed",
- titles: "not-observed",
- pageContent: "not-read",
- cookies: "not-read",
- source: "none",
- },
+ pageSnapshot: null,
+ debugProbe: null,
+ summary: {
+ tabCount: 0,
+ inspectableTabCount: 0,
+ activeTabCount: 0,
+ pageSnapshotVisibleTextLength: 0,
+ pageSnapshotFormControlCount: 0,
+ },
+ redaction: {
+ 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",
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",
owner: providerId,
@@ -399,6 +428,17 @@ function buildProviderAdvertisement(snapshot) {
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: [
{
@@ -424,8 +464,8 @@ function buildProviderAdvertisement(snapshot) {
{
kind: "chrome-extension-ingest",
address: snapshot.extension.ingestEndpoint,
- carries: ["chrome-extension-tab-metadata"],
- note: "Preferred drop-in Chrome path; extension observes tab metadata and can run explicit chrome.debugger sweeps.",
+ carries: ["chrome-extension-tab-metadata", "explicit-active-page-snapshot"],
+ 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",
@@ -435,6 +475,7 @@ function buildProviderAdvertisement(snapshot) {
"state/loki.provider_advertisement.cc",
"state/loki.eve_surface.cc",
"state/loki.chrome_debug_probe.cc",
+ "state/loki.chrome_page_snapshot.cc",
],
},
],
@@ -496,6 +537,14 @@ function buildEveSurface(snapshot) {
: "not run",
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",
kind: "inspector.kv",
@@ -614,6 +663,9 @@ async function publishSnapshot(config, snapshot) {
snapshot.debugProbe
? writeTypedDocument(join(config.stateDir, "loki.chrome_debug_probe.cc"), snapshot.debugProbe)
: Promise.resolve(),
+ snapshot.pageSnapshot
+ ? writeTypedDocument(join(config.stateDir, "loki.chrome_page_snapshot.cc"), snapshot.pageSnapshot)
+ : Promise.resolve(),
]);
return { advertisement, surface };
@@ -674,6 +726,7 @@ function startExtensionIngestServer(config) {
activeSource: snapshot.activeSource,
tabs: snapshot.summary.tabCount,
debugProbe: snapshot.debugProbe ? snapshot.debugProbe : null,
+ pageSnapshot: snapshot.pageSnapshot ? snapshot.pageSnapshot : null,
observedAt: snapshot.observedAt,
});
return;
diff --git a/test/extension-ingest-smoke.mjs b/test/extension-ingest-smoke.mjs
index 77fe212..528d516 100644
--- a/test/extension-ingest-smoke.mjs
+++ b/test/extension-ingest-smoke.mjs
@@ -74,6 +74,51 @@ try {
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: [
{
id: 100,
@@ -99,6 +144,7 @@ try {
const snapshotText = await readFile(join(stateDir, "loki.chrome_snapshot.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"')) {
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"')) {
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");
} finally {