docs(01-01): cite D-05 in must_haves per coverage gate .planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md

This commit is contained in:
2026-05-15 17:07:49 +02:00
parent 576280f6aa
commit 0811c6a292
5 changed files with 41 additions and 29 deletions

View File

@@ -25,7 +25,7 @@ must_haves:
- "ROADMAP.md Phase 1 Success Criterion #2 no longer references tab re-attach" - "ROADMAP.md Phase 1 Success Criterion #2 no longer references tab re-attach"
- "intel/decisions.md DEC-003 and DEC-010 carry an Amendment block" - "intel/decisions.md DEC-003 and DEC-010 carry an Amendment block"
- "intel/constraints.md CON-tab-capture-binding and CON-service-worker-keepalive are RETIRED with a CON-display-capture-binding added" - "intel/constraints.md CON-tab-capture-binding and CON-service-worker-keepalive are RETIRED with a CON-display-capture-binding added"
- "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry" - "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry (D-05, D-A6)"
- "Every code-touching plan (02-07) sees a consistent doc baseline before it runs" - "Every code-touching plan (02-07) sees a consistent doc baseline before it runs"
artifacts: artifacts:
- path: ".planning/PROJECT.md" - path: ".planning/PROJECT.md"

View File

@@ -18,12 +18,14 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`src/offscreen/recorder.ts` exists and exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS" - "`src/offscreen/recorder.ts` exists at the canonical source path as a real TypeScript module — strict type-check, source maps, IDE support (D-06)"
- "`src/offscreen/recorder.ts` exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS"
- "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green" - "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green"
- "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms" - "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms"
- "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)" - "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)"
- "`MediaRecorder.start(2000)` is called on session start (timeslice = 2000 ms per SPEC §4.1)" - "Capture is `navigator.mediaDevices.getDisplayMedia()` invoked from inside the offscreen document (D-01); SW-side `chrome.tabCapture.getMediaStreamId` is removed in Plan 05"
- "On `MediaStreamTrack.onended`, the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW" - "Single continuous `MediaRecorder` runs for the whole session with `mediaRecorder.start(2000)` so chunks land on cluster boundaries per SPEC §4.1 (D-09)"
- "One-time source picker fires on session start (operator picks screen/window once); on `MediaStreamTrack.onended` the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW so the popup can re-prompt next interaction (D-03)"
- "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan" - "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan"
- "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`" - "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`"
- "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix" - "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix"
@@ -72,8 +74,10 @@ handshake — that is Plan 04. To keep this plan inside its context budget, the
port-side code in `recorder.ts` is left as a small import-time placeholder port-side code in `recorder.ts` is left as a small import-time placeholder
that Plan 04 fills in. The Plan-02 port + handshake tests therefore remain that Plan 04 fills in. The Plan-02 port + handshake tests therefore remain
RED until Plan 04 lands, which is the intended choreography per the RED until Plan 04 lands, which is the intended choreography per the
wave-1 dependency graph (Plans 03 and 04 run in parallel; Plan 02 wrote both sequential wave structure (Plan 03 lands in wave 2; Plan 04 follows in wave
sets of RED tests in advance). 3 and refactors the bootstrap section of the same file). Plan 02 wrote both
sets of RED tests in advance so Plan 03 and Plan 04 each have a discrete
RED→GREEN cycle to flip.
Purpose: REQ-video-ring-buffer's load-bearing logic lives here. The ring Purpose: REQ-video-ring-buffer's load-bearing logic lives here. The ring
buffer is a pure function — exactly the TDD sweet spot. The remaining buffer is a pure function — exactly the TDD sweet spot. The remaining
@@ -155,7 +159,7 @@ export interface VideoChunk {
| Threat ID | Category | Component | Disposition | Mitigation Plan | | Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------| |-----------|----------|-----------|-------------|-----------------|
| T-1-01 | Tampering — codec downgrade | `MediaRecorder` constructor | mitigate | `assertCodecSupported()` calls `MediaRecorder.isTypeSupported('video/webm;codecs=vp9')` BEFORE constructing the recorder; if it returns false, throws an Error and emits `RECORDING_ERROR` to SW. No vp8 / h264 / default fallback chain. The strict mode covers `MediaRecorder` itself being absent (the codec-check test mocks both cases). Grep gate: `grep -v '^#' src/offscreen/recorder.ts \| grep -cE 'codecs=(vp8\|h264)'` returns 0 (no fallback codec strings in the module). | | T-1-01 | Tampering — codec downgrade | `MediaRecorder` constructor | mitigate | `assertCodecSupported()` calls `MediaRecorder.isTypeSupported('video/webm;codecs=vp9')` BEFORE constructing the recorder; if it returns false, throws an Error and emits `RECORDING_ERROR` to SW. No vp8 / h264 / default fallback chain. The strict mode covers `MediaRecorder` itself being absent (the codec-check test mocks both cases). Grep gate: `grep -v '^#' src/offscreen/recorder.ts \| grep -cE 'codecs=(vp8\|h264)'` returns 0 (no fallback codec strings in the module). |
| T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". | | T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04 — the operator opted into the "Sharing your screen" indicator as the cost of the broader capture coverage. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". |
| T-1-NEW-03-01 | DoS — unbounded buffer growth on a stuck timestamp | `addChunk` + `trimAged` | mitigate | The trim function uses arrival timestamp; if a clock anomaly produces a negative or stuck `now`, the buffer is bounded above by SPEC §10 #9 (50 MB RAM ceiling) anyway. Defensive belt: the recorder also exposes `getBuffer().length` to SW so a healthchecker can observe growth. No active rate-limit needed for Phase 1. | | T-1-NEW-03-01 | DoS — unbounded buffer growth on a stuck timestamp | `addChunk` + `trimAged` | mitigate | The trim function uses arrival timestamp; if a clock anomaly produces a negative or stuck `now`, the buffer is bounded above by SPEC §10 #9 (50 MB RAM ceiling) anyway. Defensive belt: the recorder also exposes `getBuffer().length` to SW so a healthchecker can observe growth. No active rate-limit needed for Phase 1. |
</threat_model> </threat_model>

View File

@@ -14,9 +14,9 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage`" - "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage` — this is the long-lived port keepalive that replaces `chrome.alarms` for SW idle-timer resets (D-17)"
- "Both the port-connect and the OFFSCREEN_READY send happen AFTER `chrome.runtime.onMessage.addListener` registration (Pattern 4 ordering)" - "Both the port-connect and the OFFSCREEN_READY send happen AFTER `chrome.runtime.onMessage.addListener` registration (Pattern 4 ordering)"
- "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation)" - "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation; preserves the D-17 contract across the ~5 min port-lifetime cap)"
- "The port emits a `{ type: 'PING' }` postMessage on a ≤ 25 s interval (RESEARCH.md Pattern 5)" - "The port emits a `{ type: 'PING' }` postMessage on a ≤ 25 s interval (RESEARCH.md Pattern 5)"
- "Pre-emptive reconnect runs every 290 s (belt-and-braces against the ~5 min port-lifetime cap)" - "Pre-emptive reconnect runs every 290 s (belt-and-braces against the ~5 min port-lifetime cap)"
- "The port handles incoming `REQUEST_BUFFER` messages by responding with `{ type: 'BUFFER', chunks: <getBuffer()> }`" - "The port handles incoming `REQUEST_BUFFER` messages by responding with `{ type: 'BUFFER', chunks: <getBuffer()> }`"
@@ -51,10 +51,12 @@ a contract that survives SW unloads, port 5-min cap, and offscreen
bootstrap races. bootstrap races.
Output: Updated `src/offscreen/recorder.ts` — only this single file is Output: Updated `src/offscreen/recorder.ts` — only this single file is
touched. Plan 04 is an isolated unit-of-work; Plan 05 picks up the touched. Plan 04 is an isolated unit-of-work; Plan 05 (the SW-side
SW-side `onConnect` handler in parallel (Plan 04 runs alongside Plan 05 in `onConnect` counterparty) follows in wave 4 — Plan 04 lands first in wave 3
Wave 1 with NO file overlap — Plan 04 owns offscreen-side, Plan 05 owns so the SW-side host has a well-defined offscreen-side contract to bind
SW-side). against. Plan 04 owns the offscreen-side; Plan 05 owns the SW-side; the two
files do not overlap (Plan 04 touches only `src/offscreen/recorder.ts`,
Plan 05 touches only `src/background/index.ts`).
</objective> </objective>
<execution_context> <execution_context>

View File

@@ -14,13 +14,14 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference (buffer ownership moves to offscreen per D-16/D-19)" - "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference buffer ownership moves to offscreen per D-16/D-19, and the alarms-driven keepalive is DELETED per D-18 (it never actually reset the SW idle timer; the long-lived port does)"
- "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment)" - "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment); video acquisition is now `getDisplayMedia` invoked from the offscreen module"
- "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)" - "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)"
- "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation)" - "`src/background/index.ts` no longer contains any `chrome.tabs.onActivated` handler tied to the recording lifecycle (D-14: tab-switch re-attach is non-applicable under `getDisplayMedia`; D-15: operator tab-switching no longer interrupts recording, the buffer keeps filling regardless of active tab)"
- "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation; this is the SW-side counterparty of the long-lived port keepalive per D-17)"
- "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)" - "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)"
- "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array" - "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array"
- "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`)" - "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`) per D-02"
- "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)" - "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)"
- "`npx tsc --noEmit` exits 0" - "`npx tsc --noEmit` exits 0"
artifacts: artifacts:
@@ -49,9 +50,11 @@ ring-buffer state and helpers (now owned by offscreen per D-16), DELETE the
chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19), chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19),
DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment), DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment),
DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their
message types were removed in Plan 03), and WIRE the SW-side `onConnect` message types were removed in Plan 03), DELETE any `chrome.tabs.onActivated`
handler against the `'video-keepalive'` port that Plan 04 opens from the re-attach plumbing (D-14: not applicable under the new capture API; D-15:
offscreen. operator tab switches no longer interrupt the recording), and WIRE the
SW-side `onConnect` handler against the `'video-keepalive'` port that
Plan 04 opens from the offscreen.
Purpose: REQ-video-ring-buffer's data flow on export is `popup → Purpose: REQ-video-ring-buffer's data flow on export is `popup →
SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via
@@ -200,10 +203,12 @@ If `chrome.offscreen.Reason.DISPLAY_MEDIA` is NOT in the current `@types/chrome`
(13) Delete `openIndexedDB` function — currently lines 507-520. (13) Delete `openIndexedDB` function — currently lines 507-520.
(14) Verify no `chrome.tabs.onActivated` listener exists in the file (D-14 / D-15: tab-switch handling is non-applicable under the new capture API). Run `grep -n "chrome.tabs.onActivated" src/background/index.ts`. If the grep returns any hits, DELETE those lines (the entire listener callback block). If the grep returns nothing, log "D-14/D-15 satisfied: no tab-switch handler found in SW" in the task summary.
After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit. After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit.
</action> </action>
<verify> <verify>
<automated>npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms" src/background/index.ts) -eq 0 ]</automated> <automated>npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms|chrome\.tabs\.onActivated" src/background/index.ts) -eq 0 ]</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `npx tsc --noEmit` exits 0 - `npx tsc --noEmit` exits 0
@@ -215,6 +220,7 @@ After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChun
- `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0
- `grep -v '^#' src/background/index.ts | grep -c "chrome.tabs.onActivated"` returns 0 (D-14/D-15 mitigation)
- `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1 - `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1
- `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule) - `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule)
- File line count reduced from 536 to roughly 380-400 lines (allow ±40) - File line count reduced from 536 to roughly 380-400 lines (allow ±40)
@@ -435,7 +441,7 @@ Commit cadence: TWO commits.
</verification> </verification>
<success_criteria> <success_criteria>
- `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls - `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls, no `chrome.tabs.onActivated` re-attach plumbing
- SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty) - SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty)
- SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise - SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise
- T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender) - T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender)

