Skip to content

fix(agent): isolate workspace baseline from trusted system prompt#2049

Open
Ram-Pavith wants to merge 4 commits into
MoonshotAI:mainfrom
Ram-Pavith:fix/system-prompt-baseline-isolation
Open

fix(agent): isolate workspace baseline from trusted system prompt#2049
Ram-Pavith wants to merge 4 commits into
MoonshotAI:mainfrom
Ram-Pavith:fix/system-prompt-baseline-isolation

Conversation

@Ram-Pavith

Copy link
Copy Markdown

Related Issue

Resolve #2048

Design reference (branch-local): docs/design/system-prompt-baseline-isolation.md
Issue draft: docs/design/issue-system-prompt-baseline-isolation.md

Problem

See linked issue. Short form:

Kimi's always-sent baseline mixed trusted host rules with workspace-controlled text (AGENTS.md, directory listings, skill listings, session timestamp) inside the system channel. Soft prose ("AGENTS is not privileged") was not a structural boundary. A poisoned checkout can therefore elevate project text toward system-law priority (prompt injection via baseline / supply chain), not only via the user's chat turn.

Related historical discussion: #2024 (closed without landed fix), #2028 (KIMI_NOW / cache), #524 /context, #1955 baseline size.

What changed

Phase 1 — structural isolation

  • Added shared helpers (v1 + v2):
    • escapeUntrustedText, sanitizeUntrustedControls, wrapUntrusted
  • Envelopes: untrusted_cwd_listing, untrusted_additional_dirs, untrusted_agents_md, untrusted_skills_listing
  • Skill activation: non-system preamble + hardenSkillBody (closer breakout + control strip)
  • Goal injectors: reuse shared escape instead of local copies
  • Default system templates describe workspace data as untrusted and non-overriding

