Source
Verdict
Release-pipeline gate: PASS — 0 Critical, 0 High, 0 Medium, 2 Low, 5 Info.
The 5 Info-level items are documented in the log only (per the harness handoff
rule, Info severity does not file an issue). The 2 Low items below are tracked
here for visibility — neither blocks a release, but both warrant follow-up
before the credential surface grows further.
Findings
F-2 — Plaintext credentials at rest (CWE-256)
File: Project/ZeroCommon/Voice/VoiceSettingsStore.cs:39
Affects: %LOCALAPPDATA%\AgentZeroLite\voice-settings.json
Settings persistence writes API keys (SttOpenAIApiKey, TtsOpenAIApiKey)
straight into JSON via JsonSerializer.Serialize + File.WriteAllText.
Same pattern exists in the pre-existing llm-settings.json
(OpenAIApiKey). Voice phase-2 added two more credential slots, raising
the total from one to three across the two side-car files.
Threat model under per-user Windows ACLs:
- Same user / different process → can read the file (e.g. malicious
PowerShell, IDE plugin running as the user) — not blocked
- Backup / cloud sync (OneDrive, Time Machine, Dropbox) → keys leave
the machine in plaintext — not blocked
- Disk theft / forensics on a non-TPM machine → keys readable —
not blocked
- Different user account on same machine → blocked by NTFS ACL — OK
Proposed remediation — single sweep across both files using DPAPI
with DataProtectionScope.CurrentUser:
// Save
var protectedBytes = ProtectedData.Protect(
Encoding.UTF8.GetBytes(apiKey), null, DataProtectionScope.CurrentUser);
// Stored as base64 in the JSON, or as a side-car .bin
// Load
var apiKey = Encoding.UTF8.GetString(
ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser));
Built-in to .NET (no NuGet add). CurrentUser scope means a backup
exfiltrated to another machine cannot be decrypted there. Same-user
processes can still decrypt — accepted residual risk consistent with how
VS Code / Postman / Notion handle their tokens.
F-3 — Preview package version pin (OWASP A06)
File: Project/AgentZeroWpf/AgentZeroWpf.csproj
<PackageReference Include="System.Speech" Version="10.0.0-preview.3.25171.5" />
System.Speech is pinned to a .NET 10 preview-3 build. Consistent
with the project's <TargetFramework>net10.0-windows</TargetFramework>
preview SDK, but a stable .NET 10 GA will ship a stable counterpart
that should replace this pin in the same commit that updates the SDK.
Proposed remediation — at .NET 10 GA:
- Bump
<TargetFramework> to the stable net10.0-windows.
- Update
System.Speech to its matching stable version (likely
10.0.0 or the first stable patch release).
- Re-run
build-doctor to confirm no behavioral diff.
Add a one-line tracker in memory/project_gemma4_self_build_lifecycle.md
under the existing self-built-DLL lifecycle so the bump is surfaced when
that file is consulted.
Recommendation
Sequence by effort and trigger:
-
Defer F-2 until first OpenAI key actually entered. The current
voice-settings.json on the dev machine has both Stt/Tts OpenAI key
slots empty — there is nothing to leak today. The DPAPI sweep should
land in the same commit that the user first enables an OpenAI STT or
TTS provider, so the migration is exercised on a non-empty file.
-
F-3 on .NET 10 GA day. No action until the stable SDK is
available; tracking line in the memory file is enough.
Both items are independent — fixing one does not require touching the
other.
Closes-when
Source
1267da6(voice phase-2 + INTERRUPT/LLM-cap)harness/logs/security-guard/2026-04-28-10-15-voice-subsystem-review.mdVerdict
Release-pipeline gate: PASS — 0 Critical, 0 High, 0 Medium, 2 Low, 5 Info.
The 5 Info-level items are documented in the log only (per the harness handoff
rule, Info severity does not file an issue). The 2 Low items below are tracked
here for visibility — neither blocks a release, but both warrant follow-up
before the credential surface grows further.
Findings
F-2 — Plaintext credentials at rest (CWE-256)
File:
Project/ZeroCommon/Voice/VoiceSettingsStore.cs:39Affects:
%LOCALAPPDATA%\AgentZeroLite\voice-settings.jsonSettings persistence writes API keys (
SttOpenAIApiKey,TtsOpenAIApiKey)straight into JSON via
JsonSerializer.Serialize+File.WriteAllText.Same pattern exists in the pre-existing
llm-settings.json(
OpenAIApiKey). Voice phase-2 added two more credential slots, raisingthe total from one to three across the two side-car files.
Threat model under per-user Windows ACLs:
PowerShell, IDE plugin running as the user) — not blocked
the machine in plaintext — not blocked
not blocked
Proposed remediation — single sweep across both files using DPAPI
with
DataProtectionScope.CurrentUser:Built-in to .NET (no NuGet add).
CurrentUserscope means a backupexfiltrated to another machine cannot be decrypted there. Same-user
processes can still decrypt — accepted residual risk consistent with how
VS Code / Postman / Notion handle their tokens.
F-3 — Preview package version pin (OWASP A06)
File:
Project/AgentZeroWpf/AgentZeroWpf.csprojSystem.Speechis pinned to a.NET 10 preview-3build. Consistentwith the project's
<TargetFramework>net10.0-windows</TargetFramework>preview SDK, but a stable
.NET 10GA will ship a stable counterpartthat should replace this pin in the same commit that updates the SDK.
Proposed remediation — at .NET 10 GA:
<TargetFramework>to the stablenet10.0-windows.System.Speechto its matching stable version (likely10.0.0or the first stable patch release).build-doctorto confirm no behavioral diff.Add a one-line tracker in
memory/project_gemma4_self_build_lifecycle.mdunder the existing self-built-DLL lifecycle so the bump is surfaced when
that file is consulted.
Recommendation
Sequence by effort and trigger:
Defer F-2 until first OpenAI key actually entered. The current
voice-settings.jsonon the dev machine has both Stt/Tts OpenAI keyslots empty — there is nothing to leak today. The DPAPI sweep should
land in the same commit that the user first enables an OpenAI STT or
TTS provider, so the migration is exercised on a non-empty file.
F-3 on .NET 10 GA day. No action until the stable SDK is
available; tracking line in the memory file is enough.
Both items are independent — fixing one does not require touching the
other.
Closes-when
voice-settings.jsonAND
llm-settings.json(single sweep, both files), and a load-timemigration path for any existing plaintext entries.
System.Speechbumped to the stable.NET 10GA version,verified by
build-doctor.memory/with an absolute-date line sofuture security passes know where to look.