RFAI-11: Ambient channel hardening — VS Code extension + voice prototype#1079
RFAI-11: Ambient channel hardening — VS Code extension + voice prototype#1079Chris0Jeky wants to merge 28 commits into
Conversation
Choose VS Code extension over desktop voice for ambient channel hardening based on testability, smaller privacy surface, and developer workflow fit. Add VsCodeExtension (= 11) to CaptureSource enum, register "vscode" as supported source surface, and add VsCode to LlmRequestSourceSurface.
Minimal extension with capture-selection and capture-file commands, right-click context menu, Ctrl+Shift+T keybinding, status bar indicator, and git remote hash for workspace identification. Uses VS Code SecretStorage for token management. No marketplace publishing (prototype only per ADR-0033).
Backend: 3 new theory tests verify "vscode" is accepted as a supported source surface (case-insensitive). Frontend: useVoiceCapture composable provides browser-local voice capture, explicitly rejecting webkitSpeechRecognition (streams to Google). 7 unit tests cover supported/unsupported/webkit-rejection, start/stop/transcript/error flows. Extension: context.test.ts covers buildCaptureText formatting.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a75a0efca5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "compile": "tsc -p ./", | ||
| "watch": "tsc -watch -p ./", | ||
| "lint": "eslint src", | ||
| "test": "node ./out/test/runTest.js" |
There was a problem hiding this comment.
Point extension test script to a real runner
The test script targets ./out/test/runTest.js, but this commit does not add a corresponding src/test/runTest.ts (or any checked-in runner that would produce that file), so npm test will fail with a module-not-found error in a clean checkout. This leaves the new extension without a runnable automated test entrypoint and blocks CI/local verification of the feature.
Useful? React with 👍 / 👎.
| req.on('error', (err) => reject(new Error(`Network error: ${err.message}`))); | ||
| req.write(body); | ||
| req.end(); |
There was a problem hiding this comment.
Add timeout handling for capture HTTP requests
This request path only listens for error events and never sets a request timeout, so if the configured API host accepts the connection but does not respond (for example, a hung proxy or blackholed route), the promise can remain pending for a long time and the command appears frozen with the UI stuck in "Sending...". Adding explicit timeout+abort handling would make failures deterministic and recoverable.
Useful? React with 👍 / 👎.
Adversarial Review R1 — RFAI-11: Ambient Channel HardeningCRITICALC-1: C-2: Command injection / NTLM relay via UNC HIGHH-1: HTTP client has no timeout — hangs indefinitely — H-2: H-3: H-4: MEDIUMM-1: Empty capture text sent to backend — M-2: Unvalidated M-3: Extension tests not runnable in CI — M-4: Unbounded response buffering — No limit on response body size in HTTP client. M-5: LOWL-1: VsCode vs VsCodeExtension naming asymmetry — Different enum names across layers, future maintenance trap. L-2: Empty token stored — Empty Enter in token prompt saves "" to SecretStorage. L-3: ADR-0033 filed as Accepted — Per convention should be Proposed until merged. L-4: VsCodeExtension missing from IsTranscriptSource theory — New enum value not covered in non-transcript test. L-5: Keybinding L-6: No consent hook on voice composable — No explicit consent gate in API surface for voice capture. Fix PlanAll 17 findings will be addressed (C-1 already done). Fixes incoming. |
C-2: Reject UNC paths in getGitRemoteHash to prevent NTLM relay H-1: Add 10s request timeout + timeout event handler to HTTP client H-2: Guard statusBarItem writes with disposed flag H-3: Add 4 source surface serialization pinning tests H-4: Preserve error state when onend fires after onerror M-1: Guard empty capture text in captureFileContext M-2: Validate URL format before storing apiUrl config M-3: Fix extension test script to compile and run context tests M-4: Limit response body buffering to 64KB M-5: Null recognition reference in onerror handler L-1: Document naming asymmetry with code comment L-2: Reject empty token in setToken command L-3: Mark ADR-0033 as Proposed (pre-merge convention) L-4: Add VsCodeExtension to IsTranscriptSource theory test L-5: Change keybinding to Ctrl+Shift+Alt+T (avoid Reopen Tab) L-6: Add consent acknowledgment gate to useVoiceCapture
C-R2-1: Fix test script — use tsc -p ./ (emit) instead of --noEmit;
refactor context.test.ts to inline the pure function to avoid vscode
module dependency outside extension host.
H-R2-1: Add res.on('error') no-op handler before res.destroy() to
prevent unhandled stream error event.
H-R2-2: Guard stopListening() with status !== 'error' check,
consistent with the onend handler fix from R1.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c990472479
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| }, | ||
| "scripts": { | ||
| "compile": "tsc -p ./", |
There was a problem hiding this comment.
Emit test artifact before requiring compiled test file
The test script still fails in a clean checkout because it type-checks with npx tsc --noEmit and then immediately requires ./out/context.test.js, but --noEmit does not produce out/*. Fresh evidence versus earlier review threads: the current script now points to out/context.test.js, yet it still never compiles before executing it, so npm test can fail with module-not-found unless npm run compile was run manually beforehand.
Useful? React with 👍 / 👎.
| try { | ||
| new URL(value); | ||
| } catch { |
There was a problem hiding this comment.
Reject non-HTTP schemes in API URL configuration
The URL validation only checks new URL(value) but does not enforce http:/https: schemes, so values like ftp://... are accepted by setApiUrl even though the UX says “valid HTTP(S) URL.” Later createCapture routes any non-https: protocol through http.request, which makes captures fail at runtime with a network/protocol error until the user fixes config manually.
Useful? React with 👍 / 👎.
Adversarial Review R2 — Post-Fix VerificationR1 Fix Verification
R2 New Findings (all fixed in
|
| Finding | Severity | Fix |
|---|---|---|
C-R2-1: --noEmit prevents test execution |
CRITICAL | Use tsc -p ./ (emit), inline pure function in test to avoid vscode dep |
H-R2-1: res.destroy() unhandled error event |
HIGH | Add res.on('error', () => {}) before destroy |
H-R2-2: stopListening() erases error state |
HIGH | Guard with status !== 'error' check |
Verdict: PASS — All R1 + R2 findings fixed and verified.
Test results:
- Frontend: 3292 tests pass
- Backend (relevant): 49 tests pass (CaptureRequestContract + AttributionMapper)
- Extension: 10 context tests pass, typecheck clean
- Extension test script (
npm test) now works end-to-end
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 385fb52740
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let data = ''; | ||
| let bytesReceived = 0; | ||
|
|
||
| res.on('error', () => {}); |
There was a problem hiding this comment.
Reject aborted capture responses instead of swallowing them
The response stream error handler is currently a no-op, so a connection drop after headers can leave this promise unresolved: end never fires, no rejection is emitted, and the command can stay stuck in the "Sending..." state indefinitely. This is especially reproducible with flaky proxies or mid-stream socket resets. Handle res failures (error/aborted/close) by rejecting so the UI can recover.
Useful? React with 👍 / 👎.
| @@ -0,0 +1,106 @@ | |||
| // Test the pure buildCaptureText function without importing vscode-dependent modules. | |||
| // We re-implement the type and function inline to avoid the vscode import chain. | |||
There was a problem hiding this comment.
Test the real formatter instead of a copied implementation
This test file duplicates buildCaptureText inline instead of importing the production function, so the test can pass even when src/context.ts regresses. Any future change that updates only one copy will silently invalidate coverage and let formatting bugs ship. The test should execute the actual exported formatter (or a helper extracted from it), not a reimplementation.
Useful? React with 👍 / 👎.
Cleanup Fix Evidence — 2026-05-16Addressed the current CI and bot-review blockers in commit
Verification run locally from
Waiting for GitHub CI on the pushed head before considering stack retarget/merge readiness. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a689d9f66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return new Promise<CaptureResponse>((resolve, reject) => { | ||
| const transport = url.protocol === 'https:' ? https : http; | ||
| let settled = false; |
There was a problem hiding this comment.
Enforce HTTP(S) scheme before selecting HTTP transport
createCapture falls back to http for any non-https: protocol, so a user-edited taskdeck.apiUrl (for example ftp://... in VS Code settings JSON) will bypass the command-level validator and fail at runtime with a protocol/network error instead of a deterministic config error. Fresh evidence versus earlier review threads: setApiUrl now checks protocol, but this path still reads raw config and never rejects non-HTTP schemes before http.request is chosen.
Useful? React with 👍 / 👎.
|
Follow-up fix pushed for the failing frontend lint lane. Fix commit:
Local verification:
Fresh CI is pending after the push. |
…feat/983-ambient-channel-hardening
|
Follow-up merge pushed to pick up #1078's Windows PaperReviewView test fix on this stacked branch. Update:
Local verification:
Fresh CI is pending after the push. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b660edf28a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!isLocalAbsolutePath(cwd)) return null; | ||
|
|
||
| try { | ||
| const remote = execSync('git remote get-url origin', { |
There was a problem hiding this comment.
Avoid shell-based git lookup in workspace context
This call executes 'git remote get-url origin' through execSync with cwd set to the opened workspace, which allows command resolution from the workspace directory. On Windows, a repo containing a malicious git.cmd/git.bat can be executed when capture runs, turning a normal capture action into arbitrary code execution. Use a non-shell invocation (execFile/spawn with shell: false) and explicit args to prevent workspace command hijacking.
Useful? React with 👍 / 👎.
| recognition = new SpeechRecognitionCtor() | ||
| recognition.continuous = false |
There was a problem hiding this comment.
Prevent overlapping voice recognition sessions
startListening() always creates a new SpeechRecognition instance without checking whether one is already active. If the user triggers start twice before onend, the original session keeps running but its reference is lost, so stopListening() can no longer stop it and multiple callbacks can race to mutate transcript/status. Guard against re-entry (or stop the existing instance) before assigning a new recognizer.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fdef719543
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch { | ||
| throw new Error(`Invalid API URL: "${apiUrl}". Run "Taskdeck: Set API URL" to fix.`); | ||
| } | ||
| const url = new URL('/api/capture/items', baseUrl); |
There was a problem hiding this comment.
Preserve API base path when building capture endpoint
This always constructs the request URL from an absolute path (/api/capture/items), which drops any path segment configured in taskdeck.apiUrl (for example https://host/taskdeck becomes https://host/api/capture/items). Because parseTaskdeckApiBaseUrl currently accepts such URLs, reverse-proxy/subpath deployments will consistently post to the wrong endpoint and capture will fail until users reconfigure to an origin-only URL.
Useful? React with 👍 / 👎.
|
Current-head fix/propagation evidence at
Verification run locally:
|
Fresh adversarial review - PR #1079 current head
|
…feat/983-ambient-channel-hardening
|
Fix evidence for current-head MEDIUM finding at
Verification run locally:
Fresh CI is pending on the pushed head. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e48df347e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (recognition) { | ||
| recognition.stop() | ||
| recognition = null | ||
| } |
There was a problem hiding this comment.
Preserve final transcript when stopping recognition
stopListening() clears recognition immediately after calling stop(), but SpeechRecognition.stop() can deliver the final onresult asynchronously. Because onresult ignores callbacks when recognition !== activeRecognition, users who press stop right after speaking can lose the last recognized phrase and end up with an empty/partial transcript even though recognition succeeded.
Useful? React with 👍 / 👎.
Fresh adversarial review - PR #1079 current head
|
…feat/983-ambient-channel-hardening
…feat/983-ambient-channel-hardening
|
Follow-up fix evidence at
Verification run locally:
Fresh CI is pending on the pushed head. |
Fresh adversarial review - PR #1079 current head
|
|
Follow-up fix evidence at
Verification run locally:
Fresh CI is pending on the pushed head. |
Fresh adversarial review - PR #1079 current head
|
|
Conflict resolution evidence for current head d61c084:
Fresh current-head adversarial review is running for d61c084 before merge. |
Fresh adversarial review - PR #1079 current head
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d61c0849c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| setTimeout(() => { | ||
| setStatusText('$(cloud-upload) Taskdeck'); | ||
| }, 3000); |
There was a problem hiding this comment.
Serialize status-bar reset across concurrent captures
sendCapture always schedules an unconditional status reset 3 seconds after each attempt, so overlapping captures can clobber each other’s UI state: if capture A finishes first while capture B is still running, A’s timer flips the status bar back to $(cloud-upload) Taskdeck even though B is still Sending... (or may later fail). This makes the extension report an idle/success-looking state during an in-flight request and can hide useful failure feedback for the active capture. Track the latest request (or an in-flight counter) before resetting the status text.
Useful? React with 👍 / 👎.
|
Fix evidence for review comment #1079 (comment) at head 6acab91:
Verification:
Fresh current-head re-review requested for 6acab91 before merge. |
Fresh adversarial re-review - PR #1079 head
|
|
Fix evidence for re-review comment #1079 (comment) at head 20848e6:
Verification after the fix:
Fresh current-head re-review requested for 20848e6 before merge. |
Fresh adversarial re-review - PR #1079 head
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20848e6191
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "scripts": { | ||
| "compile": "tsc -p ./", | ||
| "watch": "tsc -watch -p ./", | ||
| "lint": "eslint src", |
There was a problem hiding this comment.
Add ESLint dependency for the declared lint script
The extension manifest defines "lint": "eslint src", but this commit does not add eslint to devDependencies (or the lockfile), so npm run lint fails in a clean checkout with a command-not-found error. That makes the provided lint entrypoint non-functional for local verification and will break any CI job that invokes the lint script.
Useful? React with 👍 / 👎.
|
Fix evidence for re-review comment #1079 (comment) at head 1308419:
Verification after the fix:
Fresh current-head review requested for 1308419 before merge. |
Fresh adversarial re-review - PR #1079 head
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1308419035
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fileName: 'C:\\Users\\alice\\Documents\\private-plan.ts', | ||
| uriScheme: 'file', | ||
| uriFsPath: 'C:\\Users\\alice\\Documents\\private-plan.ts', | ||
| }); | ||
| assert(result === 'private-plan.ts', 'uses only basename for files outside a workspace'); |
There was a problem hiding this comment.
Make Windows-path assertion platform-independent
This test hard-codes a Windows-style path and expects getSafeDocumentLabel to return only private-plan.ts, but node:path.basename is platform-dependent: on Linux/macOS it treats backslashes as normal characters, so the value remains C:\\Users\\alice\\Documents\\private-plan.ts. In CI or local non-Windows runs, this causes npm test to fail even when the implementation is unchanged. Use a platform-agnostic expectation (or path.win32.basename for this fixture) so the extension test suite is runnable cross-platform.
Useful? React with 👍 / 👎.
Summary
VsCodeExtension = 11toCaptureSourceenum, registers"vscode"as supported source surface, addsVsCodetoLlmRequestSourceSurfaceextensions/vscode-taskdeck/): Capture-selection and capture-file commands, right-click context menu,Ctrl+Shift+Tkeybinding, status bar indicator, git remote hash for workspace identification, VS Code SecretStorage for token managementuseVoiceCapturecomposable provides browser-local voice capture with explicitwebkitSpeechRecognitionrejection (streams audio to Google)VsCodeExtensioninCaptureSourcetype unionTesting
"vscode"accepted as source surface (case-insensitive)buildCaptureTextformattingCloses #983
Stacked on #1078 (
feat/982-pwa-share-target)Test plan
dotnet test --filter CaptureRequestContractTestspasses with new VsCode surface testsnpx vitest --run -t "useVoiceCapture"— 7 tests passcd extensions/vscode-taskdeck && npx tsc --noEmitcompiles cleandocs/decisions/INDEX.mdCaptureSource.VsCodeExtensionflows through capture API without errors