From e0fd88e90192b1a07b8fe2f5cfb84309c12c43ea Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 24 Oct 2026 19:29:16 +0900 Subject: chore: remove private machine and network details from frontend --- backend-handoff.md | 32 ++++++ notes/assumptions-log.md | 2 +- scripts/network-policy.test.ts | 37 +++---- ...probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts | 105 ------------------- scripts/probe-tab.ts | 113 +++++++++++++++++++++ scripts/vite-network-guard.test.ts | 18 +++- src/app/resolve-http-url.test.ts | 8 +- src/app/resolve-ws-url.test.ts | 8 +- src/features/chat/ui/ChatView.test.ts | 4 +- 9 files changed, 190 insertions(+), 137 deletions(-) delete mode 100644 scripts/probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts create mode 100644 scripts/probe-tab.ts diff --git a/backend-handoff.md b/backend-handoff.md index d1d0756..98cee83 100644 --- a/backend-handoff.md +++ b/backend-handoff.md @@ -18,6 +18,38 @@ > ?archived=`) was made in the same branch and the FE consumes it via the `file:` deps (dists > rebuilt). Recorded below. +## Milestone: Current frontend privacy cleanup (`feature/privacy-cleanup`, 2026-10-24) + +**Slice status:** COMPLETE; frontend-only cleanup verified. + +- Personal machine, tailnet DNS, IPv4 and IPv6 literals in network-policy and URL + resolver fixtures are replaced with synthetic names and documentation-only addresses + (`dev-node.tailnet.example`, `192.0.2.10`, `2001:db8::10`). The Vite guard fixture + uses the same reserved examples. Host/Origin, mapped-address, interface-selection, + fail-closed and env-override coverage is retained; runtime network policy is unchanged. +- Ownership-repair documentation resolves the current user's name/group instead of + naming a personal local account. Generic home-directory fixtures, intentional public + repository-owner references, human identity and legitimate product/model references remain. +- The conversation-specific diagnostic is now `scripts/probe-tab.ts `; + it requires an explicit target before any request and reports the actual chunk count. + Regression comments describe the duplicate-emission pattern without a real conversation id. +- Audited tracked current files, including hidden config, contract mirrors and the lockfile. + No application defaults, stored user data, endpoint contracts or dependency pins changed. + Existing `file:../backend/packages/*` dependencies and additive mirrors remain unchanged + (`wire@0.12.0`, `transport-contract@0.26.0`, `ui-contract@0.2.0`). No backend ask. +- History/backups and removal of AI-attribution text from historical commit messages + belong to the coordinating task; this slice does not rewrite refs, merge or push. + Legitimate AI functionality/product references are not attribution and are retained. + +**Verification:** `bun install` succeeds without lockfile changes; `bun run typecheck` +0 errors/0 warnings; `bun run test` 2,587 passed/2 opt-in skipped twice; +`bun run check` clean (309 files); `bun run build` succeeds with existing Svelte +capture and chunk-size advisories. Diagnostic usage fails safely without a target; +an ephemeral loopback HTTP fixture verifies its two requests and dynamic chunk count. +Final current-file scan has no known private literals. Report: `reports/privacy-cleanup.md` +(gitignored). +No browser-facing runtime or wire changes; no shared backend process started or modified. + ## Milestone: Close-tab cascade subagents (`feature/close-tab-cascade-subagents`, 2026-10-24) **Slice status: COMPLETE across the isolated frontend + backend feature branches.** diff --git a/notes/assumptions-log.md b/notes/assumptions-log.md index 89b057a..3291e28 100644 --- a/notes/assumptions-log.md +++ b/notes/assumptions-log.md @@ -45,7 +45,7 @@ 2. **The `dist/` directory is owned by root** (likely from a previous root-run build). `bun run build` fails with `EACCES` when Vite tries to empty `dist/assets`. This is an environment issue, not a code issue — I verified the build succeeds with `--outDir dist-tmp` (a fresh directory I own, then - removed). The user may want to `sudo chown -R PII_REDACTED_USER_01:PII_REDACTED_USER_01 dist/` or remove it. + removed). The user may want to `sudo chown -R "$(id -un):$(id -gn)" dist/` or remove it. 3. **Parser now rejects broadcasts missing `workspaceId`.** The WS parser (`logic.ts`) returns `null` for `conversation.open` / `conversation.statusChanged` messages that lack a string `workspaceId`. diff --git a/scripts/network-policy.test.ts b/scripts/network-policy.test.ts index af7d870..4c3ed12 100644 --- a/scripts/network-policy.test.ts +++ b/scripts/network-policy.test.ts @@ -9,9 +9,10 @@ import { tailscaleSelf, } from "./network-policy"; +// Synthetic status: reserved .example DNS and documentation-only IPv4/IPv6 addresses. const tailscaleStatusJson = `{"Self":{"HostName":"laptop", - "DNSName":"PII_REDACTED_TAILSCALE_HOST_01.", - "TailscaleIPs":["PII_REDACTED_DEV_IPV4_01","PII_REDACTED_DEV_IPV6_01"]}}`; + "DNSName":"dev-node.tailnet.example.", + "TailscaleIPs":["192.0.2.10","2001:db8::10"]}}`; const allowedHosts = buildAllowedHosts({ tailscaleStatusJson, env: {} }); const allowedAddresses = new Set(["127.0.0.1", "::1", ...tailscaleSelf(tailscaleStatusJson).ips]); const policy = { allowedHosts, allowedAddresses }; @@ -23,10 +24,10 @@ describe("backend-compatible hosts", () => { ["LOCALHOST.:34204", "localhost"], ["127.0.0.1:34204", "127.0.0.1"], ["[0:0:0:0:0:0:0:1]:34204", "::1"], - ["PII_REDACTED_DEVICE_01", "PII_REDACTED_DEVICE_01"], - ["PII_REDACTED_TAILSCALE_HOST_01.:34204", "PII_REDACTED_TAILSCALE_HOST_01"], - ["PII_REDACTED_DEV_IPV4_01:34204", "PII_REDACTED_DEV_IPV4_01"], - ["[PII_REDACTED_DEV_IPV6_01]:34204", "PII_REDACTED_DEV_IPV6_01"], + ["DEV-NODE", "dev-node"], + ["DEV-NODE.tailnet.example.:34204", "dev-node.tailnet.example"], + ["192.0.2.10:34204", "192.0.2.10"], + ["[2001:db8::10]:34204", "2001:db8::10"], ])("normalizes and allows %s", (raw, expected) => { expect(normalizeHost(raw)).toBe(expected); expect(checkHost(raw, allowedHosts)).toBe("ok"); @@ -34,7 +35,7 @@ describe("backend-compatible hosts", () => { it.each( ( - "evil.com|PII_REDACTED_DEVICE_01.evil.com|PII_REDACTED_DEV_IPV4_01.nip.io|laptop|localhost.evil.com|" + + "evil.com|dev-node.evil.com|192.0.2.10.nip.io|laptop|localhost.evil.com|" + "sub.localhost|127.0.0.2|100.1.2.3|172.17.0.1|localhost@evil.com|localhost/path|" + "localhost:bad|localhost:65536|[::1]suffix|localhost\n|localhost..|%6cocalhost|" ).split("|"), @@ -48,10 +49,10 @@ describe("backend-compatible hosts", () => { ...(json !== undefined ? { tailscaleStatusJson: json } : {}), env: { DISPATCH_ALLOWED_HOSTS: " EXTRA.example.:8080, https://evil.com", - DISPATCH_TAILSCALE_NAME: "PII_REDACTED_DEVICE_01", + DISPATCH_TAILSCALE_NAME: "DEV-NODE", }, }; - const expected = "localhost,127.0.0.1,::1,extra.example,PII_REDACTED_DEVICE_01".split(","); + const expected = "localhost,127.0.0.1,::1,extra.example,dev-node".split(","); expect([...buildAllowedHosts(input)]).toEqual(expected); expect(tailscaleSelf(json)).toEqual({ ips: [] }); }); @@ -61,20 +62,20 @@ describe("backend-compatible hosts", () => { tailscaleStatusJson, env: { DISPATCH_ALLOWED_HOSTS: "extra.example" }, }); - expect(hosts.has("PII_REDACTED_TAILSCALE_HOST_01")).toBe(true); + expect(hosts.has("dev-node.tailnet.example")).toBe(true); expect(hosts.has("extra.example")).toBe(true); expect(hosts.has("laptop")).toBe(false); expect(tailscaleSelf(tailscaleStatusJson).hostName).toBe("laptop"); - expect( - tailscaleSelf('{"Self":{"TailscaleIPs":[null,"evil.com","PII_REDACTED_DEV_IPV4_01"]}}').ips, - ).toEqual(["PII_REDACTED_DEV_IPV4_01"]); + expect(tailscaleSelf('{"Self":{"TailscaleIPs":[null,"evil.com","192.0.2.10"]}}').ips).toEqual([ + "192.0.2.10", + ]); }); it.each([ ["LOCALHOST.:34204", "localhost:34204"], ["[0:0:0:0:0:0:0:1]:34204", "[::1]:34204"], ["[::1]", "[::1]"], - ["PII_REDACTED_DEVICE_01.", "PII_REDACTED_DEVICE_01"], + ["DEV-NODE.", "dev-node"], ])("preserves valid authorities for Vite: %s", (raw, expected) => { expect(normalizeHostHeader(raw)).toBe(expected); }); @@ -84,7 +85,7 @@ describe("destination interfaces (not remote peer addresses)", () => { it.each( ( "127.0.0.1|::1|0:0:0:0:0:0:0:1|::ffff:127.0.0.1|::ffff:7f00:1|" + - "PII_REDACTED_DEV_IPV4_01|::ffff:PII_REDACTED_DEV_IPV4_01|PII_REDACTED_DEV_IPV6_01" + "192.0.2.10|::ffff:192.0.2.10|2001:db8::10" ).split("|"), )("allows local address %s", (address) => { expect(isAllowedLocalAddress(address, allowedAddresses)).toBe(true); @@ -115,10 +116,10 @@ describe("destination interfaces (not remote peer addresses)", () => { describe("shared HTTP / HMR request decision", () => { it.each([ undefined, - "http://PII_REDACTED_DEVICE_01.:34204", - "https://[PII_REDACTED_DEV_IPV6_01]:443", + "http://DEV-NODE.:34204", + "https://[2001:db8::10]:443", ])("allows trusted or absent Origin %j", (origin) => { - expect(request("::ffff:PII_REDACTED_DEV_IPV4_01", "PII_REDACTED_DEVICE_01", origin)).toBe("ok"); + expect(request("::ffff:192.0.2.10", "dev-node", origin)).toBe("ok"); }); const badOrigins = "null|file://localhost|https://evil.com|http://localhost/|http://user@localhost"; diff --git a/scripts/probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts b/scripts/probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts deleted file mode 100644 index 7e2fad7..0000000 --- a/scripts/probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Focused probe: load conversation PII_REDACTED_CONVERSATION_SHORT_01 through the FE's REAL pure modules - * and see whether the tab's render path would throw. Not part of the test suite. - * - * bun scripts/probe-tab-PII_REDACTED_CONVERSATION_SHORT_01.ts - */ -import "fake-indexeddb/auto"; -import type { - ConversationHistoryResponse, - ConversationMetricsResponse, -} from "@dispatch/transport-contract"; -import { - applyHistory, - groupRenderedChunks, - initialState, - selectChunks, - selectMessages, -} from "../src/core/chunks/index.ts"; -import { - applyDurableMetrics, - initialMetricsState, - selectOrderedTurnMetrics, -} from "../src/core/metrics/index.ts"; - -const HTTP_BASE = process.env.PROBE_HTTP ?? "http://localhost:24991"; -const CONV = "PII_REDACTED_CONVERSATION_01"; - -async function historySync(id: string, sinceSeq: number): Promise { - const url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}?sinceSeq=${sinceSeq}`; - const res = await fetch(url, { headers: { Origin: "http://localhost:24204" } }); - if (!res.ok) throw new Error(`history fetch ${res.status} for ${url}`); - return (await res.json()) as ConversationHistoryResponse; -} - -async function metricsSync(id: string): Promise { - const res = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(id)}/metrics`, { - headers: { Origin: "http://localhost:24204" }, - }); - if (!res.ok) throw new Error(`metrics fetch ${res.status}`); - return (await res.json()) as ConversationMetricsResponse; -} - -let failures = 0; -const record = (name: string, ok: boolean, detail?: string) => { - if (!ok) failures += 1; - console.log(` ${ok ? "OK" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`); -}; - -console.log(`Probing conversation ${CONV} via ${HTTP_BASE}`); - -// 1. applyHistory (the tab's cold-load path) -try { - const hist = await historySync(CONV, 0); - const chunks = hist.chunks ?? []; - console.log(` history: ${chunks.length} chunks, latestSeq=${hist.latestSeq}`); - let state = initialState(); - try { - state = applyHistory(state, chunks as never); - record("applyHistory(145 chunks)", true, `state=${JSON.stringify(state).length} bytes`); - } catch (e) { - record("applyHistory", false, String(e)); - } - - // 2. selectMessages / selectChunks — what the transcript renders - try { - const messages = selectMessages(state); - record("selectMessages", true, `${messages.length} messages`); - } catch (e) { - record("selectMessages", false, String(e)); - } - - // 3. groupRenderedChunks — the render grouping - try { - const grouped = groupRenderedChunks(selectChunks(state)); - record("groupRenderedChunks", true, `${grouped.length} groups`); - } catch (e) { - record("groupRenderedChunks", false, String(e)); - } -} catch (e) { - record("history load", false, String(e)); -} - -// 4. metrics path -try { - const m = await metricsSync(CONV); - let ms = initialMetricsState(); - const turns = (m as { turns?: unknown[] }).turns ?? []; - try { - ms = applyDurableMetrics(ms, turns as never); - } catch (e) { - failures += 1; - console.log(` FAIL applyDurableMetrics — ${String(e)}`); - } - const ordered = selectOrderedTurnMetrics(ms); - record( - "metrics fold+apply", - true, - `${ordered.length} ordered turn metrics (${turns.length} turns)`, - ); -} catch (e) { - record("metrics load", false, String(e)); -} - -console.log(failures === 0 ? "\nALL OK — tab render path is clean." : `\n${failures} FAILURE(S).`); -process.exit(failures === 0 ? 0 : 1); diff --git a/scripts/probe-tab.ts b/scripts/probe-tab.ts new file mode 100644 index 0000000..16ad423 --- /dev/null +++ b/scripts/probe-tab.ts @@ -0,0 +1,113 @@ +/** + * Focused probe: load a selected conversation through the FE's REAL pure modules + * and see whether the tab's render path would throw. Not part of the test suite. + * + * bun scripts/probe-tab.ts + */ +import "fake-indexeddb/auto"; +import type { + ConversationHistoryResponse, + ConversationMetricsResponse, +} from "@dispatch/transport-contract"; +import { + applyHistory, + groupRenderedChunks, + initialState, + selectChunks, + selectMessages, +} from "../src/core/chunks/index.ts"; +import { + applyDurableMetrics, + initialMetricsState, + selectOrderedTurnMetrics, +} from "../src/core/metrics/index.ts"; + +const HTTP_BASE = process.env.PROBE_HTTP ?? "http://localhost:24991"; +const CONV = process.argv[2]; +if (!CONV) { + console.error("Usage: bun scripts/probe-tab.ts "); + process.exit(1); +} + +async function historySync(id: string, sinceSeq: number): Promise { + const url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}?sinceSeq=${sinceSeq}`; + const res = await fetch(url, { headers: { Origin: "http://localhost:24204" } }); + if (!res.ok) throw new Error(`history fetch ${res.status} for ${url}`); + return (await res.json()) as ConversationHistoryResponse; +} + +async function metricsSync(id: string): Promise { + const res = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(id)}/metrics`, { + headers: { Origin: "http://localhost:24204" }, + }); + if (!res.ok) throw new Error(`metrics fetch ${res.status}`); + return (await res.json()) as ConversationMetricsResponse; +} + +let failures = 0; +const record = (name: string, ok: boolean, detail?: string) => { + if (!ok) failures += 1; + console.log(` ${ok ? "OK" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`); +}; + +console.log(`Probing conversation ${CONV} via ${HTTP_BASE}`); + +// 1. applyHistory (the tab's cold-load path) +try { + const hist = await historySync(CONV, 0); + const chunks = hist.chunks ?? []; + console.log(` history: ${chunks.length} chunks, latestSeq=${hist.latestSeq}`); + let state = initialState(); + try { + state = applyHistory(state, chunks as never); + record( + `applyHistory(${chunks.length} chunks)`, + true, + `state=${JSON.stringify(state).length} bytes`, + ); + } catch (e) { + record("applyHistory", false, String(e)); + } + + // 2. selectMessages / selectChunks — what the transcript renders + try { + const messages = selectMessages(state); + record("selectMessages", true, `${messages.length} messages`); + } catch (e) { + record("selectMessages", false, String(e)); + } + + // 3. groupRenderedChunks — the render grouping + try { + const grouped = groupRenderedChunks(selectChunks(state)); + record("groupRenderedChunks", true, `${grouped.length} groups`); + } catch (e) { + record("groupRenderedChunks", false, String(e)); + } +} catch (e) { + record("history load", false, String(e)); +} + +// 4. metrics path +try { + const m = await metricsSync(CONV); + let ms = initialMetricsState(); + const turns = (m as { turns?: unknown[] }).turns ?? []; + try { + ms = applyDurableMetrics(ms, turns as never); + } catch (e) { + failures += 1; + console.log(` FAIL applyDurableMetrics — ${String(e)}`); + } + const ordered = selectOrderedTurnMetrics(ms); + record( + "metrics fold+apply", + true, + `${ordered.length} ordered turn metrics (${turns.length} turns)`, + ); +} catch (e) { + record("metrics load", false, String(e)); +} + +console.log(failures === 0 ? "\nALL OK — tab render path is clean." : `\n${failures} FAILURE(S).`); +process.exit(failures === 0 ? 0 : 1); diff --git a/scripts/vite-network-guard.test.ts b/scripts/vite-network-guard.test.ts index 651b635..8bef397 100644 --- a/scripts/vite-network-guard.test.ts +++ b/scripts/vite-network-guard.test.ts @@ -5,8 +5,14 @@ import { describe, expect, it, vi } from "vitest"; import { applyViteNetworkPolicy, devNetworkGuard } from "./vite-network-guard"; const policy = { - allowedHosts: new Set(["localhost", "127.0.0.1", "::1", "node.ts.net", "100.64.0.10"]), - allowedAddresses: new Set(["127.0.0.1", "::1", "100.64.0.10"]), + allowedHosts: new Set([ + "localhost", + "127.0.0.1", + "::1", + "dev-node.tailnet.example", + "192.0.2.10", + ]), + allowedAddresses: new Set(["127.0.0.1", "::1", "192.0.2.10"]), }; function install(mode: "dev" | "preview") { @@ -39,7 +45,13 @@ const cases = [ ["loopback", "127.0.0.1", "localhost", undefined, 200], ["IPv6 loopback", "::1", "[::1]:34204", undefined, 200], ["mapped address / canonical Host", "::ffff:127.0.0.1", "LOCALHOST.:34204", undefined, 200], - ["tailnet", "100.64.0.10", "node.ts.net", "https://node.ts.net:34204", 200], + [ + "tailnet", + "192.0.2.10", + "dev-node.tailnet.example", + "https://dev-node.tailnet.example:34204", + 200, + ], ["unselected interface with trusted Host", "172.17.0.1", "localhost", undefined, 403], ["missing interface", undefined, "localhost", undefined, 403], ["arbitrary IP Host", "127.0.0.1", "127.0.0.2", undefined, 421], diff --git a/src/app/resolve-http-url.test.ts b/src/app/resolve-http-url.test.ts index e544255..c556812 100644 --- a/src/app/resolve-http-url.test.ts +++ b/src/app/resolve-http-url.test.ts @@ -19,13 +19,13 @@ describe("resolveHttpUrl", () => { }); it("derives http://:24203 from http location", () => { - const result = resolveHttpUrl({}, { protocol: "http:", hostname: "PII_REDACTED_DEV_IPV4_01" }); - expect(result).toBe("http://PII_REDACTED_DEV_IPV4_01:24203"); + const result = resolveHttpUrl({}, { protocol: "http:", hostname: "192.0.2.10" }); + expect(result).toBe("http://192.0.2.10:24203"); }); it("derives https://:24203 from https location", () => { - const result = resolveHttpUrl({}, { protocol: "https:", hostname: "PII_REDACTED_DEVICE_01" }); - expect(result).toBe("https://PII_REDACTED_DEVICE_01:24203"); + const result = resolveHttpUrl({}, { protocol: "https:", hostname: "dev-node" }); + expect(result).toBe("https://dev-node:24203"); }); it("uses VITE_HTTP_PORT when set", () => { diff --git a/src/app/resolve-ws-url.test.ts b/src/app/resolve-ws-url.test.ts index ab3a379..be1ab12 100644 --- a/src/app/resolve-ws-url.test.ts +++ b/src/app/resolve-ws-url.test.ts @@ -19,13 +19,13 @@ describe("resolveWsUrl", () => { }); it("derives ws://:24205 from http location", () => { - const result = resolveWsUrl({}, { protocol: "http:", hostname: "PII_REDACTED_DEV_IPV4_01" }); - expect(result).toBe("ws://PII_REDACTED_DEV_IPV4_01:24205"); + const result = resolveWsUrl({}, { protocol: "http:", hostname: "192.0.2.10" }); + expect(result).toBe("ws://192.0.2.10:24205"); }); it("derives wss://:24205 from https location", () => { - const result = resolveWsUrl({}, { protocol: "https:", hostname: "PII_REDACTED_DEVICE_01" }); - expect(result).toBe("wss://PII_REDACTED_DEVICE_01:24205"); + const result = resolveWsUrl({}, { protocol: "https:", hostname: "dev-node" }); + expect(result).toBe("wss://dev-node:24205"); }); it("uses VITE_WS_PORT when set", () => { diff --git a/src/features/chat/ui/ChatView.test.ts b/src/features/chat/ui/ChatView.test.ts index a6bd8ac..157ccac 100644 --- a/src/features/chat/ui/ChatView.test.ts +++ b/src/features/chat/ui/ChatView.test.ts @@ -14,7 +14,7 @@ async function openAllCollapses(container: HTMLElement): Promise { } // Defensive rendering: a conversation transcript can contain duplicate -// `toolCallId`s (provider double-emission, persisted — e.g. PII_REDACTED_CONVERSATION_SHORT_01). The +// `toolCallId`s (persisted provider double-emission). The // grouped render path MUST dedupe them so the keyed {#each} never throws // Svelte `each_key_duplicate` (which aborts the reactive flush and makes the // tab appear dead). See fix-toolcall-duplicate-keys plan Part 1. @@ -45,7 +45,7 @@ describe("ChatView — duplicate toolCallIds", () => { }, provisional: false, }, - // The PII_REDACTED_CONVERSATION_SHORT_01 pattern: the same ids re-emitted immediately after. + // The duplicate-emission pattern: the same ids re-emitted immediately after. { seq: 3, role: "assistant", -- cgit v1.2.3