kimi-plugin-cc 2.5.2 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/dist/cli-client.js +8 -0
  5. package/dist/commands/swarm.js +2 -2
  6. package/dist/hooks/config-safety.js +5 -2
  7. package/dist/kimi-command.js +1 -0
  8. package/dist/kimi-engine.js +3 -0
  9. package/dist/native-v2-preflight.js +138 -30
  10. package/dist/swarm-activation.js +275 -14
  11. package/dist/version.js +1 -1
  12. package/docs/migration.md +4 -4
  13. package/docs/registry-releases.md +5 -1
  14. package/docs/safety.md +11 -9
  15. package/docs/swarm-recovery.md +2 -2
  16. package/docs/upstream-2.5.0-certification.md +50 -0
  17. package/docs/upstream-2.5.1-certification.md +45 -0
  18. package/package.json +3 -1
  19. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  20. package/plugins/kimi-codex/dist/cli-client.js +8 -0
  21. package/plugins/kimi-codex/dist/commands/swarm.js +2 -2
  22. package/plugins/kimi-codex/dist/hooks/config-safety.js +5 -2
  23. package/plugins/kimi-codex/dist/kimi-command.js +1 -0
  24. package/plugins/kimi-codex/dist/kimi-engine.js +3 -0
  25. package/plugins/kimi-codex/dist/native-v2-preflight.js +138 -30
  26. package/plugins/kimi-codex/dist/swarm-activation.js +275 -14
  27. package/plugins/kimi-codex/dist/version.js +1 -1
  28. package/plugins/kimi-dsh/dist/cli-client.js +8 -0
  29. package/plugins/kimi-dsh/dist/commands/swarm.js +2 -2
  30. package/plugins/kimi-dsh/dist/hooks/config-safety.js +5 -2
  31. package/plugins/kimi-dsh/dist/kimi-command.js +1 -0
  32. package/plugins/kimi-dsh/dist/kimi-engine.js +3 -0
  33. package/plugins/kimi-dsh/dist/native-v2-preflight.js +138 -30
  34. package/plugins/kimi-dsh/dist/swarm-activation.js +275 -14
  35. package/plugins/kimi-dsh/dist/version.js +1 -1
  36. package/plugins/kimi-dsh/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Kimi for Claude Code: code questions, reviews, fixes, parallel tasks, and MCP tasks through the local Kimi Code CLI.",
8
- "version": "2.5.2"
8
+ "version": "2.5.4"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kimi",
3
3
  "description": "Kimi for Claude Code: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tasks through the local Kimi Code CLI, with PreToolUse safety hooks.",
4
- "version": "2.5.2",
4
+ "version": "2.5.4",
5
5
  "homepage": "https://github.com/linxule/kimi-plugin-cc",
6
6
  "repository": "https://github.com/linxule/kimi-plugin-cc",
7
7
  "license": "Apache-2.0",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  > **Post-1.0 release history (v1.0.1 -> present) lives in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** and the "Version" / "Upstream compat" lines of [AGENTS.md](./AGENTS.md). Docs-only kimi-code compat checkups that don't bump the plugin version (e.g. the 0.14.2 / 0.14.3 patches) are recorded there, not here. Notable releases are summarized below; the GA entry and full pre-GA detail follow.
4
4
 
