Skip to content

Commit bf706b0

Browse files
chore: strengthen CLI quality gates
1 parent 42a0b58 commit bf706b0

99 files changed

Lines changed: 4619 additions & 1039 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ build/
1010
.coverage
1111
coverage.xml
1212
htmlcov/
13+
mutants/
1314

1415
# Editor/agent local artifacts: keep personal settings local, but track the
1516
# team-shared bits (.claude/settings.json, agents/, skills/).

.importlinter

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
[importlinter]
2+
root_package = aai_cli
3+
include_external_packages = True
4+
5+
[importlinter:contract:1]
6+
name = Core modules do not import command modules
7+
type = forbidden
8+
source_modules =
9+
aai_cli.agent
10+
aai_cli.auth
11+
aai_cli.client
12+
aai_cli.code_gen
13+
aai_cli.config
14+
aai_cli.config_builder
15+
aai_cli.context
16+
aai_cli.environments
17+
aai_cli.errors
18+
aai_cli.follow
19+
aai_cli.help_panels
20+
aai_cli.help_text
21+
aai_cli.init
22+
aai_cli.llm
23+
aai_cli.microphone
24+
aai_cli.output
25+
aai_cli.render
26+
aai_cli.stdio
27+
aai_cli.streaming
28+
aai_cli.theme
29+
aai_cli.transcribe_render
30+
aai_cli.youtube
31+
forbidden_modules =
32+
aai_cli.commands
33+
34+
[importlinter:contract:2]
35+
name = Command modules are independent
36+
type = independence
37+
modules =
38+
aai_cli.commands.account
39+
aai_cli.commands.agent
40+
aai_cli.commands.audit
41+
aai_cli.commands.claude
42+
aai_cli.commands.doctor
43+
aai_cli.commands.init
44+
aai_cli.commands.keys
45+
aai_cli.commands.llm
46+
aai_cli.commands.login
47+
aai_cli.commands.samples
48+
aai_cli.commands.sessions
49+
aai_cli.commands.stream
50+
aai_cli.commands.transcribe
51+
aai_cli.commands.transcripts
52+
53+
[importlinter:contract:3]
54+
name = Library layers do not depend on Rich rendering
55+
type = forbidden
56+
source_modules =
57+
aai_cli.client
58+
aai_cli.config
59+
aai_cli.config_builder
60+
aai_cli.environments
61+
aai_cli.errors
62+
aai_cli.llm
63+
forbidden_modules =
64+
rich

