Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/disable-session-title-egress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@moonshot-ai/kimi-code': minor
---

Session titles are generated locally. The `chat_title` request, which sent an
excerpt of the conversation to the managed platform purely to produce a
display string, is no longer made.

Sessions are still titled: the first prompt already supplies a local title,
and that text is run through the secret redactor first.
22 changes: 22 additions & 0 deletions HARDENING.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,25 @@ rationale. Line numbers are from the commit that introduced the note and will dr
**`staticEnableError: REMOTE_CONTROL_DISABLED_MESSAGE,`**

> Upstream sets this only for a non-loopback bind or `--dangerous-bypass-auth`, which leaves `POST /api/v1/remote-control {"enabled":true}` working for a plain `kimi web`: anything holding the local server token could start a public tunnel with no terminal interaction and no second confirmation. Making it unconditional turns that route into a clean refusal.

### `packages/oauth/src/managed-tools.ts`

**`export async function fetchChatTitle(`**

> Session-title generation is disabled in this fork.
>
> `chat_title` posts an excerpt of the conversation, the user's prompt and the assistant's reply, to the managed platform purely to produce a display string for the session list. It is the only call in the product that sends conversation content anywhere other than the configured model provider, so with a third-party model backend it is a second, unrelated destination for the same text.
>
> Upstream gated this behind an `auto_session_title` experimental flag. That flag no longer exists anywhere in the tree, and the bundled web UI fires the request unprompted on the first turn, so there was nothing left to turn off.
>
> Nothing is lost by refusing. `applyPromptMetadataUpdate` already sets a `replaceable` title locally from the first prompt via `titleFromPromptMetadataText`, and that text passes through the secret redactor first. Sessions stay titled; the title is a truncated prompt rather than a generated phrase.
>
> This is the chokepoint: it is the only function that sends `chat_title`, so refusing here also covers any caller a later upstream merge introduces. Upstream's implementation is kept as `fetchChatTitleRemote`, unreachable, so its tests keep running and upstream changes still merge cleanly.

### `packages/agent-core-v2/src/session/sessionTitle/sessionTitleService.ts`

**`private async generateAndApply(`**

> A second gate in front of the network seal, so the disabled path does no provider lookup and never asks the OAuth token provider for an access token. Upstream's body is kept as `generateAndApplyRemote`, unreachable.
>
> `composeTitleInput` is exported so its budget and digest-elision behaviour stays under test as a pure function, rather than being asserted through a request body that is no longer sent.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
OAuthError,
fetchChatTitle,
kimiCodeToolsUrl,
SESSION_TITLE_EGRESS_DISABLED_MESSAGE,
parseKimiCodeCustomHeaders,
resolveKimiCodeRuntimeAuth,
} from '@moonshot-ai/kimi-code-oauth';
Expand Down Expand Up @@ -88,6 +89,14 @@ export class SessionTitleService implements ISessionTitleService {
}

private async generateAndApply(
_chatContent: string,
_force: boolean,
): Promise<string | undefined> {
this.log.debug(SESSION_TITLE_EGRESS_DISABLED_MESSAGE);
return undefined;
}

private async generateAndApplyRemote(
chatContent: string,
force: boolean,
): Promise<string | undefined> {
Expand Down Expand Up @@ -166,7 +175,7 @@ function titleInputFromPrompts(prompts: readonly string[]): string | undefined {
.slice(0, MAX_TITLE_INPUT_LENGTH);
}

async function composeTitleInput(
export async function composeTitleInput(
promptSource: IAgentTitlePromptSource,
source: SessionTitleSource,
): Promise<string | undefined> {
Expand Down
Loading
Loading