5
+ ## 2.5.4 — 2026-11-06
6
+
7
+ - Restore the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The published 2.5.3 artifact contains the trial condition; this patch carries its revert without re-tagging or republishing it.
8
+ - Keep deterministic close-timeout coverage through a controlled no-close-event socket and exact timeout-message assertion, plus a protocol-correct mock close handshake. Production behavior otherwise remains unchanged from the certified 2.5.0 and 2.5.1 implementation.
9
+ - Revalidate 71 activation tests with 244 assertions, the full repository gate with 1,412 tests and 18,282 assertions, and 330-file package and consumer-security checks. Exact CLI certification remains unchanged through 2.5.1; installed hosts and operator pins are not updated by publication.
10
+
11
+ ## 2.5.3 — 2026-11-06
12
+
13
+ - Certify exact kimi-code 2.5.0 and 2.5.1 for all eight native-v2 operations. Preserve the no-plan construction, local execution and workspace-trust enforcement, authenticated WebSocket swarm state, bounded lifecycle cleanup, and the legacy-v1 0.41.x boundary. See the [2.5.0](docs/upstream-2.5.0-certification.md) and [2.5.1](docs/upstream-2.5.1-certification.md) evidence.
14
+ - Review every exact-version branch for fresh `--environment local`, hook schemas, preactivation, local environment/cwd binding, and protocol-v2 agent state. Make close-timeout coverage deterministic with a controlled no-close-event socket and exact error assertion, without relying on platform-specific `wasClean` semantics. The 2.5.1 source scan passed 18 tests with 153 assertions; candidate and production matrices each passed 24 tests with 167 assertions and no skips, including real rescue, Bash.cwd confinement, resume/taint refusal, goal-turn denials, and child-authored patches.
15
+ - Pass the full repository gate with 1,412 tests and 18,282 assertions, 330-file package checks across all three distributions, isolated consumer security, and independent read-only `claude -p` review with no must-fix findings. Update the manual smoke default to exact 2.5.1 and the current compatibility documentation; publication does not update installed hosts or operator pins.
16
+
5
17
  ## 2.5.2 — 2026-11-02
6
18
 
7
19
  - Preserve TOML multiline values, line endings, and foreign hook fields during setup; repair mixed-host orphan marker comments without altering foreign hooks.
@@ -65,6 +65,7 @@ export async function runCliPrompt(opts) {
65
65
  kimiHome: resolveKimiHome(opts.env, opts.cwd),
66
66
  env: opts.env,
67
67
  resumeSessionId: opts.resumeSessionId,
68
+ expectedExecutionRoot: opts.trustedWorkspaceRoot ?? opts.cwd,
68
69
  stage: "cli-client.spawn",
69
70
  });
70
71
  }
@@ -781,6 +782,13 @@ const KIMI_LEGACY_FORCED_VALUE = "1";
781
782
  function buildArgs(opts) {
782
783
  const args = [...(opts.prefixArgs ?? [])];
783
784
  args.push("--output-format", "stream-json");
785
+ const smokeCandidate = opts.executionPlan.certification === "test-bypass" && opts.env.KIMI_PLUGIN_CC_SMOKE === "1"
786
+ ? opts.env.KIMI_PLUGIN_CC_SMOKE_V2_CANDIDATE
787
+ : undefined;
788
+ if (opts.resumeSessionId === undefined &&
789
+ ["2.5.0", "2.5.1"].includes(opts.executionPlan.kimiVersion ?? smokeCandidate ?? "")) {
790
+ args.push("--environment", "local");
791
+ }
784
792
  if (opts.resumeSessionId !== undefined) {
785
793
  args.push("-r", opts.resumeSessionId);
786
794
  }
@@ -28,8 +28,8 @@ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js
28
28
  // only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
29
29
  // and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
30
30
  // write confinement on the coordinator and every subagent. Write mode requires
31
- // kimi-code 0.18.0+. Exact 2.3.2, 2.3.3, 2.4.0 and 2.4.1 additionally activate a same-job
32
- // session through a bounded loopback web lifecycle, then resume it through the normal print path.
31
+ // kimi-code 0.18.0+. Exact 2.3.2, 2.3.3, 2.4.0, 2.4.1, 2.5.0 and 2.5.1 additionally
32
+ // activate a same-job session through a bounded loopback web lifecycle, then resume it through the normal print path.
33
33
  const SWARM_SUMMARY_MAX = 120;
34
34
  const SWARM_AGENT_PROFILE = "<swarm>";
35
35
  const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
@@ -595,7 +595,8 @@ function validateVersionSensitiveEvents(events, version) {
595
595
  // Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
596
596
  // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
597
597
  // 2.3.0 (b5e28fd6), 2.3.1 (684bae9e), 2.3.2 (b8dd3a13), 2.3.3 (ec495f0c),
598
- // 2.4.0 (f26e4d91), and 2.4.1 (2c73f3f5): the additive events and HookDefSchema are unchanged.
598
+ // 2.4.0 (f26e4d91), 2.4.1 (2c73f3f5), 2.5.0 (41c34143), and 2.5.1 (1b02030d):
599
+ // the additive events and HookDefSchema are unchanged.
599
600
  // Require the full version string AND matching components; no inferred 2.x compatibility.
600
601
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
601
602
  (version.version === "2.0.1" && version.patch === 1) ||
@@ -608,7 +609,9 @@ function validateVersionSensitiveEvents(events, version) {
608
609
  (version.version === "2.3.2" && version.patch === 2) ||
609
610
  (version.version === "2.3.3" && version.patch === 3))) ||
610
611
  (version.minor === 4 && ((version.version === "2.4.0" && version.patch === 0) ||
611
- (version.version === "2.4.1" && version.patch === 1))))) {
612
+ (version.version === "2.4.1" && version.patch === 1))) ||
613
+ (version.minor === 5 && ((version.version === "2.5.0" && version.patch === 0) ||
614
+ (version.version === "2.5.1" && version.patch === 1))))) {
612
615
  return { valid: true };
613
616
  }