.semgrep.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
rules:
2+
- id: aai-cli.subprocess-shell-true
3+
languages: [python]
4+
severity: ERROR
5+
message: Use argument lists and explicit executables; shell=True expands the attack surface.
6+
patterns:
7+
- pattern-either:
8+
- pattern: subprocess.run(..., shell=True, ...)
9+
- pattern: subprocess.Popen(..., shell=True, ...)
10+
- pattern: subprocess.call(..., shell=True, ...)
11+
- pattern: subprocess.check_call(..., shell=True, ...)
12+
- pattern: subprocess.check_output(..., shell=True, ...)
13+
paths:
14+
exclude:
15+
- /tests/**
16+
17+
- id: aai-cli.subprocess-string-command
18+
languages: [python]
19+
severity: ERROR
20+
message: Pass subprocess commands as argv lists, not shell-like strings.
21+
patterns:
22+
- pattern-either:
23+
- pattern: subprocess.run("...", ...)
24+
- pattern: subprocess.Popen("...", ...)
25+
- pattern: subprocess.call("...", ...)
26+
- pattern: subprocess.check_call("...", ...)
27+
- pattern: subprocess.check_output("...", ...)
28+
paths:
29+
exclude:
30+
- /tests/**
31+
32+
- id: aai-cli.swallowed-broad-exception
33+
languages: [python]
34+
severity: ERROR
35+
message: Do not silently swallow broad exceptions; catch a narrower type or surface a clean error.
36+
pattern-either:
37+
- pattern: |
38+
try:
39+
...
40+
except Exception:
41+
pass
42+
- pattern: |
43+
try:
44+
...
45+
except Exception as $EXC:
46+
pass
47+
- pattern: |
48+
try:
49+
...
50+
except BaseException:
51+
pass
52+
- pattern: |
53+
try:
54+
...
55+
except BaseException as $EXC:
56+
pass
57+
paths:
58+
exclude:
59+
- /tests/**
60+
61+
- id: aai-cli.raw-urlopen-or-module-request
62+
languages: [python]
63+
severity: ERROR
64+
message: Route external HTTP through typed client helpers with fixed hosts and timeout handling.
65+
pattern-either:
66+
- pattern: urllib.request.urlopen($URL, ...)
67+
- pattern: requests.$METHOD($URL, ...)
68+
- pattern: httpx.$METHOD($URL, ...)
69+
- pattern: httpx2.$METHOD($URL, ...)
70+
paths:
71+
exclude:
72+
- /aai_cli/init/templates/**
73+
- /tests/**
74+
75+
- id: aai-cli.secret-like-value-to-stdout
76+
languages: [python]
77+
severity: ERROR
78+
message: Secret-like values must be masked or kept on stderr/internal channels before printing.
79+
patterns:
80+
- pattern-either:
81+
- pattern: print($VALUE, ...)
82+
- pattern: $CONSOLE.print($VALUE, ...)
83+
- pattern: output.emit_text($VALUE)
84+
- metavariable-regex:
85+
metavariable: $VALUE
86+
regex: (?i).*(api_?key|session_?jwt|session_?token|token|secret|password).*
87+
paths:
88+
exclude:
89+
- /aai_cli/code_gen/**
90+
- /tests/**
91+
92+
- id: aai-cli.browser-asset-secret-reference
93+
languages: [generic]
94+
severity: ERROR
95+
message: Browser-visible template assets must not reference server secrets or auth headers.
96+
pattern-either:
97+
- pattern: ASSEMBLYAI_API_KEY
98+
- pattern: Authorization
99+
- pattern: "Bearer "
100+
- pattern: sk_live
101+
- pattern: sk_test
102+
paths:
103+
include:
104+
- /aai_cli/init/templates/**/index.html
105+
- /aai_cli/init/templates/**/static/**

aai_cli/agent/audio.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any
88

99
from aai_cli.errors import CLIError
10-
from aai_cli.microphone import _default_rate, _resample, audio_missing_error
10+
from aai_cli.microphone import audio_missing_error, default_rate, resample_pcm16
1111

1212
SAMPLE_RATE = 24000 # Voice Agent native PCM16 mono rate
1313

@@ -19,7 +19,7 @@ def _output_default_rate(device: int | None = None) -> int:
1919
'paramErr' (-50) from forcing an unsupported one; agent audio (24 kHz) is
2020
resampled to it. Falls back to a safe default when the device can't be queried.
2121
"""
22-
return _default_rate("output", device)
22+
return default_rate("output", device)
2323

2424

2525
class NullPlayer:
@@ -141,7 +141,7 @@ def start(self) -> None:
141141
def feed(self, pcm: bytes) -> None:
142142
"""Queue target-rate PCM for playback, resampled to the device rate."""
143143
if self._device_rate != self._target:
144-
pcm, self._out_state = _resample(
144+
pcm, self._out_state = resample_pcm16(
145145
pcm, self._out_state, src_rate=self._target, dst_rate=self._device_rate
146146
)
147147
with self._lock:
@@ -163,7 +163,7 @@ def capture_frames(self) -> Iterator[bytes]:
163163
if chunk is None:
164164
return
165165
if self._device_rate != self._target:
166-
chunk, state = _resample(
166+
chunk, state = resample_pcm16(
167167
chunk, state, src_rate=self._device_rate, dst_rate=self._target
168168
)
169169
yield chunk

aai_cli/agent/session.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,41 +71,41 @@ def dispatch(self, event: dict[str, Any]) -> None:
7171
if handler is not None:
7272
handler(self, event)
7373

74-
def _on_session_ready(self, _event: dict[str, Any]) -> None:
74+
def on_session_ready(self, _event: dict[str, Any]) -> None:
7575
with self._lock:
7676
self.ready = True
7777
if self.ready_event is not None:
7878
self.ready_event.set()
7979
self.renderer.connected()
8080

81-
def _on_speech_started(self, _event: dict[str, Any]) -> None:
81+
def on_speech_started(self, _event: dict[str, Any]) -> None:
8282
if self.full_duplex:
8383
self.player.flush()
8484

85-
def _on_user_delta(self, event: dict[str, Any]) -> None:
85+
def on_user_delta(self, event: dict[str, Any]) -> None:
8686
self.renderer.user_partial(event.get("text", ""))
8787

88-
def _on_user_final(self, event: dict[str, Any]) -> None:
88+
def on_user_final(self, event: dict[str, Any]) -> None:
8989
self._saw_user = True
9090
self.renderer.user_final(event.get("text", ""))
9191

92-
def _on_reply_started(self, _event: dict[str, Any]) -> None:
92+
def on_reply_started(self, _event: dict[str, Any]) -> None:
9393
if not self.full_duplex:
9494
with self._lock:
9595
self.muted = True
9696
self.renderer.reply_started()
9797

98-
def _on_reply_audio(self, event: dict[str, Any]) -> None:
98+
def on_reply_audio(self, event: dict[str, Any]) -> None:
9999
data = event.get("data")
100100
if data:
101101
self.player.enqueue(base64.b64decode(data))
102102

103-
def _on_agent_transcript(self, event: dict[str, Any]) -> None:
103+
def on_agent_transcript(self, event: dict[str, Any]) -> None:
104104
self.renderer.agent_transcript(
105105
event.get("text", ""), interrupted=bool(event.get("interrupted", False))
106106
)
107107

108-
def _on_reply_done(self, event: dict[str, Any]) -> None:
108+
def on_reply_done(self, event: dict[str, Any]) -> None:
109109
if not self.full_duplex:
110110
with self._lock:
111111
self.muted = False
@@ -117,7 +117,7 @@ def _on_reply_done(self, event: dict[str, Any]) -> None:
117117
if self.exit_after_reply and self._saw_user and not interrupted:
118118
self.finished = True
119119

120-
def _raise_error(self, event: dict[str, Any]) -> None:
120+
def raise_error(self, event: dict[str, Any]) -> None:
121121
code = event.get("code", "")
122122
message = event.get("message") or code or "Voice agent error."
123123
if code in _AUTH_ERROR_CODES:
@@ -132,15 +132,15 @@ def _raise_error(self, event: dict[str, Any]) -> None:
132132
# Server event type -> the VoiceAgentSession method that handles it. Types absent
133133
# here (input.speech.stopped, tool.call, anything unrecognized) are ignored.
134134
_EVENT_HANDLERS: dict[str, Callable[[VoiceAgentSession, dict[str, Any]], None]] = {
135-
"session.ready": VoiceAgentSession._on_session_ready,
136-
"input.speech.started": VoiceAgentSession._on_speech_started,
137-
"transcript.user.delta": VoiceAgentSession._on_user_delta,
138-
"transcript.user": VoiceAgentSession._on_user_final,
139-
"reply.started": VoiceAgentSession._on_reply_started,
140-
"reply.audio": VoiceAgentSession._on_reply_audio,
141-
"transcript.agent": VoiceAgentSession._on_agent_transcript,
142-
"reply.done": VoiceAgentSession._on_reply_done,
143-
"session.error": VoiceAgentSession._raise_error,
135+
"session.ready": VoiceAgentSession.on_session_ready,
136+
"input.speech.started": VoiceAgentSession.on_speech_started,
137+
"transcript.user.delta": VoiceAgentSession.on_user_delta,
138+
"transcript.user": VoiceAgentSession.on_user_final,
139+
"reply.started": VoiceAgentSession.on_reply_started,
140+
"reply.audio": VoiceAgentSession.on_reply_audio,
141+
"transcript.agent": VoiceAgentSession.on_agent_transcript,
142+
"reply.done": VoiceAgentSession.on_reply_done,
143+
"session.error": VoiceAgentSession.raise_error,
144144
}
145145

146146

0 commit comments

Comments
 (0)