View File

@@ -17,11 +17,11 @@ requirements_addressed:
must_haves: must_haves:
truths: truths:
- "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin (the 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE)" - "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin — the entire 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE per D-08 (orphan dead-code deletion plus inline-plugin deletion in lockstep)"
- "`vite.config.ts` declares the offscreen entry via `rollupOptions.input` per RESEARCH.md Example B" - "`vite.config.ts` declares the offscreen entry via `rollupOptions.input` per RESEARCH.md Example B; crxjs picks up the new TS entry through the HTML reference and the runtime path remains `offscreen/index.html` resolvable via `chrome.runtime.getURL(...)` per D-07"
- "Top-level `offscreen/index.ts` is DELETED (dead code per audit P2 #18)" - "Top-level `offscreen/index.ts` is DELETED (dead code per audit P2 #18; D-08 explicit DELETE target)"
- "Top-level `offscreen/index.html` is DELETED (replaced by the crxjs-managed `src/offscreen/index.html` from Plan 03)" - "Top-level `offscreen/index.html` is DELETED (REPLACED by the crxjs-managed `src/offscreen/index.html` from Plan 03, per D-07 — the runtime URL semantics survive the source-tree move)"
- "`npm run build` exits 0; `dist/` contains a bundled offscreen HTML at the path the SW's `chrome.runtime.getURL` argument expects" - "`npm run build` exits 0; `dist/` contains a bundled offscreen HTML at the path the SW's `chrome.runtime.getURL` argument expects (D-07 runtime-path contract)"
- "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`" - "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`"
artifacts: artifacts:
- path: "vite.config.ts" - path: "vite.config.ts"
@@ -107,8 +107,8 @@ const url = chrome.runtime.getURL('offscreen/index.html');
| Threat ID | Category | Component | Disposition | Mitigation Plan | | Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------| |-----------|----------|-----------|-------------|-----------------|
| T-1-NEW-06-01 | Tampering — string-injected code via inline plugin | `vite.config.ts:13-216` `copy-offscreen` plugin's `this.emitFile({ source: \`<JS-as-string>\` })` | mitigate | DELETE the entire inline plugin. The replacement is a `src/offscreen/recorder.ts` real module + a `src/offscreen/index.html` declared in `rollupOptions.input`. No more long template-literal JS in `vite.config.ts`. Grep gate: `grep -v '^#' vite.config.ts \| grep -c "this.emitFile"` returns 0. | | T-1-NEW-06-01 | Tampering — string-injected code via inline plugin | `vite.config.ts:13-216` `copy-offscreen` plugin's `this.emitFile({ source: \`<JS-as-string>\` })` | mitigate | DELETE the entire inline plugin (D-08). The replacement is a `src/offscreen/recorder.ts` real module + a `src/offscreen/index.html` declared in `rollupOptions.input`. No more long template-literal JS in `vite.config.ts`. Grep gate: `grep -v '^#' vite.config.ts \| grep -c "this.emitFile"` returns 0. |
| T-1-NEW-06-02 | Tampering — orphaned root-level offscreen | `offscreen/index.ts` + `offscreen/index.html` (dead code) | mitigate | DELETE both files. After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 0. | | T-1-NEW-06-02 | Tampering — orphaned root-level offscreen | `offscreen/index.ts` + `offscreen/index.html` (dead code) | mitigate | DELETE both files (D-07 / D-08 — runtime path is preserved via the crxjs-managed `src/offscreen/index.html`). After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 0. |
</threat_model> </threat_model>
<tasks> <tasks>