kimi-plugin-cc 2.5.2 → 2.5.3
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/cli-client.js +8 -0
- package/dist/commands/swarm.js +2 -2
- package/dist/hooks/config-safety.js +5 -2
- package/dist/kimi-command.js +1 -0
- package/dist/kimi-engine.js +3 -0
- package/dist/native-v2-preflight.js +138 -30
- package/dist/swarm-activation.js +276 -14
- package/dist/version.js +1 -1
- package/docs/migration.md +4 -4
- package/docs/registry-releases.md +3 -1
- package/docs/safety.md +11 -9
- package/docs/swarm-recovery.md +2 -2
- package/docs/upstream-2.5.0-certification.md +48 -0
- package/docs/upstream-2.5.1-certification.md +43 -0
- package/package.json +3 -1
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +8 -0
- package/plugins/kimi-codex/dist/commands/swarm.js +2 -2
- package/plugins/kimi-codex/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-codex/dist/kimi-command.js +1 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +3 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +138 -30
- package/plugins/kimi-codex/dist/swarm-activation.js +276 -14
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/dist/cli-client.js +8 -0
- package/plugins/kimi-dsh/dist/commands/swarm.js +2 -2
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-dsh/dist/kimi-command.js +1 -0
- package/plugins/kimi-dsh/dist/kimi-engine.js +3 -0
- package/plugins/kimi-dsh/dist/native-v2-preflight.js +138 -30
- package/plugins/kimi-dsh/dist/swarm-activation.js +276 -14
- package/plugins/kimi-dsh/dist/version.js +1 -1
- package/plugins/kimi-dsh/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Kimi Code 2.5.1 certification evidence — 2026-11-06
|
|
2
|
+
|
|
3
|
+
## Status and exact source
|
|
4
|
+
|
|
5
|
+
**Exact CLI 2.5.1 is certified for all eight operations by the plugin 2.5.2 working tree.** The exact-source audit, non-vacuous plan/environment/swarm controls, candidate matrix, production-table matrix, frozen-install, literal full repository check, package validation, and isolated consumer-security audit passed. Independent read-only `claude -p` review returned **APPROVE with no must-fix findings**. Commit, push, tag, publication, installed-host rollout, and operator CLI pin changes are outside this certification slice and were not performed.
|
|
6
|
+
|
|
7
|
+
Candidate: `@moonshot-ai/kimi-code@2.5.1`, release commit `1b02030d8c2c6b00131b23ebb67a231614877827`, compared with certified 2.5.0 (`41c341434ae24e45bfe0f2232ec11b80531ac717`). The monitor recorded npm publication at 2026-11-05T09:58:53.967Z. A temporary isolated npm installation and its executable both report 2.5.1; its `dist/main.mjs` SHA-256 is `84156e7a0f500bb58f9734be57b26756c27eacefb9d884ef94db3af0d6b609dc`. The clean exact source checkout was verified at the release commit. The seven scoped 2.5.0→2.5.1 diffs are retained under `.claude/kimi-code-research/daily-monitor/2026-11-06-2.5.1-diffs/`.
|
|
8
|
+
|
|
9
|
+
## Source and integration review
|
|
10
|
+
|
|
11
|
+
- Scopes 01–06 are unchanged: print/CLI, before-execute and permission gates, external hooks, plan/restore, bootstrap/config/workspace, tools/fan-out/SDK. Scope 07 changes session-list visibility and web media projection only. The provider protocol omits empty system content on tool-bearing messages; the additional transcript change folds daemon media into web history. Neither changes headless stream records, execution, plan arming, hook ordering, environment binding, or WebSocket agent state.
|
|
12
|
+
- DI/scope creation, OS backends, session reload, wire restore/migrations, and the other human-review scopes are unchanged. Of the 40 pinned files, 39 are byte-identical to 2.5.0. Only `kap-server/src/routes/sessions.ts` changed, for deleted-workspace filtering in session lists; session creation and activation are unchanged.
|
|
13
|
+
- The expanded exact-tag scan passed **18 tests and 153 assertions**, with the print, environment/workspace-trust, and WebSocket checks actually executing on 2.5.1. The preserved 2.5.0 source scan also passed again with the same counts.
|
|
14
|
+
- Reviewed candidate branches cover fresh `--environment local`, exact hook-schema acceptance, preactivation, explicit local binding, and authenticated protocol-v2 WebSocket state. After the production row was added, focused activation/engine/CLI/setup tests passed **296 tests with 3,617 assertions**; preflight and launcher tests separately passed **79 tests with 186 assertions**. Typecheck, build, and generated-surface regeneration passed.
|
|
15
|
+
|
|
16
|
+
## Exact-binary controls
|
|
17
|
+
|
|
18
|
+
Controls used a task-authorized Agent Key resolved only into each process and empty absolute-path isolated homes, except the forced removed-tool control, which used a loopback scripted provider and a dummy key. No personal config or OAuth store was read, copied, or symlinked. Controls ran sequentially. Credential-bearing config was removed after the evidence runs. Raw evidence is under `.claude/kimi-code-research/cert-2.5.1-live/`.
|
|
19
|
+
|
|
20
|
+
- **Plan OFF Write:** the exact 2.5.1 marker was first; Write reached the custom deny-all hook and was denied on the same call ID. No target file or plan state was created.
|
|
21
|
+
- **Plan ON:** authenticated agent state independently proved `plan_mode:true`, auto permission, and the env-model alias before resume. Glob reached the deny hook; a **2,232-byte** plan file was written without a plan-Write hook invocation. The workspace remained empty.
|
|
22
|
+
- **Removed EnterPlanMode:** Write was offered but EnterPlanMode was absent. A scripted provider forced `call_enter_forced`; the exact binary returned `Tool "EnterPlanMode" not found` on that ID before any hook call and returned the rejection to the provider. No plan state or files appeared.
|
|
23
|
+
- **Environment:** real activation and deactivation passed with the explicit local binding. A configured remote default refused before its command launcher ran, disabled workspace trust refused before spawn, and a resumed remote binding refused before spawn.
|
|
24
|
+
- **Swarm OFF:** a real AgentSwarm call returned the inactive-mode result with zero child journals and no probe files.
|
|
25
|
+
- **Swarm ON:** preactivation produced `agent-0` and `agent-1`; each child attempted its assigned Write with a matching managed-denial result under the same toolCallId. No probes landed, and deactivation verified `swarm_mode.exit` with ambient idle reclamation set to 1 ms.
|
|
26
|
+
- **Out-of-root control:** two distinct child Writes were correlated with rescue denials on their assigned call IDs, created no files, and verified deactivation.
|
|
27
|
+
- An initial diagnostic Write attempt used a relative artifact root, causing the spawned CLI to resolve a different home without the control hook. Its Write landed only inside that disposable fixture. The run was rejected as control evidence and superseded by the valid absolute-path plan-OFF run above; it is not counted as a product failure or a hook bypass.
|
|
28
|
+
|
|
29
|
+
## Candidate matrix
|
|
30
|
+
|
|
31
|
+
The candidate matrix (`candidate-smoke.log`) passed **24 tests with 167 assertions and no skips in 358.88 seconds**. Real `runRescue` landed the confined sentinel and persisted operation/version/engine/session/artifact evidence. The absolute out-of-root Bash.cwd call received its exact same-call denial and created no file. Resume and plan-taint refusal passed. Thirteen goal turns each had a matching managed Write denial. Both pursue terminal outcomes passed. Write-swarm produced a **278-byte** child-authored patch, left the user tree clean, removed its worktree, and denied the out-of-root write.
|
|
32
|
+
|
|
33
|
+
## Production matrix and final gates
|
|
34
|
+
|
|
35
|
+
The production-table matrix (`production-smoke.log`) with the candidate override explicitly unset passed **24 tests with 167 assertions and no skips in 364.25 seconds**. It independently repeated the confined rescue and Bash.cwd evidence, resume/taint refusal, both pursue outcomes, both swarm modes, and out-of-root denial. Eleven goal turns each had a matching managed Write denial, and write-swarm again captured a **278-byte** child-authored patch with a clean user tree and removed worktree.
|
|
36
|
+
|
|
37
|
+
Frozen dependency installation checked 43 installs across 88 packages and made no changes. The literal standard `bun run check` passed **1,412 tests with 33 expected opt-in skips, 0 failures, and 18,282 assertions across 82 files in 362.99 seconds**, including the generated-distribution drift gate. Package validation passed **330 files**, all three compiled distributions, isolated hook setup/check and version refusal, a patched DSH host/profile with seven live providers/tools, and Web boot. The isolated npm consumer-security audit also passed. An independent read-only `claude -p` review returned **APPROVE with no must-fix findings** after inspecting the runtime, tests, documentation, diffs, and raw control artifacts. Its remaining notes were non-load-bearing evidence-retention limitations. Supporting logs are retained under `.claude/kimi-code-research/cert-2.5.1-live/`, including `check-standard.log`, `package-standard.log`, `consumer-security.log`, `claude-review.log`, `tag-scan-2.5.0-regression.log`, and `binary-sha256.txt`.
|
|
38
|
+
|
|
39
|
+
## Release clean-close hardening
|
|
40
|
+
|
|
41
|
+
After the original certification and independent review, Linux release CI exposed a platform-specific early WebSocket close event that could be accepted as valid final agent state. The final runtime now requires `CloseEvent.wasClean` before accepting that state, and the mock performs a protocol-correct close handshake while retaining the timeout case. Focused activation validation passed **71 tests with 244 assertions**. The full 2.5.1 production matrix was rerun against the final implementation and again passed **24 tests with 167 assertions and no skips**, this time in **336.76 seconds**, including nine denied goal turns and a 278-byte child-authored patch. Exact 2.5.0 swarm OFF/ON and out-of-root controls also passed. The final full repository check passed **1,412 tests with 18,282 assertions and zero failures in 378.11 seconds**; 330-file package validation and isolated consumer security also passed. These results supplement the original certification and review evidence and cover the final release behavior.
|
|
42
|
+
|
|
43
|
+
A subsequent CI attempt showed that native WebSocket close scheduling and `wasClean` semantics vary by platform and cannot supply a deterministic no-close-event precondition. That timeout case now uses a controlled socket which completes the protocol exchange but never emits `close`, and asserts the exact `Kimi web agent-state verification timed out.` message. The refusal is therefore tested without relying on incidental host close timing; production behavior is unchanged by this test-only follow-up.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi-plugin-cc",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "Kimi for Claude Code, Codex, and DeepSeek Harness: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tools through the local Kimi Code CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/dsh/index.js",
|
|
@@ -134,6 +134,8 @@
|
|
|
134
134
|
"docs/models.md",
|
|
135
135
|
"docs/session-visibility.md",
|
|
136
136
|
"docs/upstream-2.4.1-certification.md",
|
|
137
|
+
"docs/upstream-2.5.0-certification.md",
|
|
138
|
+
"docs/upstream-2.5.1-certification.md",
|
|
137
139
|
"README.md",
|
|
138
140
|
"LICENSE",
|
|
139
141
|
"CHANGELOG.md",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "Kimi for Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP job controls through the local Kimi Code CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "linxule"
|
|
@@ -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
|
|
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),
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
286
|
-
* Layout at 0.42.0:
|
|
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
|
|
290
|
-
*
|
|
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
|
|
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
|
|
351
|
-
if (
|
|
352
|
-
return { kind: "tainted", journal: journalPath, recordType:
|
|
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: {
|