mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(vscode): fail closed for next edit file access
This commit is contained in:
@@ -116,10 +116,11 @@ export class AutocompleteServiceManager {
|
||||
getRecentlyViewedSnippets: () => {
|
||||
// Reuse the LRU populated by the classic provider — keeps a single
|
||||
// RecentlyVisitedRangesService instance instead of double-tracking.
|
||||
// Snippets are filtered against the ignore controller before sending.
|
||||
// Suppress snippets until access checks are available, then include
|
||||
// only content explicitly approved by the ignore controller.
|
||||
const raw = this.inlineCompletionProvider.recentlyVisitedRangesService.getSnippets()
|
||||
const ignore = this.ignoreControllerSync
|
||||
const allowed = ignore ? raw.filter((s) => ignore.validateAccess(s.filepath)) : raw
|
||||
const allowed = ignore ? raw.filter((s) => ignore.validateAccess(s.filepath)) : []
|
||||
return toMercuryRecentSnippets(allowed)
|
||||
},
|
||||
onFatalError: (status) => this.handleFatalAutocompleteError(status),
|
||||
|
||||
+9
-3
@@ -16,7 +16,7 @@ export interface NextEditProviderDeps {
|
||||
/** Optional source of recently-viewed snippets (kilocode's VisibleCodeTracker can adapt to this). */
|
||||
getRecentlyViewedSnippets?: (document: vscode.TextDocument) => MercuryRecentSnippet[]
|
||||
/** Returns false for files that must not be sent to a server (.env etc). */
|
||||
isFileAllowed?: (fsPath: string) => Promise<boolean>
|
||||
isFileAllowed: (fsPath: string) => Promise<boolean>
|
||||
/** Telemetry hook fired on every suggestion result. */
|
||||
onSuggestion?: (event: NextEditSuggestionEvent) => void
|
||||
onFatalError?: (status: number | null) => void
|
||||
@@ -67,8 +67,8 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
if (document.uri.scheme !== "file") return undefined
|
||||
if (this.deps.suggestionManager?.isPending()) return undefined
|
||||
|
||||
// Never send an ignored file (.env, secrets, etc.) to the model.
|
||||
if (this.deps.isFileAllowed && !(await this.deps.isFileAllowed(document.uri.fsPath))) return undefined
|
||||
// Never send a file unless the access policy explicitly approves it.
|
||||
if (!(await this.allowed(document.uri.fsPath))) return undefined
|
||||
|
||||
const isExplicit = context.triggerKind === vscode.InlineCompletionTriggerKind.Invoke
|
||||
if (!isExplicit) {
|
||||
@@ -95,6 +95,12 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
}
|
||||
}
|
||||
|
||||
private async allowed(path: string): Promise<boolean> {
|
||||
const allow = this.deps.isFileAllowed
|
||||
if (!allow) return false
|
||||
return allow(path).catch(() => false)
|
||||
}
|
||||
|
||||
private swapAbortController(token: vscode.CancellationToken): AbortController {
|
||||
this.currentAbort?.abort()
|
||||
const abort = new AbortController()
|
||||
|
||||
+40
-1
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import type { KiloConnectionService } from "../../../cli-backend"
|
||||
import { NextEditInlineCompletionProvider } from "../NextEditInlineCompletionProvider"
|
||||
import { NextEditInlineCompletionProvider, type NextEditProviderDeps } from "../NextEditInlineCompletionProvider"
|
||||
import type { NextEditSuggestionManager } from "../NextEditSuggestionManager"
|
||||
|
||||
vi.mock("vscode", () => {
|
||||
@@ -55,14 +55,51 @@ function doc(text: string): vscode.TextDocument {
|
||||
range: { end: new vscode.Position(line, lines[line].length) },
|
||||
}),
|
||||
getText: () => text,
|
||||
uri: { fsPath: "/workspace/test.ts", scheme: "file" },
|
||||
} as unknown as vscode.TextDocument
|
||||
}
|
||||
|
||||
describe("NextEditInlineCompletionProvider", () => {
|
||||
it("does not send a document when the access policy is missing at runtime", async () => {
|
||||
const connection = { getClientAsync: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({ connectionService: connection } as unknown as NextEditProviderDeps)
|
||||
|
||||
const out = await provider.provideInlineCompletionItems(
|
||||
doc("const value = 1"),
|
||||
new vscode.Position(0, 0),
|
||||
{} as vscode.InlineCompletionContext,
|
||||
{} as vscode.CancellationToken,
|
||||
)
|
||||
|
||||
expect(out).toBeUndefined()
|
||||
expect(connection.getClientAsync).not.toHaveBeenCalled()
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
it("does not send a document when the access policy fails", async () => {
|
||||
const connection = { getClientAsync: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: connection as unknown as KiloConnectionService,
|
||||
isFileAllowed: async () => Promise.reject(new Error("unavailable")),
|
||||
})
|
||||
|
||||
const out = await provider.provideInlineCompletionItems(
|
||||
doc("const value = 1"),
|
||||
new vscode.Position(0, 0),
|
||||
{} as vscode.InlineCompletionContext,
|
||||
{} as vscode.CancellationToken,
|
||||
)
|
||||
|
||||
expect(out).toBeUndefined()
|
||||
expect(connection.getClientAsync).not.toHaveBeenCalled()
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
it("stashes same-line rewrites before the cursor for decorated acceptance", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: {} as KiloConnectionService,
|
||||
isFileAllowed: async () => true,
|
||||
suggestionManager: mgr as unknown as NextEditSuggestionManager,
|
||||
})
|
||||
const text = "const oldName = make()"
|
||||
@@ -90,6 +127,7 @@ describe("NextEditInlineCompletionProvider", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: {} as KiloConnectionService,
|
||||
isFileAllowed: async () => true,
|
||||
suggestionManager: mgr as unknown as NextEditSuggestionManager,
|
||||
})
|
||||
|
||||
@@ -111,6 +149,7 @@ describe("NextEditInlineCompletionProvider", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: {} as KiloConnectionService,
|
||||
isFileAllowed: async () => true,
|
||||
suggestionManager: mgr as unknown as NextEditSuggestionManager,
|
||||
})
|
||||
|
||||
|
||||
+17
-1
@@ -34,7 +34,7 @@ function doc(path: string, initial: string): Doc {
|
||||
|
||||
describe("EditHistoryTracker", () => {
|
||||
it("retains chronological edits across files for Mercury context", async () => {
|
||||
const tracker = new EditHistoryTracker()
|
||||
const tracker = new EditHistoryTracker({ isFileAllowed: async () => true })
|
||||
const a = doc("/workspace/a.ts", "const a = 1\n")
|
||||
const b = doc("/workspace/b.ts", "const b = 1\n")
|
||||
const open = (vscode.workspace as unknown as { open(doc: vscode.TextDocument): void }).open
|
||||
@@ -43,6 +43,7 @@ describe("EditHistoryTracker", () => {
|
||||
open(b)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
a.setText("const a = 2\n")
|
||||
await tracker.flush(a)
|
||||
b.setText("const b = 2\n")
|
||||
@@ -58,6 +59,21 @@ describe("EditHistoryTracker", () => {
|
||||
tracker.dispose()
|
||||
})
|
||||
|
||||
it("does not retain edits when the access policy is missing at runtime", async () => {
|
||||
const tracker = new EditHistoryTracker({} as { isFileAllowed: (path: string) => Promise<boolean> })
|
||||
const a = doc("/workspace/a.ts", "const a = 1\n")
|
||||
const open = (vscode.workspace as unknown as { open(doc: vscode.TextDocument): void }).open
|
||||
|
||||
open(a)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
a.setText("const a = 2\n")
|
||||
await tracker.flush(a)
|
||||
|
||||
expect(await tracker.getRecentDiffs()).toEqual([])
|
||||
tracker.dispose()
|
||||
})
|
||||
|
||||
it("never returns edits from denied documents", async () => {
|
||||
const denied = new Set(["/workspace/.env"])
|
||||
const tracker = new EditHistoryTracker({ isFileAllowed: async (path) => !denied.has(path) })
|
||||
|
||||
@@ -7,7 +7,7 @@ const DEFAULT_MAX_DIFFS = 5
|
||||
type Options = {
|
||||
debounceMs?: number
|
||||
maxDiffs?: number
|
||||
isFileAllowed?: (fsPath: string) => Promise<boolean>
|
||||
isFileAllowed: (fsPath: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
type Diff = {
|
||||
@@ -30,7 +30,7 @@ export class EditHistoryTracker implements vscode.Disposable {
|
||||
private readonly diffs: Diff[] = []
|
||||
private readonly subscriptions: vscode.Disposable[] = []
|
||||
|
||||
constructor(private readonly options: Options = {}) {
|
||||
constructor(private readonly options: Options) {
|
||||
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS
|
||||
|
||||
// Seed snapshots on open so the FIRST edit in a freshly-opened file is
|
||||
@@ -152,8 +152,9 @@ export class EditHistoryTracker implements vscode.Disposable {
|
||||
}
|
||||
|
||||
private async allowed(key: string): Promise<boolean> {
|
||||
if (!this.options.isFileAllowed) return true
|
||||
return this.options.isFileAllowed(key).catch(() => false)
|
||||
const allow = this.options.isFileAllowed
|
||||
if (!allow) return false
|
||||
return allow(key).catch(() => false)
|
||||
}
|
||||
|
||||
private reject(key: string): void {
|
||||
|
||||
Reference in New Issue
Block a user