Skip to content

Commit 395d537

Browse files
authored
feat(kimi-code): summarize workspace trust with configuration sources (#4056)
* feat(kimi-code): show what trusting activates in the workspace trust prompt * fix(kimi-code): keep trust prompt non-blocking and disclose only external additional dirs * fix(kimi-code): mark trust disclosure incomplete when discovery times out or fails * fix(kimi-code): exclude disabled MCP servers from trust disclosure and cap key lists * fix(kimi-code): disclose disabled user MCP servers shadowed by project config * test(kimi-code): merge redundant trust prompt disclosure cases * test(node-sdk): merge mirrored user/project MCP override cases * Update trust-panel-disclosure.md Signed-off-by: 7Sageer <sag77r@hotmail.com> * fix(kimi-code): disclose symlinked additional dirs by real target in the trust prompt * fix(kimi-code): bound trust disclosure I/O and harden path containment * fix(kimi-code): tighten trust prompt disclosure copy * refactor(kimi-code): compute the trust disclosure inside the engine The disclosure composition (project-origin MCP servers, additional dirs with realpath containment, instruction sources) moves from the SDK into agent-core-v2's workspace scope as WorkspaceTrustDisclosureService, constructed alongside the other generation services. The SDK's getWorkspaceTrustInfo becomes a thin forward, so the config-merge semantics live next to the workspaceTrust gate that owns them instead of being re-derived downstream. Also drops the shadowed-user-server section and the per-section deadlines with their disclosureComplete flag: the engine reads the same loaded state session creation awaits, so the prompt needs no timeout plumbing of its own, and the empty state stays a single honest copy. * refactor(kimi-code): compress trust disclosure tests and disclose only effective profiles Merge mirrored test cases across the three suites and share fixtures; small source trims (shared wrap helper, context.workspaceId). The disclosed agent profiles now pass the same resolution the session catalog applies: a workspace profile clashing with a builtin without override: true, or losing to a higher-priority candidate, is no longer reported as activated. * test(kimi-code): drop the engine disclosure suite in favor of e2e coverage The SDK e2e suite exercises the disclosure through the real engine, so the stub-based engine unit file duplicated it; a builtin-clash agent case moves there to keep the suppressed-profile behavior pinned. Also folds the sanitize vectors into the MCP render case and trims the trust type docs. * fix(kimi-code): cap untrusted MCP target fields and correct profile wording A crafted .mcp.json can supply arbitrarily long command/args/url strings; each field now truncates at 80 chars before wrapping. The profiles line no longer claims they feed the agent — a non-default workspace profile is only made available as a subagent, the default agent still binds the default profile. * fix(kimi-code): honor profile overrides and reveal instruction symlinks in trust disclosure A workspace profile with override: true replaces a same-name builtin or higher-priority candidate in the session catalog, so the disclosure now applies the same replace semantics instead of dropping it. Instruction paths get the same realpath treatment as additional dirs: a project AGENTS.md symlinked to a file outside the project renders its real target instead of hiding behind the lexical path. * fix(kimi-code): disclose skill roots escaping the project in the trust prompt Read/Write/Edit/Grep/Glob all add getSkillRoots() to their allowed roots, so a .kimi-code/skills symlink to an external directory is an outside-project file access grant that trusting activates. The disclosure now merges those roots (resolved via the same projectRoots() the skill source uses) into the outside-directories list. Also caps each displayed MCP key name, not just the key count. * fix(kimi-code): cap the bearer-token env var label in the trust prompt * refactor(kimi-code): disclose only real locations in the trust prompt The path → realPath arrow is dropped; gated directories and AGENTS.md entries are disclosed as their resolved real locations, and the pair type collapses back to plain strings. Entries resolving to the same real directory collapse into one. * fix(kimi-code): tighten the trust prompt list caps to fit one screen * fix(kimi-code): lower the trust prompt list caps further * fix(agent-core-v2): keep explicit agent-file precedence in the trust disclosure * Revert "fix(agent-core-v2): keep explicit agent-file precedence in the trust disclosure" This reverts commit 6e5c45d. * fix(agent-core-v2): preserve profile priority in trust disclosure * fix(kimi-code): keep workspace trust choices visible * fix(kimi-code): align trust prompt paging keys * docs: remove trust prompt keyboard additions * feat(kimi-code): summarize workspace trust with configuration sources * chore(agent-core-v2): sync workspace instruction state manifest * fix(kimi-code): recognize dot-prefixed project directories in trust summary * refactor(kimi-code): reuse existing trust configuration sources * fix(kimi-code): keep trust disclosure independent of skill watchers * refactor(kimi-code): condense additional directory disclosure --------- Signed-off-by: 7Sageer <sag77r@hotmail.com>
1 parent e3bf50c commit 395d537

12 files changed

Lines changed: 909 additions & 213 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Replace inline MCP commands in the workspace trust prompt with activation summaries and configuration paths for manual inspection.

‎apps/kimi-code/src/tui/components/dialogs/trust-prompt.ts‎

Lines changed: 153 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,32 @@ import {
66
type Component,
77
type Focusable,
88
} from '@moonshot-ai/pi-tui';
9-
10-
import type { WorkspaceTrustMcpServerInfo } from '@moonshot-ai/kimi-code-sdk';
9+
import type { WorkspaceTrustInfo } from '@moonshot-ai/kimi-code-sdk';
1110

1211
import { SELECT_POINTER } from '#/tui/constant/symbols';
13-
import { currentTheme } from '#/tui/theme';
12+
import { currentTheme, type ColorToken } from '#/tui/theme';
13+
import { pageView } from '#/tui/utils/paging';
1414

1515
export type TrustPromptChoice = 'trust' | 'distrust';
1616

1717
export interface TrustPromptOptions {
1818
readonly workDir: string;
19-
/** Project-level MCP servers that trusting would enable; may be empty. */
20-
readonly gatedMcpServers: readonly WorkspaceTrustMcpServerInfo[];
21-
/** Esc resolves to 'distrust' as well. */
19+
readonly info: WorkspaceTrustInfo;
20+
readonly getAvailableRows?: () => number;
2221
readonly onSelect: (choice: TrustPromptChoice) => void;
2322
}
2423

25-
interface TrustPromptOption {
26-
readonly value: TrustPromptChoice;
27-
readonly label: string;
28-
readonly description: string;
29-
}
30-
31-
const OPTIONS: readonly TrustPromptOption[] = [
32-
{
33-
value: 'trust',
34-
label: 'Trust this folder',
35-
description: 'Enable project MCP servers. Remembered for this folder.',
36-
},
37-
{
38-
value: 'distrust',
39-
label: "Don't trust",
40-
description: 'Exit Kimi Code. Asked again next launch.',
41-
},
24+
const OPTIONS: readonly { value: TrustPromptChoice; label: string }[] = [
25+
{ value: 'trust', label: 'Trust and continue' },
26+
{ value: 'distrust', label: 'Exit' },
4227
];
4328

4429
export class TrustPromptComponent implements Component, Focusable {
4530
focused = false;
4631
private selectedIndex = 0;
32+
private disclosureIndex = 0;
33+
private disclosurePageSize = 1;
34+
private canConfirm = true;
4735

4836
constructor(private readonly opts: TrustPromptOptions) {}
4937

@@ -62,73 +50,171 @@ export class TrustPromptComponent implements Component, Focusable {
6250
this.selectedIndex = Math.min(OPTIONS.length - 1, this.selectedIndex + 1);
6351
return;
6452
}
65-
if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
53+
const previousPage = matchesKey(data, Key.left) || matchesKey(data, Key.pageUp);
54+
const nextPage = matchesKey(data, Key.right) || matchesKey(data, Key.pageDown);
55+
if (previousPage || nextPage) {
56+
this.disclosureIndex = Math.max(
57+
0,
58+
this.disclosureIndex + (previousPage ? -1 : 1) * this.disclosurePageSize,
59+
);
60+
return;
61+
}
62+
if (this.canConfirm && (matchesKey(data, Key.enter) || matchesKey(data, Key.space))) {
6663
this.opts.onSelect(OPTIONS[this.selectedIndex]!.value);
6764
}
6865
}
6966

7067
render(width: number): string[] {
7168
const rule = currentTheme.fg('primary', '─'.repeat(width));
72-
const lines = [
69+
const availableRows = Math.max(0, Math.floor(this.opts.getAvailableRows?.() ?? Infinity));
70+
const header = [
7371
rule,
7472
currentTheme.boldFg('primary', ' Trust this folder?'),
7573
currentTheme.fg('textMuted', ' ↑↓ navigate · Enter select · Esc exit'),
7674
'',
77-
...wrapTextWithAnsi(this.opts.workDir, Math.max(20, width - 2)).map(
78-
(line) => ` ${currentTheme.fg('textStrong', line)}`,
75+
];
76+
const body = [
77+
...wrap(this.opts.workDir, 1, width, 'textStrong'),
78+
'',
79+
...this.renderDisclosure(width),
80+
];
81+
const footer = [
82+
...wrap(
83+
'Trust is remembered for this folder, including future project config changes.',
84+
1,
85+
width,
86+
'textMuted',
7987
),
88+
...wrap('Tool approvals follow your permission settings.', 1, width, 'textMuted'),
8089
'',
90+
...OPTIONS.map((option, i) => {
91+
const selected = i === this.selectedIndex;
92+
const pointer = selected ? SELECT_POINTER : ' ';
93+
const label = selected
94+
? currentTheme.boldFg('primary', option.label)
95+
: currentTheme.fg('text', option.label);
96+
return currentTheme.fg(selected ? 'primary' : 'textDim', ` ${pointer} `) + label;
97+
}),
98+
rule,
8199
];
82-
83-
const notice =
84-
'Project-level MCP servers are disabled until you explicitly choose Trust. Trust starts the listed project MCP targets and remembers this folder.';
85-
for (const line of wrapTextWithAnsi(notice, Math.max(20, width - 2))) {
86-
lines.push(` ${currentTheme.fg('textMuted', line)}`);
100+
this.canConfirm = header.length + footer.length + 2 <= availableRows;
101+
if (!this.canConfirm) {
102+
return [header[1]!, ' Enlarge terminal to review sources. Esc exit.']
103+
.slice(0, availableRows)
104+
.map((line) => truncateToWidth(line, width));
87105
}
88-
if (this.opts.gatedMcpServers.length > 0) {
89-
lines.push(` ${currentTheme.fg('warning', 'Project MCP targets:')}`);
90-
for (const server of this.opts.gatedMcpServers) {
91-
const details = formatMcpTarget(server);
92-
for (const line of wrapTextWithAnsi(details, Math.max(20, width - 4))) {
93-
lines.push(` ${currentTheme.fg('warning', line)}`);
94-
}
95-
}
96-
}
97-
lines.push('');
106+
const needsPaging = header.length + body.length + footer.length > availableRows;
107+
this.disclosurePageSize = needsPaging
108+
? availableRows - header.length - footer.length - 1
109+
: body.length;
110+
const page = pageView(body.length, this.disclosureIndex, this.disclosurePageSize);
111+
this.disclosureIndex = page.start;
112+
const lines = [...header, ...body.slice(page.start, page.end)];
113+
while (lines.length < header.length + this.disclosurePageSize) lines.push('');
114+
if (page.pageCount > 1)
115+
lines.push(currentTheme.fg('textMuted', ` ←→ page · ${page.page + 1} / ${page.pageCount}`));
116+
lines.push(...footer);
117+
return lines.map((line) => truncateToWidth(line, width));
118+
}
98119

99-
for (let i = 0; i < OPTIONS.length; i += 1) {
100-
const option = OPTIONS[i]!;
101-
const selected = i === this.selectedIndex;
102-
const pointer = selected ? SELECT_POINTER : ' ';
103-
const label = selected
104-
? currentTheme.boldFg('primary', option.label)
105-
: currentTheme.fg('text', option.label);
106-
lines.push(currentTheme.fg(selected ? 'primary' : 'textDim', ` ${pointer} `) + label);
107-
for (const line of wrapTextWithAnsi(option.description, Math.max(20, width - 4))) {
108-
lines.push(` ${currentTheme.fg('textMuted', line)}`);
120+
private renderDisclosure(width: number): string[] {
121+
const {
122+
gatedMcpServers,
123+
gatedAdditionalDirs,
124+
additionalDirSources,
125+
instructionSources,
126+
warnings,
127+
} = this.opts.info;
128+
const lines: string[] = [];
129+
if (gatedMcpServers.length > 0) {
130+
lines.push(
131+
...wrap(
132+
`Start ${gatedMcpServers.length} MCP ${
133+
gatedMcpServers.length === 1 ? 'server' : 'servers'
134+
} automatically`,
135+
1,
136+
width,
137+
'warning',
138+
),
139+
);
140+
const origins = [...new Set(gatedMcpServers.map((server) => server.origin))];
141+
lines.push(
142+
...wrap(
143+
`Config: ${origins.map((path) => relativize(this.opts.workDir, path)).join(', ')}`,
144+
3,
145+
width,
146+
'textMuted',
147+
),
148+
'',
149+
);
150+
}
151+
if (gatedAdditionalDirs.length > 0) {
152+
lines.push(
153+
...wrap(
154+
`Access ${gatedAdditionalDirs.length} ${
155+
gatedAdditionalDirs.length === 1 ? 'folder' : 'folders'
156+
} outside this project`,
157+
1,
158+
width,
159+
'warning',
160+
),
161+
);
162+
if (additionalDirSources.length > 0) {
163+
lines.push(
164+
...wrap(
165+
`Config: ${additionalDirSources
166+
.map((path) => relativize(this.opts.workDir, path))
167+
.join(', ')}`,
168+
3,
169+
width,
170+
'textMuted',
171+
),
172+
);
109173
}
110174
lines.push('');
111175
}
112-
113-
lines.push(rule);
114-
return lines.map((line) => truncateToWidth(line, width));
176+
if (instructionSources.paths.length > 0) {
177+
const hasInstructions =
178+
instructionSources.agentsMdPaths.length > 0 || instructionSources.skills.length > 0;
179+
const subject = hasInstructions
180+
? instructionSources.agentProfiles.length > 0
181+
? 'instructions and agent profiles'
182+
: 'instructions'
183+
: 'agent profiles';
184+
lines.push(...wrap(`Load project ${subject}`, 1, width, 'text'));
185+
lines.push(
186+
...wrap(
187+
`Check: ${instructionSources.paths.map((path) => relativize(this.opts.workDir, path)).join(' · ')}`,
188+
3,
189+
width,
190+
'textMuted',
191+
),
192+
'',
193+
);
194+
}
195+
for (const warning of warnings) lines.push(...wrap(warning, 1, width, 'warning'));
196+
if (lines.length === 0)
197+
lines.push(
198+
...wrap('No project integrations or instructions to activate.', 1, width, 'textMuted'),
199+
'',
200+
);
201+
return lines;
115202
}
116203
}
117204

118-
function formatMcpTarget(server: WorkspaceTrustMcpServerInfo): string {
119-
if (server.transport === 'stdio') {
120-
const args = server.args === undefined ? '' : ` args=${JSON.stringify(server.args)}`;
121-
const cwd = server.cwd === undefined ? '' : ` cwd=${server.cwd}`;
122-
return sanitizeForDisplay(`${server.name} (stdio): command=${server.command ?? ''}${args}${cwd}`);
123-
}
124-
return sanitizeForDisplay(`${server.name} (${server.transport}): url=${server.url ?? ''}`);
205+
function wrap(text: string, indent: number, width: number, color: ColorToken): string[] {
206+
return wrapTextWithAnsi(sanitizeForDisplay(text), Math.max(1, width - indent)).map(
207+
(line) => `${' '.repeat(indent)}${currentTheme.fg(color, line)}`,
208+
);
209+
}
210+
211+
function relativize(workDir: string, path: string): string {
212+
const normalizedDir = workDir.replaceAll('\\', '/');
213+
const normalizedPath = path.replaceAll('\\', '/');
214+
const prefix = normalizedDir.endsWith('/') ? normalizedDir : `${normalizedDir}/`;
215+
return normalizedPath.startsWith(prefix) ? normalizedPath.slice(prefix.length) : normalizedPath;
125216
}
126217

127-
/**
128-
* Drops C0/C1 control characters (including ESC) from workspace-supplied text:
129-
* the trust prompt renders before the workspace is trusted, so a planted
130-
* `.mcp.json` must not inject terminal control sequences into it.
131-
*/
132218
function sanitizeForDisplay(value: string): string {
133219
let result = '';
134220
for (const char of value) {

‎apps/kimi-code/src/tui/kimi-tui.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3888,7 +3888,14 @@ export class KimiTUI {
38883888
try {
38893889
info = await this.harness.getWorkspaceTrustInfo(workDir);
38903890
} catch {
3891-
info = { trusted: false, gatedMcpServers: [] };
3891+
info = {
3892+
trusted: false,
3893+
gatedMcpServers: [],
3894+
gatedAdditionalDirs: [],
3895+
additionalDirSources: [],
3896+
warnings: ['Could not inspect project settings.'],
3897+
instructionSources: { agentsMdPaths: [], skills: [], agentProfiles: [], paths: [] },
3898+
};
38923899
}
38933900
if (info.trusted) {
38943901
return false;
@@ -3899,7 +3906,9 @@ export class KimiTUI {
38993906
this.mountEditorReplacement(
39003907
new TrustPromptComponent({
39013908
workDir,
3902-
gatedMcpServers: info.gatedMcpServers,
3909+
info,
3910+
getAvailableRows: () =>
3911+
this.state.terminal.rows - (this.state.ui instanceof TuiAltScreen ? 1 : 0),
39033912
onSelect: (c) => {
39043913
resolve(c);
39053914
},

0 commit comments

Comments
 (0)