kimi-plugin-cc 2.5.1 → 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 +15 -1
- package/README.md +62 -4
- package/dist/cli-client.js +8 -0
- package/dist/commands/setup.js +78 -51
- package/dist/commands/swarm.js +2 -2
- package/dist/dsh/companion.js +9 -6
- package/dist/dsh/index.js +15 -2
- package/dist/hooks/config-safety.js +5 -2
- package/dist/hooks/managed-block.js +86 -20
- 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 +8 -6
- package/docs/registry-releases.md +66 -3
- package/docs/safety.md +14 -10
- 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 +40 -5
- 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/setup.js +78 -51
- package/plugins/kimi-codex/dist/commands/swarm.js +2 -2
- package/plugins/kimi-codex/dist/dsh/companion.js +9 -6
- package/plugins/kimi-codex/dist/dsh/index.js +15 -2
- package/plugins/kimi-codex/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
- 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/README.md +4 -1
- package/plugins/kimi-dsh/dist/cli-client.js +8 -0
- package/plugins/kimi-dsh/dist/commands/setup.js +78 -51
- package/plugins/kimi-dsh/dist/commands/swarm.js +2 -2
- package/plugins/kimi-dsh/dist/dsh/companion.js +9 -6
- package/plugins/kimi-dsh/dist/dsh/index.js +15 -2
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +86 -20
- 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 +33 -3
|
@@ -45,10 +45,80 @@ const MATCHER_LINE_RE = /^matcher\s*=/;
|
|
|
45
45
|
/** `timeout = <int>` — the only other key our managed [[hooks]] table emits. */
|
|
46
46
|
const TIMEOUT_LINE_RE = /^timeout\s*=\s*\d+\s*$/;
|
|
47
47
|
const HOOKS_TABLE_RE = /^\[\[hooks\]\]\s*$/;
|
|
48
|
+
// Recognize comment lines lexically, not text inside TOML string values.
|
|
49
|
+
// Whole-document parsing remains the authority for TOML validity.
|
|
50
|
+
function scanTomlLines(contents) {
|
|
51
|
+
const markers = new Set();
|
|
52
|
+
const tables = new Set();
|
|
53
|
+
let state = "plain";
|
|
54
|
+
let line = 0;
|
|
55
|
+
let blankPrefix = true;
|
|
56
|
+
for (let i = 0; i < contents.length; i += 1) {
|
|
57
|
+
const c = contents[i];
|
|
58
|
+
if (c === "\n") {
|
|
59
|
+
line += 1;
|
|
60
|
+
blankPrefix = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const onlyWhitespaceBefore = blankPrefix;
|
|
64
|
+
if (c !== " " && c !== "\t" && c !== "\r")
|
|
65
|
+
blankPrefix = false;
|
|
66
|
+
if (state === "plain") {
|
|
67
|
+
if (c === "[" && onlyWhitespaceBefore)
|
|
68
|
+
tables.add(line);
|
|
69
|
+
if (c === "#") {
|
|
70
|
+
if (onlyWhitespaceBefore) {
|
|
71
|
+
const end = contents.indexOf("\n", i);
|
|
72
|
+
const comment = contents.slice(i, end < 0 ? contents.length : end).trim();
|
|
73
|
+
if (BEGIN_LINE_RE.test(comment) || END_LINE_RE.test(comment))
|
|
74
|
+
markers.add(line);
|
|
75
|
+
}
|
|
76
|
+
const end = contents.indexOf("\n", i);
|
|
77
|
+
if (end < 0)
|
|
78
|
+
break;
|
|
79
|
+
i = end - 1;
|
|
80
|
+
}
|
|
81
|
+
else if (c === '"' || c === "'") {
|
|
82
|
+
const triple = contents.slice(i, i + 3) === c.repeat(3);
|
|
83
|
+
state = c === '"' ? (triple ? "multibasic" : "basic") : (triple ? "multiliteral" : "literal");
|
|
84
|
+
if (triple)
|
|
85
|
+
i += 2;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const basic = state === "basic" || state === "multibasic";
|
|
90
|
+
const quote = basic ? '"' : "'";
|
|
91
|
+
if (basic && c === "\\") {
|
|
92
|
+
if (contents[i + 1] === "\n") {
|
|
93
|
+
line += 1;
|
|
94
|
+
blankPrefix = true;
|
|
95
|
+
}
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c !== quote)
|
|
100
|
+
continue;
|
|
101
|
+
if (state === "basic" || state === "literal")
|
|
102
|
+
state = "plain";
|
|
103
|
+
else if (contents.slice(i, i + 3) === quote.repeat(3)) {
|
|
104
|
+
while (contents[i + 1] === quote)
|
|
105
|
+
i += 1;
|
|
106
|
+
state = "plain";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { markers, tables };
|
|
111
|
+
}
|
|
112
|
+
export function markerCommentLines(contents) {
|
|
113
|
+
return scanTomlLines(contents).markers;
|
|
114
|
+
}
|
|
48
115
|
/** Locate every BEGIN/END marker line, in order, with its parsed host id. */
|
|
49
116
|
function findMarkers(lines) {
|
|
50
117
|
const hits = [];
|
|
118
|
+
const comments = markerCommentLines(lines.join("\n"));
|
|
51
119
|
for (let i = 0; i < lines.length; i += 1) {
|
|
120
|
+
if (!comments.has(i))
|
|
121
|
+
continue;
|
|
52
122
|
const trimmed = lines[i].trim();
|
|
53
123
|
const begin = BEGIN_LINE_RE.exec(trimmed);
|
|
54
124
|
if (begin !== null) {
|
|
@@ -66,7 +136,7 @@ function findMarkers(lines) {
|
|
|
66
136
|
}
|
|
67
137
|
/**
|
|
68
138
|
* Pair BEGIN/END markers into well-formed blocks. Return the first orphan
|
|
69
|
-
* (BEGIN-without-END or END-without-BEGIN);
|
|
139
|
+
* (BEGIN-without-END or END-without-BEGIN); verification fails closed on either.
|
|
70
140
|
*/
|
|
71
141
|
function pairBlocks(lines) {
|
|
72
142
|
const markers = findMarkers(lines);
|
|
@@ -83,6 +153,8 @@ function pairBlocks(lines) {
|
|
|
83
153
|
// end
|
|
84
154
|
if (open === null)
|
|
85
155
|
return { orphan: "END-without-BEGIN" };
|
|
156
|
+
if (open.hostId !== marker.hostId)
|
|
157
|
+
return { orphan: "BEGIN-without-END" };
|
|
86
158
|
const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
|
|
87
159
|
blocks.push({
|
|
88
160
|
hostId: open.hostId,
|
|
@@ -436,13 +508,13 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
436
508
|
* The deletion range `[start, end)` is conservative: only the contiguous
|
|
437
509
|
* non-blank body after `[[hooks]]`, stopping at a blank line, table header, or
|
|
438
510
|
* managed marker so adjacent user content is preserved. A separate full-span
|
|
439
|
-
* scan
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* caller's ownership checks.
|
|
511
|
+
* scan refuses any further TOML field before the next table, including quoted
|
|
512
|
+
* and dotted keys, so pruning cannot attach those fields to another table.
|
|
513
|
+
* `command` is decoded for the caller's ownership checks.
|
|
443
514
|
*/
|
|
444
515
|
export function findBareApprovalHookTables(contents) {
|
|
445
516
|
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
517
|
+
const tableLines = scanTomlLines(contents).tables;
|
|
446
518
|
const managed = new Set();
|
|
447
519
|
const paired = pairBlocks(lines);
|
|
448
520
|
if ("blocks" in paired) {
|
|
@@ -455,7 +527,7 @@ export function findBareApprovalHookTables(contents) {
|
|
|
455
527
|
for (let i = 0; i < lines.length; i += 1) {
|
|
456
528
|
if (managed.has(i))
|
|
457
529
|
continue;
|
|
458
|
-
if (!HOOKS_TABLE_RE.test(lines[i].trim()))
|
|
530
|
+
if (!tableLines.has(i) || !HOOKS_TABLE_RE.test(lines[i].trim()))
|
|
459
531
|
continue;
|
|
460
532
|
// Collect the contiguous table body.
|
|
461
533
|
let j = i + 1;
|
|
@@ -500,22 +572,16 @@ export function findBareApprovalHookTables(contents) {
|
|
|
500
572
|
}
|
|
501
573
|
j += 1;
|
|
502
574
|
}
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
// matcher is schema-valid but can disable the hook at runtime. Foreign keys
|
|
508
|
-
// and other TOML spellings are handled by whole-file schema validation and
|
|
509
|
-
// the parser-based installed checks, not by this prune heuristic.
|
|
510
|
-
let matcherBeyondBody = false;
|
|
575
|
+
// Blank lines and marker comments do not end a TOML table. Never leave any
|
|
576
|
+
// field behind to be rebound to a preceding table after pruning, regardless
|
|
577
|
+
// of the field's spelling or whether it is valid for a hook.
|
|
578
|
+
let fieldBeyondBody = false;
|
|
511
579
|
for (let k = j; k < lines.length; k += 1) {
|
|
512
580
|
const t = lines[k].trim();
|
|
513
|
-
|
|
514
|
-
// managed marker; a matcher past that point belongs to a different table.
|
|
515
|
-
if (t.startsWith("[") || BEGIN_LINE_RE.test(t) || END_LINE_RE.test(t))
|
|
581
|
+
if (tableLines.has(k))
|
|
516
582
|
break;
|
|
517
|
-
if (
|
|
518
|
-
|
|
583
|
+
if (t.length > 0 && !t.startsWith("#")) {
|
|
584
|
+
fieldBeyondBody = true;
|
|
519
585
|
break;
|
|
520
586
|
}
|
|
521
587
|
}
|
|
@@ -527,7 +593,7 @@ export function findBareApprovalHookTables(contents) {
|
|
|
527
593
|
commandDecoded.length > 0 &&
|
|
528
594
|
hasPreToolUseEvent &&
|
|
529
595
|
!hasMatcher &&
|
|
530
|
-
!
|
|
596
|
+
!fieldBeyondBody) {
|
|
531
597
|
ranges.push({ start: i, end: j, command: commandDecoded });
|
|
532
598
|
}
|
|
533
599
|
}
|
package/dist/kimi-command.js
CHANGED
|
@@ -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
|
]);
|
package/dist/kimi-engine.js
CHANGED
|
@@ -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: {
|