614
617
  if (version.major !== 0) {
@@ -47,6 +47,7 @@ const RESERVED_PREFIX_FLAGS = new Set([
47
47
  "--plan", "--auto", "-y", "--yolo", "--yes", "--auto-approve", "--manual",
48
48
  // Model / profile / roots.
49
49
  "-m", "--model", "--skills-dir", "--agent", "--agent-file", "--add-dir",
50
+ "--environment", "--environment-cwd",
50
51
  // Terminal informational flags (would replace the run with a banner).
51
52
  "-V", "--version", "-h", "--help",
52
53
  ]);
@@ -45,6 +45,8 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
45
45
  "2.3.3",
46
46
  "2.4.0",
47
47
  "2.4.1",
48
+ "2.5.0",
49
+ "2.5.1",
48
50
  ]);
49
51
  /** Per-operation native-v2 certification. One green operation never certifies another. */
50
52
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -127,6 +129,7 @@ export async function prepareKimiExecutionPlan(options) {
127
129
  kimiHome: resolveKimiHome(options.env, options.cwd),
128
130
  env: options.env,
129
131
  resumeSessionId: options.resumeSessionId,
132
+ expectedExecutionRoot: options.cwd,
130
133
  stage: "kimi-engine.plan",
131
134
  });
132
135
  };
@@ -13,9 +13,10 @@
13
13
  // B. the EnterPlanMode tool (already hook-denied for every label);
14
14
  // C. resuming a session whose agent wire journal holds a durable
15
15
  // `plan_mode.enter` record (restore() folds ONLY agents/*/wire.jsonl).
16
- // This module closes A and C before any process is spawned, and refuses the
16
+ // This module closes A and C before any process is spawned, refuses the
17
17
  // v2-only experimental features (tower, subagent fork) that this plugin does
18
- // not certify. B stays in runtime/hooks/approval-policy.ts.
18
+ // not certify, and requires local execution for configured and resumed sessions.
19
+ // B stays in runtime/hooks/approval-policy.ts.
19
20
  //
20
21
  // Contract:
21
22
  // - Pure inspection: never writes, never rewrites operator config, never
@@ -155,6 +156,48 @@ export async function inspectPlanModeConfig(kimiHome) {
155
156
  reason: `\`${offending.join("`, `")}\` (upstream setting \`default_plan_mode\`) is set in ${configPath} and is not \`false\`.`,
156
157
  };
157
158
  }
