Skip to content

fix(speech): dictation language selection, agent socket permissions, log cap - #47

Open
killme2008 wants to merge 6 commits into
AhakeyAI:mainfrom
killme2008:feat/dictation-language-and-hardening
Open

fix(speech): dictation language selection, agent socket permissions, log cap#47
killme2008 wants to merge 6 commits into
AhakeyAI:mainfrom
killme2008:feat/dictation-language-and-hardening

Conversation

@killme2008

@killme2008 killme2008 commented Aug 1, 2026

Copy link
Copy Markdown

1. 系统语言设为英文时,中文语音被英语模型转写

makeSpeechRecognizer() 只取 Locale.preferredLanguages.first。系统语言是英文时首选列表为 ["en-CN", "zh-Hans-CN"],中文轮不到。SFSpeechRecognizer 还会把 en-CN 静默归一化成 en-US

rawText=Kasi my friends          # 哈喽 my friends
rawText=Kanshi main fence        # 看事们分身

加一个持久化的语言设置,入口在语音键的「语音输入方式」和引导的「开始体验」,权限诊断弹窗显示当前生效语言。自动推断仍按首选语言顺序,改为在 supportedLocales() 内匹配;只给语言码时由 SFSpeechRecognizer 决定地区,因为 supportedLocales()Seten 的 13 个变体取出来不稳定。

引导步骤

语音输入方式

2. Agent 控制 socket 可被冒充

srwxr-xr-x  1 user  wheel  /tmp/ahakey.sock

/tmp 全局可写,startSocketListener() 发现已有监听会主动让位。其他本机用户可抢先 bind,对每个 {"cmd":"permission"}switchState: 0,全部自动批准。

socket 移到 ~/Library/Application Support/AhaKeyConfig/agent.sock(目录 0700),bind() 时用 umask + chmod 置为 0600accept()getpeereid() 校验 uid。同 uid 进程仍可连接,Unix 权限位管不了这层。

路径引用同步更新 HookSupportAhaKeyAgentmain.swiftPluginHostAgentManagerscripts/ahakey-state.sh

sun_path 只有 104 字节,原先各处用 strcpy 填充且无长度检查。/tmp/ahakey.sock 是 16 字节所以一直没事,新路径在短用户名下就有 65 字节,用户名超过约 44 字符会溢出。新增 AhaKeySocket.makeAddress,放不下就返回 nil,改用 strlcpy,六处填充点全部走它。

3. native-speech.log 无上限

逐字记录每次转写且从不截断。改为 1 MB 轮转,最多 5 个文件。

测试

新增 test target 和 CI 的 swift test。16 个用例覆盖语言匹配与日志轮转。

macOS 15 / Swift 6.2 clean build + test 通过。安装后确认选中文可使 locale=zh-CN 生效,socket 为 srw-------,部署目标仍为 macOS 12。

/tmp is world-writable, so any local user can bind ahakey.sock before the
agent does. The agent steps aside when it finds an existing listener, so a
squatter would then answer every approval request with switchState 0 and
silently defeat the physical lever the approval flow is built on.

Put the socket under ~/Library/Application Support/AhaKeyConfig (0700),
create it 0600 with a tightened umask around bind(), and drop connections
whose peer uid is not ours.

Processes running as the same user can still connect; Unix permissions
cannot express that boundary.
makeSpeechRecognizer() only ever tried Locale.preferredLanguages.first and
otherwise fell back to the system default recognizer. On a Mac whose UI
language is English but whose owner dictates Chinese, the preferred list
reads ["en-CN", "zh-Hans-CN"] — only the first entry was consulted, so
zh-Hans-CN was never reached and Chinese speech was transcribed by an
English model.

SFSpeechRecognizer compounds this: asking for en-CN hands back en-US
without saying so, and nothing surfaced which model was actually running.

- Add a persisted language preference, exposed as a picker on the voice
  key's input-method box and in the onboarding "try it" step, where a
  wrong language shows up on the very first recording.
- Resolve candidates against supportedLocales() instead of trusting an
  arbitrary identifier, and show which language will actually be used.
- Automatic resolution keeps preferred-language order and defers "which
  region for this language" to SFSpeechRecognizer, because
  supportedLocales() is a Set — en alone has 13 regional variants, so
  picking from it directly is not stable between runs.

The onboarding view receives a value plus a bag of closures, so the
preference reaches it through AhaKeyOnboardingPermissionState and a new
setSpeechLocale action rather than by handing it the service.
The matching rules are the part of the language handling most likely to
rot: exact language+region, fall back to language alone, skip tags that
parse to nothing. They lived as private statics on a @mainactor class,
unreachable from a test.

Move them to SpeechLocaleResolver, inject the "which region for this
language" step so tests do not depend on the host's installed speech
assets, and add the package's first test target.
native-speech.log records every transcript verbatim — rawText and
outputText, i.e. everything the user has ever dictated — and nothing ever
truncated it. On a machine that has been dictating for a while it is the
most sensitive file the app writes, with no bound on how far back it goes.

Rotate at 1 MB and keep five files at most, dropping the oldest, so recent
history stays available for debugging while the tail ages out.
The package gained its first test target in this branch; without a step
here the tests would never run on CI.
sun_path holds 104 bytes and the fill sites used strcpy without a length
check. /tmp/ahakey.sock was 16 bytes so the margin was never at risk; the
new path under the home directory is 65 bytes for a short user name and
grows with it, leaving room for roughly 44 characters before the copy
overflows.

Add AhaKeySocket.makeAddress, which refuses paths that do not fit and
copies with strlcpy, and route all six fill sites through it. AgentManager
and PluginHost carry their own copy since the targets do not share sources.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant