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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -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 +275 -14
- package/dist/version.js +1 -1
- package/docs/migration.md +4 -4
- package/docs/registry-releases.md +5 -1
- package/docs/safety.md +11 -9
- package/docs/swarm-recovery.md +2 -2
- package/docs/upstream-2.5.0-certification.md +50 -0
- package/docs/upstream-2.5.1-certification.md +45 -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 +275 -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 +275 -14
- package/plugins/kimi-dsh/dist/version.js +1 -1
- package/plugins/kimi-dsh/package.json +1 -1
|
@@ -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: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { constants as fsConstants } from "node:fs";
|
|
3
4
|
import { open } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -9,7 +10,7 @@ import { assertExecutionPlanMatchesSpawn, assertNoUnsafeExperimentalSelector, }
|
|
|
9
10
|
import { resolveKimiHome } from "./kimi-home.js";
|
|
10
11
|
import { assertNativeV2Preflight } from "./native-v2-preflight.js";
|
|
11
12
|
import { collectDescendantIdentities, collectProcessGroupsIdentities, probeProcessIdentity, revalidateProcessIdentities, waitForProcessTreeExit, } from "./process-tree.js";
|
|
12
|
-
const PREACTIVATION_VERSIONS = ["2.3.2", "2.3.3", "2.4.0", "2.4.1"];
|
|
13
|
+
const PREACTIVATION_VERSIONS = ["2.3.2", "2.3.3", "2.4.0", "2.4.1", "2.5.0", "2.5.1"];
|
|
13
14
|
const READY_TIMEOUT_MS = 10_000;
|
|
14
15
|
const REST_TIMEOUT_MS = 5_000;
|
|
15
16
|
// Exact 2.4.0 can take longer than 5s to drain session/workspace lifecycle work.
|
|
@@ -18,6 +19,8 @@ const JOURNAL_TIMEOUT_MS = 2_000;
|
|
|
18
19
|
const JOURNAL_POLL_MS = 50;
|
|
19
20
|
const READY_LINE_MAX_CHARS = 65_536;
|
|
20
21
|
const REST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
22
|
+
const WEBSOCKET_MAX_FRAMES = 64;
|
|
23
|
+
const WEBSOCKET_RESPONSE_MAX_CHARS = 1024 * 1024;
|
|
21
24
|
const JOURNAL_MAX_BYTES = 64 * 1024 * 1024;
|
|
22
25
|
const DESCENDANT_SNAPSHOT_TIMEOUT_MS = 2_000;
|
|
23
26
|
const DESCENDANT_TRACK_INTERVAL_MS = 50;
|
|
@@ -39,11 +42,12 @@ export async function preactivateSwarmSession(options) {
|
|
|
39
42
|
web = await startWebChild(options, prepared.env, failureCode);
|
|
40
43
|
await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
|
|
41
44
|
const model = await resolveProfileModel(web, options, failureCode);
|
|
42
|
-
identity = await createSession(web, options, failureCode);
|
|
45
|
+
identity = await createSession(web, options, prepared.expectedVersion, failureCode);
|
|
46
|
+
await verifyLocalEnvironment(web, identity, options.cwd, prepared.expectedVersion, options.signal, failureCode);
|
|
43
47
|
options.onRecoveryEvent?.({ kind: "session", ...identity });
|
|
44
48
|
profileMutationAttempted = true;
|
|
45
49
|
await postSwarmProfile(web, identity, true, model, options.signal, failureCode);
|
|
46
|
-
await verifySwarmStatus(web, identity, true, model, options.signal, failureCode);
|
|
50
|
+
await verifySwarmStatus(web, identity, true, model, prepared.expectedVersion, options.signal, failureCode);
|
|
47
51
|
await shutdownWeb(web, options.signal, failureCode);
|
|
48
52
|
await requireSwarmJournal(prepared.kimiHome, identity, "enter", options.signal, failureCode);
|
|
49
53
|
throwIfAborted(options.signal, failureCode, "swarm-activation.complete");
|
|
@@ -86,8 +90,9 @@ export async function deactivateSwarmSession(options) {
|
|
|
86
90
|
throwIfAborted(options.signal, failureCode, "swarm-deactivation.pre-spawn");
|
|
87
91
|
web = await startWebChild(options, prepared.env, failureCode);
|
|
88
92
|
await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
|
|
93
|
+
await verifyLocalEnvironment(web, options, options.cwd, prepared.expectedVersion, options.signal, failureCode);
|
|
89
94
|
await postSwarmProfile(web, options, false, undefined, options.signal, failureCode);
|
|
90
|
-
await verifySwarmStatus(web, options, false, undefined, options.signal, failureCode);
|
|
95
|
+
await verifySwarmStatus(web, options, false, undefined, prepared.expectedVersion, options.signal, failureCode);
|
|
91
96
|
await shutdownWeb(web, options.signal, failureCode);
|
|
92
97
|
await requireSwarmJournal(prepared.kimiHome, options, "exit", options.signal, failureCode);
|
|
93
98
|
options.onRecoveryEvent?.({ kind: "disabled" });
|
|
@@ -124,6 +129,7 @@ async function prepareWebSpawn(options, failureCode) {
|
|
|
124
129
|
await assertNativeV2Preflight({
|
|
125
130
|
kimiHome,
|
|
126
131
|
env: options.env,
|
|
132
|
+
expectedExecutionRoot: options.cwd,
|
|
127
133
|
stage: "swarm-activation.pre-spawn",
|
|
128
134
|
});
|
|
129
135
|
}
|
|
@@ -329,13 +335,29 @@ async function resolveProfileModel(web, options, failureCode) {
|
|
|
329
335
|
}
|
|
330
336
|
return data.default_model;
|
|
331
337
|
}
|
|
332
|
-
async function createSession(web, options, failureCode) {
|
|
333
|
-
const
|
|
338
|
+
async function createSession(web, options, expectedVersion, failureCode) {
|
|
339
|
+
const request = { metadata: { cwd: options.cwd } };
|
|
340
|
+
if (["2.5.0", "2.5.1"].includes(expectedVersion)) {
|
|
341
|
+
request.environment_id = "local";
|
|
342
|
+
request.environment_cwd = options.cwd;
|
|
343
|
+
}
|
|
344
|
+
const data = await restData(web, "POST", "/api/v1/sessions", request, options.signal, failureCode);
|
|
334
345
|
if (!isRecord(data) || !validPathId(data.id) || !validPathId(data.workspace_id)) {
|
|
335
346
|
throw failure(failureCode, "swarm-activation.create-session", "Kimi web returned an invalid session identity.");
|
|
336
347
|
}
|
|
337
348
|
return { sessionId: data.id, workspaceId: data.workspace_id };
|
|
338
349
|
}
|
|
350
|
+
async function verifyLocalEnvironment(web, identity, expectedCwd, expectedVersion, signal, failureCode) {
|
|
351
|
+
if (!["2.5.0", "2.5.1"].includes(expectedVersion))
|
|
352
|
+
return;
|
|
353
|
+
const data = await restData(web, "GET", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/environment`, undefined, signal, failureCode);
|
|
354
|
+
if (!isRecord(data)) {
|
|
355
|
+
throw failure(failureCode, "swarm-activation.environment", "Kimi web /environment returned an invalid payload.");
|
|
356
|
+
}
|
|
357
|
+
if (data.workspace_id !== identity.workspaceId || data.environment_id !== "local" || data.cwd !== expectedCwd) {
|
|
358
|
+
throw failure(failureCode, "swarm-activation.environment", "Kimi web /environment did not confirm the expected local workspace binding.");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
339
361
|
async function postSwarmProfile(web, identity, active, model, signal, failureCode) {
|
|
340
362
|
const agentConfig = {
|
|
341
363
|
swarm_mode: active,
|
|
@@ -345,24 +367,263 @@ async function postSwarmProfile(web, identity, active, model, signal, failureCod
|
|
|
345
367
|
agentConfig.model = model;
|
|
346
368
|
await restData(web, "POST", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/profile`, { agent_config: agentConfig }, signal, failureCode);
|
|
347
369
|
}
|
|
348
|
-
async function verifySwarmStatus(web, identity, active, model, signal, failureCode) {
|
|
349
|
-
const data =
|
|
370
|
+
async function verifySwarmStatus(web, identity, active, model, expectedVersion, signal, failureCode) {
|
|
371
|
+
const data = ["2.5.0", "2.5.1"].includes(expectedVersion)
|
|
372
|
+
? await readWebSocketAgentState(web, identity.sessionId, signal, failureCode)
|
|
373
|
+
: await restData(web, "GET", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/status`, undefined, signal, failureCode);
|
|
374
|
+
const source = ["2.5.0", "2.5.1"].includes(expectedVersion) ? "agent state" : "/status";
|
|
350
375
|
if (!isRecord(data)) {
|
|
351
|
-
throw failure(failureCode, "swarm-activation.status",
|
|
376
|
+
throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} returned an invalid payload.`);
|
|
352
377
|
}
|
|
353
378
|
if (data.swarm_mode !== active) {
|
|
354
|
-
throw failure(failureCode, "swarm-activation.status", `Kimi web
|
|
379
|
+
throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} did not confirm swarm_mode=${active}.`, { expected_swarm_mode: active });
|
|
355
380
|
}
|
|
356
381
|
if (data.plan_mode !== false) {
|
|
357
|
-
throw failure(failureCode, "swarm-activation.status",
|
|
382
|
+
throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} did not confirm plan mode is off.`);
|
|
358
383
|
}
|
|
359
|
-
if (data.
|
|
360
|
-
throw failure(failureCode, "swarm-activation.status", "Kimi web
|
|
384
|
+
if (["2.5.0", "2.5.1"].includes(expectedVersion) && data.tower_mode !== false) {
|
|
385
|
+
throw failure(failureCode, "swarm-activation.status", "Kimi web agent state did not confirm tower mode is off.");
|
|
386
|
+
}
|
|
387
|
+
const permission = ["2.5.0", "2.5.1"].includes(expectedVersion) ? data.permission_mode : data.permission;
|
|
388
|
+
if (permission !== "auto") {
|
|
389
|
+
throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} did not confirm auto permission mode.`);
|
|
361
390
|
}
|
|
362
391
|
if (active && data.model !== model) {
|
|
363
|
-
throw failure(failureCode, "swarm-activation.status",
|
|
392
|
+
throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} did not confirm the selected model alias.`);
|
|
364
393
|
}
|
|
365
394
|
}
|
|
395
|
+
async function readWebSocketAgentState(web, sessionId, signal, failureCode) {
|
|
396
|
+
throwIfAborted(signal, failureCode, "swarm-activation.status");
|
|
397
|
+
await refreshOwnedDescendants(web);
|
|
398
|
+
throwIfAborted(signal, failureCode, "swarm-activation.status");
|
|
399
|
+
try {
|
|
400
|
+
return await new Promise((resolve, reject) => {
|
|
401
|
+
let socket;
|
|
402
|
+
const bearerProtocol = `kimi-code.bearer.${web.token}`;
|
|
403
|
+
const helloId = `swarm-activation-hello-${randomUUID()}`;
|
|
404
|
+
const subscribeId = `swarm-activation-subscribe-${randomUUID()}`;
|
|
405
|
+
try {
|
|
406
|
+
const url = new URL(web.origin);
|
|
407
|
+
url.protocol = "ws:";
|
|
408
|
+
url.pathname = "/api/v1/ws";
|
|
409
|
+
socket = new WebSocket(url, bearerProtocol);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
reject(failure(failureCode, "swarm-activation.status", "Could not open the authenticated Kimi web agent-state channel."));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let settled = false;
|
|
416
|
+
let serverHello = false;
|
|
417
|
+
let clientHelloAcknowledged = false;
|
|
418
|
+
let subscribeSent = false;
|
|
419
|
+
let subscriptionAcknowledged = false;
|
|
420
|
+
let closeRequested = false;
|
|
421
|
+
let latestState;
|
|
422
|
+
let frames = 0;
|
|
423
|
+
let receivedChars = 0;
|
|
424
|
+
const timer = setTimeout(() => {
|
|
425
|
+
settle(failure(failureCode, "swarm-activation.status", "Kimi web agent-state verification timed out.", { timeout_ms: REST_TIMEOUT_MS }));
|
|
426
|
+
}, REST_TIMEOUT_MS);
|
|
427
|
+
const cleanup = () => {
|
|
428
|
+
clearTimeout(timer);
|
|
429
|
+
signal?.removeEventListener("abort", onAbort);
|
|
430
|
+
socket.removeEventListener("open", onOpen);
|
|
431
|
+
socket.removeEventListener("message", onMessage);
|
|
432
|
+
socket.removeEventListener("error", onError);
|
|
433
|
+
socket.removeEventListener("close", onClose);
|
|
434
|
+
try {
|
|
435
|
+
socket.close();
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const settle = (result) => {
|
|
441
|
+
if (settled)
|
|
442
|
+
return;
|
|
443
|
+
settled = true;
|
|
444
|
+
cleanup();
|
|
445
|
+
if (result instanceof RuntimeError)
|
|
446
|
+
reject(result);
|
|
447
|
+
else
|
|
448
|
+
resolve(result);
|
|
449
|
+
};
|
|
450
|
+
const rejectState = (message) => {
|
|
451
|
+
settle(failure(failureCode, "swarm-activation.status", message));
|
|
452
|
+
};
|
|
453
|
+
const send = (frame) => {
|
|
454
|
+
try {
|
|
455
|
+
socket.send(JSON.stringify(frame));
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
rejectState("Could not send a Kimi web agent-state control frame.");
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
const completeIfReady = () => {
|
|
464
|
+
if (serverHello &&
|
|
465
|
+
clientHelloAcknowledged &&
|
|
466
|
+
subscriptionAcknowledged &&
|
|
467
|
+
latestState !== undefined &&
|
|
468
|
+
!closeRequested) {
|
|
469
|
+
closeRequested = true;
|
|
470
|
+
try {
|
|
471
|
+
socket.close();
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
rejectState("Could not close the Kimi web agent-state channel for final validation.");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const onOpen = () => {
|
|
479
|
+
if (socket.protocol !== bearerProtocol) {
|
|
480
|
+
rejectState("Kimi web did not authenticate the agent-state channel with the expected subprotocol.");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
send({
|
|
484
|
+
type: "client_hello",
|
|
485
|
+
id: helloId,
|
|
486
|
+
payload: { client_id: "kimi-plugin-cc-swarm-activation" },
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
const onMessage = (event) => {
|
|
490
|
+
if (settled)
|
|
491
|
+
return;
|
|
492
|
+
if (typeof event.data !== "string") {
|
|
493
|
+
rejectState("Kimi web agent-state channel returned a non-text frame.");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
frames += 1;
|
|
497
|
+
receivedChars += event.data.length;
|
|
498
|
+
if (frames > WEBSOCKET_MAX_FRAMES || receivedChars > WEBSOCKET_RESPONSE_MAX_CHARS) {
|
|
499
|
+
rejectState("Kimi web agent-state channel exceeded the bounded response limit.");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
let frame;
|
|
503
|
+
try {
|
|
504
|
+
frame = JSON.parse(event.data);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
rejectState("Kimi web agent-state channel returned malformed JSON.");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (!isRecord(frame) || typeof frame.type !== "string") {
|
|
511
|
+
rejectState("Kimi web agent-state channel returned an invalid frame.");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (frame.type === "server_hello") {
|
|
515
|
+
if (serverHello || !isRecord(frame.payload) ||
|
|
516
|
+
frame.payload.protocol_version !== 2 ||
|
|
517
|
+
typeof frame.payload.ws_connection_id !== "string" ||
|
|
518
|
+
!isRecord(frame.payload.capabilities) ||
|
|
519
|
+
frame.payload.capabilities.event_batching !== false ||
|
|
520
|
+
frame.payload.capabilities.compression !== false) {
|
|
521
|
+
rejectState("Kimi web agent-state channel did not provide the expected protocol-v2 hello.");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
serverHello = true;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (!serverHello) {
|
|
528
|
+
rejectState("Kimi web agent-state channel sent data before its protocol hello.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (frame.type === "ack") {
|
|
532
|
+
if (typeof frame.id !== "string" || typeof frame.code !== "number" || !isRecord(frame.payload)) {
|
|
533
|
+
rejectState("Kimi web agent-state channel returned an invalid acknowledgement.");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (frame.id === helloId) {
|
|
537
|
+
if (clientHelloAcknowledged || frame.code !== 0) {
|
|
538
|
+
rejectState("Kimi web agent-state channel rejected the client hello.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
clientHelloAcknowledged = true;
|
|
542
|
+
subscribeSent = send({
|
|
543
|
+
type: "subscribe_v2",
|
|
544
|
+
id: subscribeId,
|
|
545
|
+
payload: { session_id: sessionId, transcript: { main: "block" } },
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (frame.id === subscribeId) {
|
|
550
|
+
if (subscriptionAcknowledged || !subscribeSent || frame.code !== 0 ||
|
|
551
|
+
!Array.isArray(frame.payload.accepted) ||
|
|
552
|
+
frame.payload.accepted.length !== 1 ||
|
|
553
|
+
frame.payload.accepted[0] !== sessionId ||
|
|
554
|
+
!Array.isArray(frame.payload.not_found) ||
|
|
555
|
+
frame.payload.not_found.length !== 0) {
|
|
556
|
+
rejectState("Kimi web agent-state channel did not accept the exact main-session subscription.");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
subscriptionAcknowledged = true;
|
|
560
|
+
completeIfReady();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
rejectState("Kimi web agent-state channel returned an unexpected acknowledgement.");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (frame.type === "agent") {
|
|
567
|
+
if (!subscribeSent || !isAgentStateFrame(frame, sessionId)) {
|
|
568
|
+
rejectState("Kimi web agent-state channel returned invalid or misbound main-agent state.");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
latestState = frame;
|
|
572
|
+
completeIfReady();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (frame.type === "ping") {
|
|
576
|
+
if (isRecord(frame.payload) && typeof frame.payload.nonce === "string") {
|
|
577
|
+
send({ type: "pong", payload: { nonce: frame.payload.nonce } });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const onAbort = () => {
|
|
582
|
+
settle(cancelledFailure(failureCode, "swarm-activation.status"));
|
|
583
|
+
};
|
|
584
|
+
const onError = () => {
|
|
585
|
+
rejectState("Kimi web agent-state channel failed before valid state was received.");
|
|
586
|
+
};
|
|
587
|
+
const onClose = () => {
|
|
588
|
+
if (closeRequested &&
|
|
589
|
+
serverHello &&
|
|
590
|
+
clientHelloAcknowledged &&
|
|
591
|
+
subscriptionAcknowledged &&
|
|
592
|
+
latestState !== undefined) {
|
|
593
|
+
settle(latestState);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
rejectState("Kimi web agent-state channel closed before valid state was received.");
|
|
597
|
+
};
|
|
598
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
599
|
+
socket.addEventListener("open", onOpen);
|
|
600
|
+
socket.addEventListener("message", onMessage);
|
|
601
|
+
socket.addEventListener("error", onError);
|
|
602
|
+
socket.addEventListener("close", onClose);
|
|
603
|
+
if (signal?.aborted === true)
|
|
604
|
+
onAbort();
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
await refreshOwnedDescendants(web);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function isAgentStateFrame(frame, sessionId) {
|
|
612
|
+
return frame.session_id === sessionId &&
|
|
613
|
+
frame.agent_id === "main" &&
|
|
614
|
+
typeof frame.event_created_at === "string" &&
|
|
615
|
+
Number.isFinite(Date.parse(frame.event_created_at)) &&
|
|
616
|
+
(frame.status === "idle" || frame.status === "running") &&
|
|
617
|
+
(frame.model === undefined || typeof frame.model === "string") &&
|
|
618
|
+
(frame.thinking === undefined || typeof frame.thinking === "string") &&
|
|
619
|
+
(frame.permission_mode === undefined ||
|
|
620
|
+
frame.permission_mode === "manual" ||
|
|
621
|
+
frame.permission_mode === "yolo" ||
|
|
622
|
+
frame.permission_mode === "auto") &&
|
|
623
|
+
typeof frame.plan_mode === "boolean" &&
|
|
624
|
+
typeof frame.swarm_mode === "boolean" &&
|
|
625
|
+
typeof frame.tower_mode === "boolean";
|
|
626
|
+
}
|
|
366
627
|
async function shutdownWeb(web, signal, failureCode) {
|
|
367
628
|
await restData(web, "POST", "/api/v1/shutdown", undefined, signal, failureCode);
|
|
368
629
|
const settlement = await waitForChildClose(web, signal, failureCode);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi-plugin-cc",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"description": "Kimi Code subagents for DeepSeek Harness: all seven ask, review, challenge, rescue, pursue, swarm, and swarm-write operations through the kimi-plugin-cc companion runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/dsh/index.js",
|