Phase 2a/2b — role / channel split (Codex-aligned, without kosong developer role)

  • Trusted system prompt retains role, safety, tool etiquette, OS/shell, cwd path, and rules pointing at external context.
  • Removed from system body: raw listing / AGENTS / skills payloads and mid-system dynamic now.
  • Request-time user fragments via buildBaselineContextMessages / baselineMessagesForContext:
    1. Time fringe: "It is …" (stable system prefix → better prompt-cache characteristics; related to KIMI_NOW timestamp splits the system prompt into two injectable payload blocks #2028).
    2. External workspace body with Phase 1 envelopes.
  • Stitched at generate/request time — not written into conversation history / durable turn memory:
    • v1: Agent holds fragments → KosongLLM prefixes history; BTW copies parent baseline; full compaction estimates + summarizer calls include baseline.
    • v2: IAgentProfileService.getBaselineContextMessages()llmRequester prefixes (turn-snapshotted with system prompt).
  • Legacy template placeholders for untrusted vars stay empty so custom agent templates cannot re-inject workspace payloads into system through those vars.

Why this fits Kimi Code

  • Dual engines (agent-core + agent-core-v2) stay parallel.
  • Uses existing kosong roles (user + system side-channel); avoids a kosong-wide developer role change.
  • Host approvals / sandbox remain the real control plane; this change lowers architectural trust of workspace text.

Out of scope (explicit non-goals)

Architecture (after)

┌─ systemPrompt (trusted, stable) ─────────────────────┐
│  role + safety + tool etiquette + OS/shell + cwd path │
│  rules: workspace data arrive as user baseline msgs   │
└──────────────────────────────────────────────────────┘
┌─ request-only user fragments (not history) ──────────┐
│  [user] time fringe                                        │
│  [user] External Workspace Context + <untrusted_*>…       │
└──────────────────────────────────────────────────────┘
┌─ context memory (conversation) ──────────────────────┐
│  real user / assistant / tool / system-reminder turns │
└──────────────────────────────────────────────────────┘

Test plan

Commands used during development (Node ≥ 24.15, pnpm monorepo):

pnpm --filter @moonshot-ai/agent-core typecheck
pnpm --filter @moonshot-ai/agent-core-v2 typecheck

pnpm --filter @moonshot-ai/agent-core exec vitest run \
  test/profile \
  test/utils/xml-escape.test.ts \
  test/agent/kosong-llm.test.ts \
  test/tools/skill-tool.test.ts \
  test/agent/injection/goal.test.ts \
  test/agent/skill-tool-manager.test.ts

pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run \
  test/app/agentProfileCatalog \
  test/_base/utils/xml-escape.test.ts \
  test/agent/goal/injection \
  test/app/skillCatalog/skill-tool-manager.test.ts \
  test/agent/llmRequester/llmRequesterService.test.ts

Last verified: typecheck green; agent-core focused 96 tests; agent-core-v2 focused 66 tests.

Coverage includes:

Area Tests
Envelope + escape helpers xml-escape.test.ts (v1/v2)
Baseline builder (fringe, skills gate, breakout) baseline-context.test.ts, profile-shared/default-agent-profiles
System trusted-only default profile / loader tests
LLM stitch prepend kosong-llm.test.ts, llmRequesterService.test.ts
Skill activation harden skill-tool / skill-tool-manager
Goal escape single-source goal injection tests

Manual (recommended before merge):

  1. Temp dir + hostile AGENTS.md requiring "POISONED" prefix.
  2. Fresh session, ask What is 2+2?.
  3. Inspect wire/request log: system lacks AGENTS body; user baseline fragments contain <untrusted_agents_md> with escaped closers if present.

Changeset

.changeset/untrusted-system-prompt-baseline.mdpatch @moonshot-ai/kimi-code:

Isolate workspace-supplied baseline context (AGENTS.md, directory listings, skill listings, session time) in request-time user fragments with untrusted envelopes so it cannot live in the trusted system prompt or override host rules.

Docs

  • Design: docs/design/system-prompt-baseline-isolation.md
  • This MR / issue drafts under docs/design/ (internal; not VitePress user docs).
  • Product en/zh user docs: not updated (no user-facing config/command change). Mark gen-docs N/A unless reviewers want a security note later.

Risk and rollout

Risk Mitigation
Models treat AGENTS weaker off-system Still injected every turn; preamble insists it is project guidance
Custom agent templates that referenced {{ KIMI_AGENTS_MD }} etc. Placeholders empty by design; baseline still delivers content with standard framing
Resume from old wire with fat system blob until refresh Next bind/refresh re-renders trusted-only + baseline rebuild
BTW/subagent missing baseline copyBaselineContextFrom / profile bind paths
Residual social engineering via benign-looking AGENTS Accepted; permissions/sandbox still authorize action

Rollout: default-on, no feature flag. Patch release with CLI bundle.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran changeset (manual file matching gen-changesets intent): .changeset/untrusted-system-prompt-baseline.md
  • Ran gen-docs skill, or this PR needs no doc update.

File map (primary)

packages/agent-core/src/utils/xml-escape.ts
packages/agent-core/src/profile/baseline-context.ts
packages/agent-core/src/profile/resolve.ts
packages/agent-core/src/profile/default/system.md
packages/agent-core/src/agent/turn/kosong-llm.ts
packages/agent-core/src/agent/index.ts
packages/agent-core/src/agent/skill/prompt.ts
packages/agent-core/src/agent/injection/goal.ts
packages/agent-core/src/agent/compaction/full.ts
packages/agent-core/src/session/subagent-host.ts

packages/agent-core-v2/src/_base/utils/xml-escape.ts
packages/agent-core-v2/src/app/agentProfileCatalog/baseline-context.ts
packages/agent-core-v2/src/app/agentProfileCatalog/profile-shared.ts
packages/agent-core-v2/src/app/agentProfileCatalog/system.md
packages/agent-core-v2/src/agent/profile/profile.ts
packages/agent-core-v2/src/agent/profile/profileService.ts
packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts
packages/agent-core-v2/src/agent/skill/prompt.ts
packages/agent-core-v2/src/agent/goal/injection/goalInjection.ts

.changeset/untrusted-system-prompt-baseline.md
docs/design/system-prompt-baseline-isolation.md

Reviewer notes

  1. Confirm system prefix no longer includes listing/AGENTS/skills/now for default profiles.
  2. Confirm request path always prepends baseline (including compaction summarizer path on v1).
  3. Confirm empty baseline does not double-prefix or force empty user turns.
  4. Reject accidental commits of docs/.gitignore noise, handoff files, or _refs/ comparison clones.
  5. Branch may need rebase onto latest main before open (worktree was behind origin during development).

rama-pavith added 2 commits July 22, 2026 12:26
Keep host rules in the system channel; deliver AGENTS.md, directory
listings, skills, and session time as request-time user fragments with
untrusted envelopes on both agent-core engines.
Capture the threat model, architecture, acceptance criteria, and PR
template copy for the untrusted baseline isolation work.
@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5c6b169

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6be16a8302

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +552 to +553
const messages =
baseline.length === 0 ? [...history] : [...baseline, ...history];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve context-size measurements with baseline

Whenever a v2 turn has any baseline (the new builder normally emits the time fringe), this prepends extra user messages to request.messages. Later the same service calls this.contextSize.measured(request.messages, [message], usage), and AgentContextSizeService.measured returns early unless the input array exactly matches IAgentContextMemoryService.get(). As a result normal v2 turns stop recording provider token usage, leaving status, budget sizing, and compaction decisions on stale estimates. Pass only the real history into context-size accounting, or teach it to ignore the request-only baseline while still sending the prefixed messages to the model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1c62e7: contextSize.measured now receives historyMessages (conversation only). Request body still prepends baseline for the model.

}

