Compare commits
10 Commits
178fdd5b77
...
acb9033293
| Author | SHA1 | Date | |
|---|---|---|---|
| acb9033293 | |||
| 0811c6a292 | |||
| 576280f6aa | |||
| 519a0d8a99 | |||
| 61c3e03069 | |||
| 51890b0bc4 | |||
| e55c1ae5d6 | |||
| b8219af5b9 | |||
| c24fcda818 | |||
| 74ff472811 |
@@ -2,14 +2,14 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v2.0.0
|
milestone: v2.0.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: planning
|
status: executing
|
||||||
stopped_at: Phase 1 context gathered
|
stopped_at: Phase 1 context gathered
|
||||||
last_updated: "2026-05-15T13:40:45.486Z"
|
last_updated: "2026-05-15T15:08:45.135Z"
|
||||||
last_activity: 2026-05-15 — bootstrap from intel synthesis (PROJECT.md,
|
last_activity: 2026-05-15 -- Phase 1 planning complete
|
||||||
progress:
|
progress:
|
||||||
total_phases: 5
|
total_phases: 5
|
||||||
completed_phases: 0
|
completed_phases: 0
|
||||||
total_plans: 0
|
total_plans: 7
|
||||||
completed_plans: 0
|
completed_plans: 0
|
||||||
percent: 0
|
percent: 0
|
||||||
---
|
---
|
||||||
@@ -29,8 +29,8 @@ no server, no password leaks.
|
|||||||
|
|
||||||
Phase: 1 of 5 (Stabilize video pipeline)
|
Phase: 1 of 5 (Stabilize video pipeline)
|
||||||
Plan: 0 of TBD in current phase
|
Plan: 0 of TBD in current phase
|
||||||
Status: Ready to plan
|
Status: Ready to execute
|
||||||
Last activity: 2026-05-15 — bootstrap from intel synthesis (PROJECT.md,
|
Last activity: 2026-05-15 -- Phase 1 planning complete
|
||||||
REQUIREMENTS.md, ROADMAP.md, STATE.md written)
|
REQUIREMENTS.md, ROADMAP.md, STATE.md written)
|
||||||
|
|
||||||
Progress: [░░░░░░░░░░] 0%
|
Progress: [░░░░░░░░░░] 0%
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ files_modified:
|
|||||||
- .planning/intel/constraints.md
|
- .planning/intel/constraints.md
|
||||||
- manifest.json
|
- manifest.json
|
||||||
autonomous: true
|
autonomous: true
|
||||||
|
scope_exception: "doc-text-only edits with no TypeScript compilation; cognitive load substantially lower than code plans of equal task count"
|
||||||
requirements:
|
requirements:
|
||||||
- REQ-video-ring-buffer
|
- REQ-video-ring-buffer
|
||||||
requirements_addressed:
|
requirements_addressed:
|
||||||
@@ -24,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"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 02
|
plan: 02
|
||||||
type: execute
|
type: execute
|
||||||
wave: 0
|
wave: 1
|
||||||
depends_on: ["01"]
|
depends_on: ["01"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- package.json
|
- package.json
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 03
|
plan: 03
|
||||||
type: tdd
|
type: tdd
|
||||||
wave: 1
|
wave: 2
|
||||||
depends_on: ["02"]
|
depends_on: ["02"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- src/offscreen/recorder.ts
|
- src/offscreen/recorder.ts
|
||||||
@@ -10,6 +10,7 @@ files_modified:
|
|||||||
- src/shared/logger.ts
|
- src/shared/logger.ts
|
||||||
- src/shared/types.ts
|
- src/shared/types.ts
|
||||||
autonomous: true
|
autonomous: true
|
||||||
|
style_divergence_note: "OffscreenLogger uses `...args: unknown[]` (strict-mode hygiene) where existing Logger / ContentLogger use `...args: any[]` (project convention). Refactoring the existing two for consistency is OUT OF SCOPE for Phase 1; they remain on the legacy pattern."
|
||||||
requirements:
|
requirements:
|
||||||
- REQ-video-ring-buffer
|
- REQ-video-ring-buffer
|
||||||
requirements_addressed:
|
requirements_addressed:
|
||||||
@@ -17,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"
|
||||||
@@ -71,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
|
||||||
@@ -154,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>
|
||||||
|
|
||||||
@@ -519,20 +524,20 @@ export class OffscreenLogger {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private logWithLevel(level: 'log' | 'warn' | 'error', ...args: any[]) {
|
private logWithLevel(level: 'log' | 'warn' | 'error', ...args: unknown[]) {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
console[level](`[OS:${this.context}] ${timestamp}`, ...args);
|
console[level](`[OS:${this.context}] ${timestamp}`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: unknown[]) {
|
||||||
this.logWithLevel('log', ...args);
|
this.logWithLevel('log', ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(...args: any[]) {
|
warn(...args: unknown[]) {
|
||||||
this.logWithLevel('warn', ...args);
|
this.logWithLevel('warn', ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(...args: any[]) {
|
error(...args: unknown[]) {
|
||||||
this.logWithLevel('error', ...args);
|
this.logWithLevel('error', ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 04
|
plan: 04
|
||||||
type: tdd
|
type: tdd
|
||||||
wave: 1
|
wave: 3
|
||||||
depends_on: ["02", "03"]
|
depends_on: ["02", "03"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- src/offscreen/recorder.ts
|
- src/offscreen/recorder.ts
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 05
|
plan: 05
|
||||||
type: execute
|
type: execute
|
||||||
wave: 2
|
wave: 4
|
||||||
depends_on: ["03", "04"]
|
depends_on: ["03", "04"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- src/background/index.ts
|
- src/background/index.ts
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 06
|
plan: 06
|
||||||
type: execute
|
type: execute
|
||||||
wave: 2
|
wave: 5
|
||||||
depends_on: ["03"]
|
depends_on: ["03", "05"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- vite.config.ts
|
- vite.config.ts
|
||||||
- offscreen/index.ts
|
- offscreen/index.ts
|
||||||
- offscreen/index.html
|
- offscreen/index.html
|
||||||
|
- src/background/index.ts
|
||||||
autonomous: true
|
autonomous: true
|
||||||
requirements:
|
requirements:
|
||||||
- REQ-video-ring-buffer
|
- REQ-video-ring-buffer
|
||||||
@@ -16,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"
|
||||||
@@ -106,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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 01-stabilize-video-pipeline
|
phase: 01-stabilize-video-pipeline
|
||||||
plan: 07
|
plan: 07
|
||||||
type: execute
|
type: execute
|
||||||
wave: 3
|
wave: 6
|
||||||
depends_on: ["05", "06"]
|
depends_on: ["05", "06"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- tests/fixtures/last_30sec.webm
|
- tests/fixtures/last_30sec.webm
|
||||||
@@ -139,7 +139,7 @@ The full Phase-1 Mokosh build:
|
|||||||
- Archive is delivered via `chrome.downloads`
|
- Archive is delivered via `chrome.downloads`
|
||||||
</what-built>
|
</what-built>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>which ffprobe && ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm; test $? -eq 0</automated>
|
<automated>test -f tests/fixtures/last_30sec.webm && which ffprobe && ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm; test $? -eq 0</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<how-to-verify>
|
<how-to-verify>
|
||||||
**Pre-flight (automated, do these in a bash shell before opening Chrome):**
|
**Pre-flight (automated, do these in a bash shell before opening Chrome):**
|
||||||
|
|||||||
@@ -1017,10 +1017,11 @@ stream.getTracks().forEach((track) => {
|
|||||||
**If this table contains items:** The planner should treat them as
|
**If this table contains items:** The planner should treat them as
|
||||||
candidates for user verification during `/gsd-plan-phase` review.
|
candidates for user verification during `/gsd-plan-phase` review.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions (RESOLVED)
|
||||||
|
|
||||||
1. **Will `MediaRecorder.start(2000)` produce ffprobe-clean WebM on a
|
1. **Will `MediaRecorder.start(2000)` produce ffprobe-clean WebM on a
|
||||||
typical screen-cap?**
|
typical screen-cap?**
|
||||||
|
- **RESOLVED:** Cluster-boundary alignment is resolved by the D-12 ffprobe acceptance gate (enforced in Plan 03 Task 2 verify path + Plan 07 Task 1) and the D-13 restart-segments fallback (pre-staged as a commented skeleton in `src/offscreen/recorder.ts` per Plan 03; activated by re-plan after Plan 07 if the gate fails).
|
||||||
- What we know: Cluster boundaries align with keyframes; Chrome
|
- What we know: Cluster boundaries align with keyframes; Chrome
|
||||||
keyframes appear every ~3-5 s by default (vp9 `kf_max_dist=100` on
|
keyframes appear every ~3-5 s by default (vp9 `kf_max_dist=100` on
|
||||||
a 30 fps stream); timeslice does NOT force keyframes.
|
a 30 fps stream); timeslice does NOT force keyframes.
|
||||||
@@ -1034,6 +1035,7 @@ candidates for user verification during `/gsd-plan-phase` review.
|
|||||||
this in the success criteria.
|
this in the success criteria.
|
||||||
|
|
||||||
2. **Does the 5-minute port lifetime kill the recording session?**
|
2. **Does the 5-minute port lifetime kill the recording session?**
|
||||||
|
- **RESOLVED:** Plan 04's 290 s pre-emptive reconnect logic plus the synchronous onDisconnect → connectPort reconnect path mitigate the cap whether it applies to port lifetime or SW lifetime; either way the offscreen reconnects within seconds and the buffer is unaffected.
|
||||||
- What we know: Multiple corroborating community sources cite a ~5
|
- What we know: Multiple corroborating community sources cite a ~5
|
||||||
minute hard cap on long-lived ports.
|
minute hard cap on long-lived ports.
|
||||||
- What's unclear: Whether the cap applies to *port lifetime* (the
|
- What's unclear: Whether the cap applies to *port lifetime* (the
|
||||||
@@ -1045,6 +1047,7 @@ candidates for user verification during `/gsd-plan-phase` review.
|
|||||||
reconnect is still harmless.
|
reconnect is still harmless.
|
||||||
|
|
||||||
3. **What's the exact crxjs path-emit behavior for the offscreen entry?**
|
3. **What's the exact crxjs path-emit behavior for the offscreen entry?**
|
||||||
|
- **RESOLVED:** Plan 06 Task 2 performs runtime verification — runs `npm run build`, inspects `dist/` for whichever of `dist/src/offscreen/index.html` or `dist/offscreen/index.html` was emitted, then edits `src/background/index.ts`'s `chrome.runtime.getURL(...)` argument to match (this is why Plan 06 now lists `src/background/index.ts` in files_modified per the iteration-1 dependency-correctness fix).
|
||||||
- What we know: The discussion #919 working answer uses
|
- What we know: The discussion #919 working answer uses
|
||||||
`input: { offscreen: 'src/offscreen/offscreen.html' }` and SW
|
`input: { offscreen: 'src/offscreen/offscreen.html' }` and SW
|
||||||
fetches `chrome.runtime.getURL('src/offscreen/offscreen.html')`.
|
fetches `chrome.runtime.getURL('src/offscreen/offscreen.html')`.
|
||||||
|
|||||||
Reference in New Issue
Block a user