159
+ export async function inspectEnvironmentConfig(kimiHome) {
160
+ const parsed = await readParsedConfig(kimiHome);
161
+ if (parsed.kind === "unreadable") {
162
+ return { kind: "refuse", refusalKind: "v2-config-unreadable", reason: parsed.reason };
163
+ }
164
+ const section = parsed.config?.["environments"];
165
+ if (section !== undefined) {
166
+ if (section === null || typeof section !== "object" || Array.isArray(section)) {
167
+ return {
168
+ kind: "refuse",
169
+ refusalKind: "v2-environment-configured",
170
+ reason: "The [environments] section is not a table, so the execution environment cannot be verified as local.",
171
+ };
172
+ }
173
+ if (section["default"] !== undefined) {
174
+ return {
175
+ kind: "refuse",
176
+ refusalKind: "v2-environment-configured",
177
+ reason: "A default remote execution environment is configured in [environments]; plugin-managed jobs require the local environment.",
178
+ };
179
+ }
180
+ }
181
+ const workspace = parsed.config?.["workspace"];
182
+ if (workspace !== undefined) {
183
+ if (workspace === null || typeof workspace !== "object" || Array.isArray(workspace)) {
184
+ return {
185
+ kind: "refuse",
186
+ refusalKind: "v2-workspace-trust-disabled",
187
+ reason: "The [workspace] section is not a table, so project trust requirements cannot be verified.",
188
+ };
189
+ }
190
+ const trustDisabled = Object.entries(workspace).some(([key, value]) => upstreamSnakeToCamel(key) === "requireTrust" && value !== true);
191
+ if (trustDisabled) {
192
+ return {
193
+ kind: "refuse",
194
+ refusalKind: "v2-workspace-trust-disabled",
195
+ reason: "Workspace trust is disabled in [workspace]; plugin-managed jobs require project trust enforcement to remain enabled.",
196
+ };
197
+ }
198
+ }
199
+ return { kind: "ok" };
200
+ }
158
201
  /**
159
202
  * Refuse v2-only experimental features this plugin does not certify.
160
203
  * Sources (per-flag precedence upstream is env → [experimental] config →
@@ -239,40 +282,39 @@ async function openJournalSafely(journalPath, maxBytes) {
239
282
  }
240
283
  return { kind: "ok", handle };
241
284
  }
242
- /**
243
- * Scan one open journal's lines for a plan-mode control record. Returns the
244
- * record `type` string on a match, "<unparseable>" for a suspicious line
245
- * that fails to parse, or null when the journal is clean.
246
- */
247
- async function scanJournalLines(handle) {
285
+ async function scanJournalLines(handle, expectedExecutionRoot) {
248
286
  const rl = createInterface({ input: handle.createReadStream() });
249
287
  try {
250
288
  for await (const rawLine of rl) {
251
289
  const line = rawLine.trim();
252
290
  if (line.length === 0)
253
291
  continue;
254
- // Parse EVERY record and check its DECODED `type`. A raw-substring filter
255
- // is unsafe on its own: a JSON-escaped type such as
256
- // {"type":"plan_mode.enter"} contains no literal "plan_mode." yet
257
- // upstream JSON.parse decodes it to "plan_mode.enter" and restores active
258
- // plan mode. So parse first (this catches the escape). If a line FAILS to
259
- // parse, upstream cannot fold it into a plan event either — but be
260
- // conservative: a malformed line that still LOOKS plan-related is treated
261
- // as tainted rather than skipped, so a hand-corrupted journal cannot hide
262
- // a plan record behind a parse error.
263
292
  let parsed;
264
293
  try {
265
294
  parsed = JSON.parse(line);
266
295
  }
267
296
  catch {
268
297
  if (line.includes('"plan_mode.') || line.includes('"plan.')) {
269
- return "<unparseable>";
298
+ return { kind: "tainted", recordType: "<unparseable>" };
299
+ }
300
+ if (line.includes('"environment.set_binding')) {
301
+ return { kind: "environment", recordType: "<unparseable-environment-binding>" };
270
302
  }
271
303
  continue;
272
304
  }
273
- const type = parsed?.["type"];
305
+ const record = parsed;
306
+ const type = record?.["type"];
274
307
  if (typeof type === "string" && (type.startsWith("plan_mode.") || type.startsWith("plan."))) {
275
- return type;
308
+ return { kind: "tainted", recordType: type };
309
+ }
310
+ if (type === "environment.set_binding") {
311
+ if (record?.["environmentId"] !== "local") {
312
+ return { kind: "environment", recordType: type };
313
+ }
314
+ const cwd = record?.["cwd"];
315
+ if (expectedExecutionRoot !== undefined && cwd !== undefined && cwd !== expectedExecutionRoot) {
316
+ return { kind: "environment", recordType: "environment.set_binding.cwd" };
317
+ }
276
318
  }
277
319
  }
278
320
  return null;
@@ -281,16 +323,48 @@ async function scanJournalLines(handle) {
281
323
  rl.close();
282
324
  }
283
325
  }
326
+ async function inspectSessionMetadataEnvironment(statePath, expectedExecutionRoot) {
327
+ const read = await readSmallFileSafe(statePath, CONFIG_MAX_BYTES);
328
+ if (read.kind === "absent")
329
+ return null;
330
+ if (read.kind === "unreadable") {
331
+ return { kind: "unavailable", reason: `${statePath}: ${read.reason}` };
332
+ }
333
+ let metadata;
334
+ try {
335
+ metadata = JSON.parse(read.contents);
336
+ }
337
+ catch {
338
+ return { kind: "unavailable", reason: `${statePath}: malformed session metadata` };
339
+ }
340
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
341
+ return { kind: "unavailable", reason: `${statePath}: invalid session metadata` };
342
+ }
343
+ const record = metadata;
344
+ const environmentId = record["environmentId"];
345
+ if (environmentId !== undefined && environmentId !== "local") {
346
+ return { kind: "environment", journal: statePath, recordType: "session.metadata.environmentId" };
347
+ }
348
+ const environmentCwd = record["environmentCwd"];
349
+ if ((environmentId === undefined || environmentId === "local") &&
350
+ expectedExecutionRoot !== undefined &&
351
+ environmentCwd !== undefined &&
352
+ environmentCwd !== expectedExecutionRoot) {
353
+ return { kind: "environment", journal: statePath, recordType: "session.metadata.environmentCwd" };
354
+ }
355
+ return null;
356
+ }
284
357
  /**
285
- * Scan every agent wire journal of a saved session for plan-mode records.
286
- * Layout at 0.42.0: `<kimiHome>/sessions/<workspaceId>/<sessionId>/agents/<agentId>/wire.jsonl`
358
+ * Scan saved session metadata and every agent wire journal for plan-mode records
359
+ * and nonlocal environment bindings. Layout at 0.42.0:
360
+ * `<kimiHome>/sessions/<workspaceId>/<sessionId>/agents/<agentId>/wire.jsonl`
287
361
  * (workspaceId is unknown to the plugin → enumerate `sessions/*` with bounded
288
362
  * readdir). Raw-line scan of EVERY agent journal: a JSON line whose `type`
289
- * starts with `plan_mode.` or `plan.` → tainted (fail-closed on a line that
290
- * looks suspicious but does not parse). No session dir, no journal, unreadable,
363
+ * starts with `plan_mode.` or `plan.` → tainted; `environment.set_binding` must
364
+ * name `environmentId:"local"`. No session dir, no journal, unreadable metadata,
291
365
  * or any single journal above the size cap → unavailable. Never modifies files.
292
366
  */
293
- export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
367
+ export async function scanSessionJournalsForPlan(kimiHome, sessionId, expectedExecutionRoot) {
294
368
  const sessionsDir = path.join(kimiHome, "sessions");
295
369
  const workspaceEntries = await boundedReaddir(sessionsDir, DIR_ENUM_CAP);
296
370
  if (workspaceEntries.kind === "absent") {
@@ -316,7 +390,11 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
316
390
  }
317
391
  if (!workspaceEntry.isDirectory())
318
392
  continue;
319
- const agentsDir = path.join(sessionsDir, workspaceEntry.name, sessionId, "agents");
393
+ const sessionDir = path.join(sessionsDir, workspaceEntry.name, sessionId);
394
+ const metadataFinding = await inspectSessionMetadataEnvironment(path.join(sessionDir, "state.json"), expectedExecutionRoot);
395
+ if (metadataFinding !== null)
396
+ return metadataFinding;
397
+ const agentsDir = path.join(sessionDir, "agents");
320
398
  const agentEntries = await boundedReaddir(agentsDir, DIR_ENUM_CAP);
321
399
  if (agentEntries.kind === "absent")
322
400
  continue;
@@ -347,9 +425,12 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
347
425
  return { kind: "unavailable", reason: `${journalPath}: ${opened.reason}` };
348
426
  }
349
427
  try {
350
- const taintedType = await scanJournalLines(opened.handle);
351
- if (taintedType !== null) {
352
- return { kind: "tainted", journal: journalPath, recordType: taintedType };
428
+ const finding = await scanJournalLines(opened.handle, expectedExecutionRoot);
429
+ if (finding?.kind === "tainted") {
430
+ return { kind: "tainted", journal: journalPath, recordType: finding.recordType };
431
+ }
432
+ if (finding?.kind === "environment") {
433
+ return { kind: "environment", journal: journalPath, recordType: finding.recordType };
353
434
  }
354
435
  }
355
436
  finally {
@@ -363,6 +444,8 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
363
444
  * CLI_V2_PLAN_MODE_CONFIGURED details.refusal_kind = "v2-plan-mode-configured"
364
445
  * CLI_V2_CONFIG_UNREADABLE details.refusal_kind = "v2-config-unreadable"
365
446
  * CLI_V2_EXPERIMENTAL_UNSAFE details.refusal_kind = "v2-experimental-unsafe", details.selectors
447
+ * CLI_V2_ENVIRONMENT_UNSAFE / CLI_V2_WORKSPACE_TRUST_UNSAFE for unsafe config
448
+ * KIMI_SESSION_ENVIRONMENT_UNSAFE details.refusal_kind = "session-environment-unsafe"
366
449
  * KIMI_SESSION_PLAN_TAINTED details.refusal_kind = "session-plan-tainted", details.record_type
367
450
  * KIMI_SESSION_JOURNAL_UNAVAILABLE details.refusal_kind = "session-journal-unavailable"
368
451
  * Every message carries a truthful remedy (edit the named setting / start a
@@ -384,6 +467,22 @@ export async function assertNativeV2Preflight(options) {
384
467
  details: { refusal_kind: planMode.refusalKind, retryable_after_setup: false },
385
468
  });
386
469
  }
470
+ const environment = await inspectEnvironmentConfig(options.kimiHome);
471
+ if (environment.kind === "refuse") {
472
+ const code = environment.refusalKind === "v2-environment-configured"
473
+ ? "CLI_V2_ENVIRONMENT_UNSAFE"
474
+ : environment.refusalKind === "v2-workspace-trust-disabled"
475
+ ? "CLI_V2_WORKSPACE_TRUST_UNSAFE"
476
+ : "CLI_V2_CONFIG_UNREADABLE";
477
+ const remedy = environment.refusalKind === "v2-environment-configured"
478
+ ? `Remove \`environments.default\` in ${configPath} so plugin-managed sessions stay local.`
479
+ : environment.refusalKind === "v2-workspace-trust-disabled"
480
+ ? `Set \`workspace.require_trust = true\` (or remove it) in ${configPath}.`
481
+ : `Fix or remove the unreadable file at ${configPath} so the execution environment can be verified as local.`;
482
+ throw new RuntimeError(code, `${environment.reason} ${remedy}`, stage, {
483
+ details: { refusal_kind: environment.refusalKind, retryable_after_setup: false },
484
+ });
485
+ }
387
486
  const experimental = await inspectExperimentalSelectors(options.kimiHome, options.env);
388
487
  if (experimental.kind === "refuse") {
389
488
  throw new RuntimeError("CLI_V2_EXPERIMENTAL_UNSAFE", `${experimental.reason} Unset the listed selector(s) before retrying.`, stage, {
@@ -395,7 +494,16 @@ export async function assertNativeV2Preflight(options) {
395
494
  });
396
495
  }
397
496
  if (options.resumeSessionId !== undefined) {
398
- const journalScan = await scanSessionJournalsForPlan(options.kimiHome, options.resumeSessionId);
497
+ const journalScan = await scanSessionJournalsForPlan(options.kimiHome, options.resumeSessionId, options.expectedExecutionRoot);
498
+ if (journalScan.kind === "environment") {
499
+ throw new RuntimeError("KIMI_SESSION_ENVIRONMENT_UNSAFE", `Saved session ${options.resumeSessionId} has an unsafe execution environment binding (${journalScan.recordType} in ${journalScan.journal}). Start a fresh local session; saved session state was not modified.`, stage, {
500
+ details: {
501
+ refusal_kind: "session-environment-unsafe",
502
+ record_type: journalScan.recordType,
503
+ retryable_after_setup: false,
504
+ },
505
+ });
506
+ }
399
507
  if (journalScan.kind === "tainted") {
400
508
  throw new RuntimeError("KIMI_SESSION_PLAN_TAINTED", `Saved session ${options.resumeSessionId} previously entered plan mode (record type "${journalScan.recordType}" in ${journalScan.journal}). Start a fresh session; saved session state was not modified.`, stage, {
401
509
  details: {