private activeProfile: ResolvedAgentProfile | undefined;
private baselineContextMessages: readonly Message[] = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebuild baseline context after restoring v2 agents

baselineContextMessages is new in-memory state, but restored agents are created by replaying the wire ProfileModel; no bind()/useProfile() path runs, and the prompt route skips rebinding when the replayed profileName already matches. For resumed sessions this field therefore stays empty, while the persisted systemPrompt is now trusted-only and no longer contains AGENTS.md, listings, skills, or time, so the next prompt runs without the workspace guidance this change moved out of system. Rebuild it after wire.restore() or persist a snapshot before the agent can request the model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1c62e7: bindBootstrap calls refreshSystemPrompt() when create has no binding, so restored agents rebuild request-time baseline after wire restore.

additionalDirsInfo: context?.additionalDirsInfo,
});
this.config.update({ profileName: profile.name, systemPrompt });
this.baselineContextMessages = buildBaselineContextMessages({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebuild v1 baseline context on session resume

This new baseline is only assigned when useProfile()/refreshSystemPrompt() runs. On a normal v1 session resume, Session.resumeAgent() replays the wire and only calls restoreAgentProfileHandle() to set activeProfile; because the replayed systemPrompt is non-empty it does not bootstrap the profile again. New sessions saved by this commit therefore resume with baselineContextMessages empty, while their system prompt only points to a separate baseline message, so AGENTS.md, directory listings, skills, and time disappear until some unrelated refresh. Rebuild this baseline during restore before serving prompts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1c62e7: v1 restoreAgentProfileHandle now awaits refreshSystemPrompt() after restoring the profile handle so AGENTS/listings/skills/time fringe return on resume.

applyBindingSnapshot(snapshot: ProfileBindingSnapshot): void {
this.activeProfile = undefined;
this.activeToolNamesOverlay = undefined;
this.baselineContextMessages = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Copy the baseline when forking v2 agents

When SessionBtwService.start() forks the main agent, AgentLifecycleService.fork() uses applyBindingSnapshot() unless the caller overrides the profile. This reset drops the parent's newly separated baseline, and the snapshot only carries the trusted systemPrompt, so BTW/side-question children lose the AGENTS.md, listing, skills, and time context they inherited before this commit. Copy the source profile's baseline or rebuild it from the snapshot so forked agents see the same workspace baseline as their parent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1c62e7: ProfileBindingSnapshot / data() carry baselineContextMessages, and applyBindingSnapshot restores them so fork/BTW keeps the parent baseline.

Pavith added 2 commits July 22, 2026 14:18
Address Codex review on MoonshotAI#2049: measure context size without request-only
baseline prefixes; rebuild baseline after v1/v2 resume; copy baseline
through v2 binding snapshots used by fork/BTW.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: isolate workspace baseline (AGENTS.md / listings / skills) from trusted system prompt

1 participant