Skip to content

Commit bb6410c

Browse files
anandgupta42claude
andauthored
fix: address code review findings — security, CI/CD hardening, test coverage (#45)
* fix: address code review findings — security, CI/CD hardening, and test coverage Security: - Add confirmation prompt before executing well-known auth commands - Pin all GitHub Actions to SHA in release/CI/docs/publish workflows - Scope workflow permissions per-job (least privilege) Bug fixes: - Fix copy-paste log messages in MCP (resources logged as "prompts") - Log MCP server initialization errors instead of silent swallow - Add void prefix to floating promises in MCP census telemetry - Re-add telemetry events to buffer on flush failure (one retry) - Clean compaction attempt map on abort signal CI/CD hardening: - Pin ruff (0.9.10), bun (1.3.9), build (1.2.2) versions - Add skip-existing to standalone publish-engine.yml Test coverage (+87 tests): - 50 new telemetry tests: parseConnectionString, toAppInsightsEnvelopes, flush/retry, shutdown, buffer overflow, init lifecycle - 40 new processor tests: tool call telemetry, categorization, error recovery, doom loop detection, context utilization - 24 new MCP tests: error recovery, tool registration, initialization resilience, timeout, status transitions, cleanup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: validate wellknown auth command type before execution Addresses PR review comment — validate that wellknown.auth.command is actually an array of strings before using it, preventing unexpected types from a malicious server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a08e571 commit bb6410c

File tree

11 files changed

+2434
-46
lines changed

11 files changed

+2434
-46
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ jobs:
1111
name: TypeScript
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4
14+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1515

16-
- uses: oven-sh/setup-bun@v2
16+
- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
17+
with:
18+
bun-version: "1.3.9"
1719

1820
- name: Cache Bun dependencies
19-
uses: actions/cache@v4
21+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
2022
with:
2123
path: ~/.bun/install/cache
2224
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
@@ -39,14 +41,14 @@ jobs:
3941
name: Lint
4042
runs-on: ubuntu-latest
4143
steps:
42-
- uses: actions/checkout@v4
44+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
4345

44-
- uses: actions/setup-python@v5
46+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
4547
with:
4648
python-version: "3.12"
4749

4850
- name: Install linter
49-
run: pip install ruff
51+
run: pip install ruff==0.9.10
5052

5153
- name: Lint
5254
run: ruff check src
@@ -59,9 +61,9 @@ jobs:
5961
matrix:
6062
python-version: ["3.10", "3.11", "3.12"]
6163
steps:
62-
- uses: actions/checkout@v4
64+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6365

64-
- uses: actions/setup-python@v5
66+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
6567
with:
6668
python-version: ${{ matrix.python-version }}
6769
cache: 'pip'

.github/workflows/docs.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
runs-on: ubuntu-latest
2121
steps:
2222
- name: Checkout
23-
uses: actions/checkout@v4
23+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2424

2525
- name: Setup Python
26-
uses: actions/setup-python@v5
26+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
2727
with:
2828
python-version: "3.12"
2929
cache: "pip"
@@ -36,10 +36,10 @@ jobs:
3636
run: mkdocs build -f docs/mkdocs.yml -d site
3737

3838
- name: Setup Pages
39-
uses: actions/configure-pages@v5
39+
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
4040

4141
- name: Upload artifact
42-
uses: actions/upload-pages-artifact@v3
42+
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
4343
with:
4444
path: docs/site
4545

@@ -52,4 +52,4 @@ jobs:
5252
steps:
5353
- name: Deploy to GitHub Pages
5454
id: deployment
55-
uses: actions/deploy-pages@v4
55+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

.github/workflows/publish-engine.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@ jobs:
1313
permissions:
1414
id-token: write
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1717

18-
- uses: actions/setup-python@v5
18+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
1919
with:
2020
python-version: "3.12"
2121
cache: "pip"
2222
cache-dependency-path: packages/altimate-engine/pyproject.toml
2323

2424
- name: Install build tools
25-
run: pip install build
25+
run: pip install build==1.2.2
2626

2727
- name: Build package
2828
run: python -m build
2929
working-directory: packages/altimate-engine
3030

3131
- name: Publish to PyPI
32-
uses: pypa/gh-action-pypi-publish@release/v1
32+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
3333
with:
3434
packages-dir: packages/altimate-engine/dist/
35+
skip-existing: true

.github/workflows/release.yml

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@ concurrency:
99
group: release
1010
cancel-in-progress: false
1111

12-
permissions:
13-
contents: write
14-
id-token: write
15-
1612
env:
1713
GH_REPO: AltimateAI/altimate-code
1814

1915
jobs:
2016
build:
2117
name: Build (${{ matrix.os }})
2218
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
2321
strategy:
2422
fail-fast: false
2523
matrix:
2624
os: [linux, darwin, win32]
2725
steps:
28-
- uses: actions/checkout@v4
26+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2927

30-
- uses: oven-sh/setup-bun@v2
28+
- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
29+
with:
30+
bun-version: "1.3.9"
3131

3232
- name: Cache Bun dependencies
33-
uses: actions/cache@v4
33+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
3434
with:
3535
path: ~/.bun/install/cache
3636
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
@@ -50,7 +50,7 @@ jobs:
5050
MODELS_DEV_API_JSON: test/tool/fixtures/models-api.json
5151

5252
- name: Upload build artifacts
53-
uses: actions/upload-artifact@v4
53+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
5454
with:
5555
name: dist-${{ matrix.os }}
5656
path: packages/altimate-code/dist/
@@ -59,13 +59,17 @@ jobs:
5959
name: Publish to npm
6060
needs: build
6161
runs-on: ubuntu-latest
62+
permissions:
63+
contents: read
6264
steps:
63-
- uses: actions/checkout@v4
65+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6466

65-
- uses: oven-sh/setup-bun@v2
67+
- uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
68+
with:
69+
bun-version: "1.3.9"
6670

6771
- name: Cache Bun dependencies
68-
uses: actions/cache@v4
72+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
6973
with:
7074
path: ~/.bun/install/cache
7175
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
@@ -76,7 +80,7 @@ jobs:
7680
run: bun install
7781

7882
- name: Download all build artifacts
79-
uses: actions/download-artifact@v4
83+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
8084
with:
8185
pattern: dist-*
8286
path: packages/altimate-code/dist/
@@ -124,23 +128,23 @@ jobs:
124128
contents: read
125129
id-token: write
126130
steps:
127-
- uses: actions/checkout@v4
131+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
128132

129-
- uses: actions/setup-python@v5
133+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
130134
with:
131135
python-version: "3.12"
132136
cache: 'pip'
133137
cache-dependency-path: packages/altimate-engine/pyproject.toml
134138

135139
- name: Install build tools
136-
run: pip install build
140+
run: pip install build==1.2.2
137141

138142
- name: Build package
139143
run: python -m build
140144
working-directory: packages/altimate-engine
141145

142146
- name: Publish to PyPI
143-
uses: pypa/gh-action-pypi-publish@release/v1
147+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
144148
with:
145149
packages-dir: packages/altimate-engine/dist/
146150
skip-existing: true
@@ -152,7 +156,7 @@ jobs:
152156
permissions:
153157
contents: write
154158
steps:
155-
- uses: actions/checkout@v4
159+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
156160
with:
157161
fetch-depth: 0
158162

@@ -206,14 +210,14 @@ jobs:
206210
CURRENT_TAG: ${{ github.ref_name }}
207211

208212
- name: Download all build artifacts
209-
uses: actions/download-artifact@v4
213+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
210214
with:
211215
pattern: dist-*
212216
path: packages/altimate-code/dist/
213217
merge-multiple: true
214218

215219
- name: Create GitHub Release
216-
uses: softprops/action-gh-release@v2
220+
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
217221
with:
218222
body_path: notes.md
219223
draft: false

packages/altimate-code/src/cli/cmd/auth.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,23 @@ export const AuthLoginCommand = cmd({
314314
prompts.intro("Add credential")
315315
if (args.url) {
316316
const wellknown = await fetch(`${args.url}/.well-known/altimate-code`).then((x) => x.json() as any)
317-
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
317+
const raw = wellknown?.auth?.command
318+
if (!Array.isArray(raw) || !raw.every((c: unknown) => typeof c === 'string')) {
319+
prompts.log.warn('Invalid auth command from server')
320+
prompts.outro('Done')
321+
return
322+
}
323+
const cmd = raw as string[]
324+
const confirm = await prompts.confirm({
325+
message: `The server requests to run: ${cmd.join(" ")}. Allow?`,
326+
})
327+
if (prompts.isCancel(confirm) || !confirm) {
328+
prompts.log.warn("Aborted.")
329+
prompts.outro("Done")
330+
return
331+
}
318332
const proc = Bun.spawn({
319-
cmd: wellknown.auth.command,
333+
cmd,
320334
stdout: "pipe",
321335
})
322336
const exit = await proc.exited

packages/altimate-code/src/mcp/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ export namespace MCP {
188188
return
189189
}
190190

191-
const result = await create(key, mcp).catch(() => undefined)
191+
const result = await create(key, mcp).catch((e) => {
192+
log.warn("failed to initialize MCP server", { key, error: e instanceof Error ? e.message : String(e) })
193+
return undefined
194+
})
192195
if (!result) return
193196

194197
status[key] = result.status
@@ -244,7 +247,7 @@ export namespace MCP {
244247

245248
async function fetchResourcesForClient(clientName: string, client: Client) {
246249
const resources = await withTimeout(client.listResources(), DEFAULT_TIMEOUT).catch((e) => {
247-
log.error("failed to get prompts", { clientName, error: e.message })
250+
log.error("failed to get resources", { clientName, error: e.message })
248251
return undefined
249252
})
250253

@@ -380,7 +383,7 @@ export namespace MCP {
380383
})
381384
// Census: collect tool and resource counts (fire-and-forget, never block connect)
382385
const remoteTransport = name === "SSE" ? "sse" as const : "streamable-http" as const
383-
Promise.all([
386+
void Promise.all([
384387
client.listTools().catch(() => ({ tools: [] })),
385388
client.listResources().catch(() => ({ resources: [] })),
386389
]).then(([toolsList, resourcesList]) => {
@@ -496,7 +499,7 @@ export namespace MCP {
496499
duration_ms: Date.now() - localConnectStart,
497500
})
498501
// Census: collect tool and resource counts (fire-and-forget, never block connect)
499-
Promise.all([
502+
void Promise.all([
500503
client.listTools().catch(() => ({ tools: [] })),
501504
client.listResources().catch(() => ({ resources: [] })),
502505
]).then(([toolsList, resourcesList]) => {
@@ -781,7 +784,7 @@ export namespace MCP {
781784
const client = clientsSnapshot[clientName]
782785

783786
if (!client) {
784-
log.warn("client not found for prompt", {
787+
log.warn("client not found for resource", {
785788
clientName: clientName,
786789
})
787790
return undefined
@@ -792,7 +795,7 @@ export namespace MCP {
792795
uri: resourceUri,
793796
})
794797
.catch((e) => {
795-
log.error("failed to get prompt from MCP server", {
798+
log.error("failed to read resource from MCP server", {
796799
clientName: clientName,
797800
resourceUri: resourceUri,
798801
error: e.message,

packages/altimate-code/src/session/compaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export namespace SessionCompaction {
164164
}) {
165165
const attempt = (compactionAttempts.get(input.sessionID) ?? 0) + 1
166166
compactionAttempts.set(input.sessionID, attempt)
167+
input.abort.addEventListener("abort", () => {
168+
compactionAttempts.delete(input.sessionID)
169+
}, { once: true })
167170
Telemetry.track({
168171
type: "compaction_triggered",
169172
timestamp: Date.now(),

packages/altimate-code/src/telemetry/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,13 @@ export namespace Telemetry {
474474
log.debug("telemetry flush failed", { status: response.status })
475475
}
476476
} catch {
477-
// Silently drop on failure — telemetry must never break the CLI
477+
// Re-add events that haven't been retried yet to avoid data loss
478+
const retriable = events.filter((e) => !(e as any)._retried)
479+
for (const e of retriable) {
480+
;(e as any)._retried = true
481+
}
482+
const space = Math.max(0, MAX_BUFFER_SIZE - buffer.length)
483+
buffer.unshift(...retriable.slice(0, space))
478484
} finally {
479485
clearTimeout(timeout)
480486
}

0 commit comments

Comments
 (0)