From 5a475ce15cffd1cb5d3ce40047d4cf9cbe7d1303 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:33:56 +0200 Subject: [PATCH 01/14] ci: restrict ci workflow token permissions --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eebcc4d..cc84ee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest From 8e42415cbc77b0a6d34bcf9c9877c9d202b002e7 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:34:45 +0200 Subject: [PATCH 02/14] ci: pin ci workflow actions --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc84ee0..31e0567 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 22 cache: npm From e2c4b88517cd2da82a5a1a6b902bd2229a46efbd Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:34:59 +0200 Subject: [PATCH 03/14] ci: disable checkout credential persistence --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31e0567..ffe1f40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 22 From daa6ed4a13ad4a65e1f816241be34abe342a0476 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:35:16 +0200 Subject: [PATCH 04/14] ci: ignore install scripts in ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffe1f40..3afb70b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,5 +19,5 @@ jobs: with: node-version: 22 cache: npm - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run ci From e5dac507854630792a7cdc88c78ea10b9a2aa057 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:35:35 +0200 Subject: [PATCH 05/14] ci: scope release workflow permissions --- .github/workflows/publish.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6f12894..cde9bb0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,13 +7,15 @@ on: concurrency: release permissions: - contents: write - pull-requests: write - id-token: write + contents: read jobs: release: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write steps: - uses: actions/checkout@v6 with: From 12f6c1643889574926f6165e40767c6c413c1986 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:35:53 +0200 Subject: [PATCH 06/14] ci: add release job timeout --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cde9bb0..a49fc01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,6 +12,7 @@ permissions: jobs: release: runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: write pull-requests: write From 15764f9376d419882230695217613d79ecbf134e Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:37:33 +0200 Subject: [PATCH 07/14] ci: pin release workflow actions --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a49fc01..0bdb98e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,11 +18,11 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 24 registry-url: https://registry.npmjs.org @@ -34,7 +34,7 @@ jobs: - run: npm run ci - name: Create version PR or publish - uses: changesets/action@v1 + uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b with: version: npm run version-packages publish: npm run publish-packages From a160005b05d56abd11f621ac4b772fd562401642 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:37:50 +0200 Subject: [PATCH 08/14] ci: disable release checkout credential persistence --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0bdb98e..5862215 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,6 +21,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: From 01e4995615040c3245046379cca5553634a6eb1c Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:38:05 +0200 Subject: [PATCH 09/14] ci: avoid install lifecycle scripts in release --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5862215..7fa06c2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,9 +29,8 @@ jobs: registry-url: https://registry.npmjs.org cache: npm - - run: npm install -g npm@latest - run: node --version && npm --version - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run ci - name: Create version PR or publish From 4f85c0ff91290a626728d90486a7da6537749742 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:38:54 +0200 Subject: [PATCH 10/14] ci: split release provenance from build --- .github/workflows/publish.yml | 21 ++++++++++++++++++++- package.json | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7fa06c2..d202b64 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,9 +10,29 @@ permissions: contents: read jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 22 + cache: npm + + - run: node --version && npm --version + - run: npm ci --ignore-scripts + - run: npm run ci + release: runs-on: ubuntu-latest timeout-minutes: 20 + needs: verify permissions: contents: write pull-requests: write @@ -31,7 +51,6 @@ jobs: - run: node --version && npm --version - run: npm ci --ignore-scripts - - run: npm run ci - name: Create version PR or publish uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b diff --git a/package.json b/package.json index 41f37dc..e7a4425 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "ci": "npm run typecheck && npm test && npm run compat:codegraph && npm pack --dry-run", "compat:codegraph": "codegraph --version", "local-release": "changeset version && changeset publish", - "publish-packages": "node -e \"const {execSync}=require('node:child_process'); const p=require('./package.json'); const spec=p.name+'@'+p.version; try { execSync('npm view '+spec+' version', {stdio:'ignore'}); console.log(spec+' already published; skipping.'); } catch { execSync('npm publish --access public --provenance', {stdio:'inherit'}); }\"", + "publish-packages": "node -e \"const {execSync}=require('node:child_process'); const p=require('./package.json'); const spec=p.name+'@'+p.version; try { execSync('npm view '+spec+' version', {stdio:'ignore'}); console.log(spec+' already published; skipping.'); } catch { execSync('npm publish --access public --provenance --ignore-scripts', {stdio:'inherit'}); }\"", "prepack": "npm run typecheck && npm test", "prepublishOnly": "npm run ci", "typecheck": "tsc --noEmit", From e60a8843d0654b5d78e5f8bdf2e5a9f1a5cff9f0 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:39:44 +0200 Subject: [PATCH 11/14] quality: add skylos gate policy --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..23e98c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.skylos] +complexity = 10 +nesting = 3 +max_args = 5 +max_lines = 50 +ignore = [] + +[tool.skylos.languages.typescript] +complexity = 15 +nesting = 4 + +[tool.skylos.gate] +fail_on_critical = true +max_security = 0 +max_quality = 0 +strict = true From 1026608a1f0a4caf15707564c0b83163683d801b Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:40:01 +0200 Subject: [PATCH 12/14] quality: add pre-commit policy --- .pre-commit-config.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d253251 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: local + hooks: + - id: npm-ci + name: npm run ci + entry: npm run ci + language: system + pass_filenames: false From 9862064a6cec57ff05c06e3ba200e53839116e31 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:41:19 +0200 Subject: [PATCH 13/14] quality: split json rpc session helpers --- extensions/codegraph.ts | 185 +++++++++++++++++++++++++--------------- 1 file changed, 116 insertions(+), 69 deletions(-) diff --git a/extensions/codegraph.ts b/extensions/codegraph.ts index 906622b..c293c85 100644 --- a/extensions/codegraph.ts +++ b/extensions/codegraph.ts @@ -135,6 +135,10 @@ const ToolDefinitions = [ type ToolName = (typeof ToolDefinitions)[number]["name"]; type ToolParams = Record & { projectPath?: string }; type JsonRpcRequest = (method: string, params: Record) => Promise; +type PendingJsonRpcRequests = Map void; + reject: (error: Error) => void; +}>; const MaxDiagnosticLength = 1000; @@ -194,67 +198,106 @@ async function runJsonRpcSession( signal: AbortSignal | undefined, fn: (request: JsonRpcRequest) => Promise, ): Promise { - let nextId = 1; - let stdout = ""; - let stderr = ""; - const pending = new Map void; - reject: (error: Error) => void; - }>(); - - const cleanup = () => { - for (const entry of pending.values()) { - entry.reject(new Error("CodeGraph MCP process closed before responding.")); - } - pending.clear(); - if (!child.killed) child.kill(); - }; - + const pending: PendingJsonRpcRequests = new Map(); + const stderr = { value: "" }; + const cleanup = () => cleanupJsonRpcChild(child, pending); const onAbort = () => cleanup(); + signal?.addEventListener("abort", onAbort, { once: true }); + attachJsonRpcHandlers(child, pending, stderr); + + try { + const sendRequest = createJsonRpcRequestSender(child, pending); + await initializeJsonRpcSession(cwd, sendRequest, sendJsonRpcNotification.bind(undefined, child)); + return await fn(sendRequest); + } finally { + signal?.removeEventListener("abort", onAbort); + cleanup(); + } +} + +function cleanupJsonRpcChild( + child: ChildProcessWithoutNullStreams, + pending: PendingJsonRpcRequests, +): void { + rejectPendingJsonRpcRequests( + pending, + new Error("CodeGraph MCP process closed before responding."), + ); + if (!child.killed) child.kill(); +} + +function rejectPendingJsonRpcRequests( + pending: PendingJsonRpcRequests, + error: Error, +): void { + for (const entry of pending.values()) entry.reject(error); + pending.clear(); +} + +function attachJsonRpcHandlers( + child: ChildProcessWithoutNullStreams, + pending: PendingJsonRpcRequests, + stderr: { value: string }, +): void { + const stdout = { value: "" }; child.stdout.on("data", (chunk) => { - stdout += chunk.toString("utf-8"); - let newline; - while ((newline = stdout.indexOf("\n")) !== -1) { - const line = stdout.slice(0, newline).trim(); - stdout = stdout.slice(newline + 1); - if (!line) continue; - - let msg: any; - try { - msg = JSON.parse(line); - } catch { - continue; - } - - if (msg.id !== undefined && pending.has(msg.id)) { - const { resolve, reject } = pending.get(msg.id)!; - pending.delete(msg.id); - if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error))); - else resolve(msg.result); - } - } + handleJsonRpcStdout(chunk, stdout, pending); }); - child.stderr.on("data", (chunk) => { - stderr += chunk.toString("utf-8"); + stderr.value += chunk.toString("utf-8"); }); + child.on("error", (err) => rejectPendingJsonRpcRequests(pending, err)); + child.on("exit", (code) => rejectPendingJsonRpcOnExit(pending, stderr.value, code)); +} - child.on("error", (err) => { - for (const entry of pending.values()) entry.reject(err); - pending.clear(); - }); +function handleJsonRpcStdout( + chunk: Buffer, + stdout: { value: string }, + pending: PendingJsonRpcRequests, +): void { + stdout.value += chunk.toString("utf-8"); + let newline; + while ((newline = stdout.value.indexOf("\n")) !== -1) { + const line = stdout.value.slice(0, newline).trim(); + stdout.value = stdout.value.slice(newline + 1); + if (line) resolveJsonRpcLine(line, pending); + } +} - child.on("exit", (code) => { - if (pending.size === 0) return; - const diagnostic = sanitizeDiagnostic(stderr.trim()); - const msg = diagnostic || `CodeGraph MCP process exited with code ${code}`; - for (const entry of pending.values()) entry.reject(new Error(msg)); - pending.clear(); - }); +function resolveJsonRpcLine(line: string, pending: PendingJsonRpcRequests): void { + let msg: any; + try { + msg = JSON.parse(line); + } catch { + return; + } + + if (msg.id === undefined || !pending.has(msg.id)) return; + const { resolve, reject } = pending.get(msg.id)!; + pending.delete(msg.id); + if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error))); + else resolve(msg.result); +} + +function rejectPendingJsonRpcOnExit( + pending: PendingJsonRpcRequests, + stderr: string, + code: number | null, +): void { + if (pending.size === 0) return; + const diagnostic = sanitizeDiagnostic(stderr.trim()); + const msg = diagnostic || `CodeGraph MCP process exited with code ${code}`; + rejectPendingJsonRpcRequests(pending, new Error(msg)); +} - const sendRequest: JsonRpcRequest = (method, params) => { +function createJsonRpcRequestSender( + child: ChildProcessWithoutNullStreams, + pending: PendingJsonRpcRequests, +): JsonRpcRequest { + let nextId = 1; + return (method, params) => { const id = nextId++; const payload = { jsonrpc: "2.0", id, method, params }; const promise = new Promise((resolve, reject) => { @@ -263,26 +306,30 @@ async function runJsonRpcSession( child.stdin.write(`${JSON.stringify(payload)}\n`); return promise; }; +} - const sendNotification = (method: string, params: Record) => { - child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); - }; +function sendJsonRpcNotification( + child: ChildProcessWithoutNullStreams, + method: string, + params: Record, +): void { + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); +} - try { - const rootUri = pathToFileURL(cwd).href; - await sendRequest("initialize", { - protocolVersion: "2024-11-05", - rootUri, - workspaceFolders: [{ uri: rootUri, name: cwd.split(/[\\/]/).pop() || cwd }], - capabilities: {}, - clientInfo: { name: "pi-codegraph", version: "0.1.0" }, - }); - sendNotification("initialized", {}); - return await fn(sendRequest); - } finally { - signal?.removeEventListener("abort", onAbort); - cleanup(); - } +async function initializeJsonRpcSession( + cwd: string, + sendRequest: JsonRpcRequest, + sendNotification: (method: string, params: Record) => void, +): Promise { + const rootUri = pathToFileURL(cwd).href; + await sendRequest("initialize", { + protocolVersion: "2024-11-05", + rootUri, + workspaceFolders: [{ uri: rootUri, name: cwd.split(/[\\/]/).pop() || cwd }], + capabilities: {}, + clientInfo: { name: "pi-codegraph", version: "0.1.0" }, + }); + sendNotification("initialized", {}); } export async function callCodeGraphTool( From 7162965c3135a39187371f4f2b0a7b062646dd29 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Sun, 31 May 2026 13:42:07 +0200 Subject: [PATCH 14/14] security: suppress npm lockfile integrity false positives --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 23e98c2..edb2a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ nesting = 3 max_args = 5 max_lines = 50 ignore = [] +exclude = ["package-lock.json"] [tool.skylos.languages.typescript] complexity = 15