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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +15 -1
  4. package/README.md +62 -4
  5. package/dist/cli-client.js +8 -0
  6. package/dist/commands/setup.js +78 -51
  7. package/dist/commands/swarm.js +2 -2
  8. package/dist/dsh/companion.js +9 -6
  9. package/dist/dsh/index.js +15 -2
  10. package/dist/hooks/config-safety.js +5 -2
  11. package/dist/hooks/managed-block.js +86 -20
  12. package/dist/kimi-command.js +1 -0
  13. package/dist/kimi-engine.js +3 -0
  14. package/dist/native-v2-preflight.js +138 -30
  15. package/dist/swarm-activation.js +276 -14
  16. package/dist/version.js +1 -1
  17. package/docs/migration.md +8 -6
  18. package/docs/registry-releases.md +66 -3
  19. package/docs/safety.md +14 -10
  20. package/docs/swarm-recovery.md +2 -2
  21. package/docs/upstream-2.5.0-certification.md +48 -0
  22. package/docs/upstream-2.5.1-certification.md +43 -0
  23. package/package.json +40 -5
  24. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  25. package/plugins/kimi-codex/dist/cli-client.js +8 -0
  26. package/plugins/kimi-codex/dist/commands/setup.js +78 -51
  27. package/plugins/kimi-codex/dist/commands/swarm.js +2 -2
  28. package/plugins/kimi-codex/dist/dsh/companion.js +9 -6
  29. package/plugins/kimi-codex/dist/dsh/index.js +15 -2
  30. package/plugins/kimi-codex/dist/hooks/config-safety.js +5 -2
  31. package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
  32. package/plugins/kimi-codex/dist/kimi-command.js +1 -0
  33. package/plugins/kimi-codex/dist/kimi-engine.js +3 -0
  34. package/plugins/kimi-codex/dist/native-v2-preflight.js +138 -30
  35. package/plugins/kimi-codex/dist/swarm-activation.js +276 -14
  36. package/plugins/kimi-codex/dist/version.js +1 -1
  37. package/plugins/kimi-dsh/README.md +4 -1
  38. package/plugins/kimi-dsh/dist/cli-client.js +8 -0
  39. package/plugins/kimi-dsh/dist/commands/setup.js +78 -51
  40. package/plugins/kimi-dsh/dist/commands/swarm.js +2 -2
  41. package/plugins/kimi-dsh/dist/dsh/companion.js +9 -6
  42. package/plugins/kimi-dsh/dist/dsh/index.js +15 -2
  43. package/plugins/kimi-dsh/dist/hooks/config-safety.js +5 -2
  44. package/plugins/kimi-dsh/dist/hooks/managed-block.js +86 -20
  45. package/plugins/kimi-dsh/dist/kimi-command.js +1 -0
  46. package/plugins/kimi-dsh/dist/kimi-engine.js +3 -0
  47. package/plugins/kimi-dsh/dist/native-v2-preflight.js +138 -30
  48. package/plugins/kimi-dsh/dist/swarm-activation.js +276 -14
  49. package/plugins/kimi-dsh/dist/version.js +1 -1
  50. package/plugins/kimi-dsh/package.json +33 -3
@@ -13,9 +13,10 @@
13
13
  // B. the EnterPlanMode tool (already hook-denied for every label);
14
14
  // C. resuming a session whose agent wire journal holds a durable
15
15
  // `plan_mode.enter` record (restore() folds ONLY agents/*/wire.jsonl).
16
- // This module closes A and C before any process is spawned, and refuses the
16
+ // This module closes A and C before any process is spawned, refuses the
17
17
  // v2-only experimental features (tower, subagent fork) that this plugin does
18
- // not certify. B stays in runtime/hooks/approval-policy.ts.
18
+ // not certify, and requires local execution for configured and resumed sessions.
19
+ // B stays in runtime/hooks/approval-policy.ts.
19
20
  //
20
21
  // Contract:
21
22
  // - Pure inspection: never writes, never rewrites operator config, never
@@ -155,6 +156,48 @@ export async function inspectPlanModeConfig(kimiHome) {
155
156
  reason: `\`${offending.join("`, `")}\` (upstream setting \`default_plan_mode\`) is set in ${configPath} and is not \`false\`.`,
156
157
  };
157
158
  }
159
+ export async function inspectEnvironmentConfig(kimiHome) {
160
+ const parsed = await readParsedConfig(kimiHome);
161
+ if (parsed.kind === "unreadable") {
162
+ return { kind: "refuse", refusalKind: "v2-config-unreadable", reason: parsed.reason };
163
+ }
164
+ const section = parsed.config?.["environments"];
165
+ if (section !== undefined) {
166
+ if (section === null || typeof section !== "object" || Array.isArray(section)) {
167
+ return {
168
+ kind: "refuse",
169
+ refusalKind: "v2-environment-configured",
170
+ reason: "The [environments] section is not a table, so the execution environment cannot be verified as local.",
171
+ };
172
+ }
173
+ if (section["default"] !== undefined) {
174
+ return {
175
+ kind: "refuse",
176
+ refusalKind: "v2-environment-configured",
177
+ reason: "A default remote execution environment is configured in [environments]; plugin-managed jobs require the local environment.",
178
+ };
179
+ }
180
+ }
181
+ const workspace = parsed.config?.["workspace"];
182
+ if (workspace !== undefined) {
183
+ if (workspace === null || typeof workspace !== "object" || Array.isArray(workspace)) {
184
+ return {
185
+ kind: "refuse",
186
+ refusalKind: "v2-workspace-trust-disabled",
187
+ reason: "The [workspace] section is not a table, so project trust requirements cannot be verified.",
188
+ };
189
+ }
190
+ const trustDisabled = Object.entries(workspace).some(([key, value]) => upstreamSnakeToCamel(key) === "requireTrust" && value !== true);
191
+ if (trustDisabled) {
192
+ return {
193
+ kind: "refuse",
194
+ refusalKind: "v2-workspace-trust-disabled",
195
+ reason: "Workspace trust is disabled in [workspace]; plugin-managed jobs require project trust enforcement to remain enabled.",
196
+ };
197
+ }
198
+ }
199
+ return { kind: "ok" };
200
+ }
158
201
  /**
159
202
  * Refuse v2-only experimental features this plugin does not certify.
160
203
  * Sources (per-flag precedence upstream is env → [experimental] config →
@@ -239,40 +282,39 @@ async function openJournalSafely(journalPath, maxBytes) {
239
282
  }
240
283
  return { kind: "ok", handle };
241
284
  }
242
- /**
243
- * Scan one open journal's lines for a plan-mode control record. Returns the
244
- * record `type` string on a match, "<unparseable>" for a suspicious line
245
- * that fails to parse, or null when the journal is clean.
246
- */
247
- async function scanJournalLines(handle) {
285
+ async function scanJournalLines(handle, expectedExecutionRoot) {
248
286
  const rl = createInterface({ input: handle.createReadStream() });
249
287
  try {
250
288
  for await (const rawLine of rl) {
251
289
  const line = rawLine.trim();
252
290
  if (line.length === 0)
253
291
  continue;
254
- // Parse EVERY record and check its DECODED `type`. A raw-substring filter
255
- // is unsafe on its own: a JSON-escaped type such as
256
- // {"type":"plan_mode.enter"} contains no literal "plan_mode." yet
257
- // upstream JSON.parse decodes it to "plan_mode.enter" and restores active
258
- // plan mode. So parse first (this catches the escape). If a line FAILS to
259
- // parse, upstream cannot fold it into a plan event either — but be
260
- // conservative: a malformed line that still LOOKS plan-related is treated
261
- // as tainted rather than skipped, so a hand-corrupted journal cannot hide
262
- // a plan record behind a parse error.
263
292
  let parsed;
264
293
  try {
265
294
  parsed = JSON.parse(line);
266
295
  }
267
296
  catch {
268
297
  if (line.includes('"plan_mode.') || line.includes('"plan.')) {
269
- return "<unparseable>";
298
+ return { kind: "tainted", recordType: "<unparseable>" };
299
+ }
300
+ if (line.includes('"environment.set_binding')) {
301
+ return { kind: "environment", recordType: "<unparseable-environment-binding>" };
270
302
  }
271
303
  continue;
272
304
  }
273
- const type = parsed?.["type"];
305
+ const record = parsed;
306
+ const type = record?.["type"];
274
307
  if (typeof type === "string" && (type.startsWith("plan_mode.") || type.startsWith("plan."))) {
275
- return type;
308
+ return { kind: "tainted", recordType: type };
309
+ }
310
+ if (type === "environment.set_binding") {
311
+ if (record?.["environmentId"] !== "local") {
312
+ return { kind: "environment", recordType: type };
313
+ }
314
+ const cwd = record?.["cwd"];
315
+ if (expectedExecutionRoot !== undefined && cwd !== undefined && cwd !== expectedExecutionRoot) {
316
+ return { kind: "environment", recordType: "environment.set_binding.cwd" };
317
+ }
276
318
  }
277
319
  }
278
320
  return null;
@@ -281,16 +323,48 @@ async function scanJournalLines(handle) {
281
323
  rl.close();
282
324
  }
283
325
  }
326
+ async function inspectSessionMetadataEnvironment(statePath, expectedExecutionRoot) {
327
+ const read = await readSmallFileSafe(statePath, CONFIG_MAX_BYTES);
328
+ if (read.kind === "absent")
329
+ return null;
330
+ if (read.kind === "unreadable") {
331
+ return { kind: "unavailable", reason: `${statePath}: ${read.reason}` };
332
+ }
333
+ let metadata;
334
+ try {
335
+ metadata = JSON.parse(read.contents);
336
+ }
337
+ catch {
338
+ return { kind: "unavailable", reason: `${statePath}: malformed session metadata` };
339
+ }
340
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
341
+ return { kind: "unavailable", reason: `${statePath}: invalid session metadata` };
342
+ }
343
+ const record = metadata;
344
+ const environmentId = record["environmentId"];
345
+ if (environmentId !== undefined && environmentId !== "local") {
346
+ return { kind: "environment", journal: statePath, recordType: "session.metadata.environmentId" };
347
+ }
348
+ const environmentCwd = record["environmentCwd"];
349
+ if ((environmentId === undefined || environmentId === "local") &&
350
+ expectedExecutionRoot !== undefined &&
351
+ environmentCwd !== undefined &&
352
+ environmentCwd !== expectedExecutionRoot) {
353
+ return { kind: "environment", journal: statePath, recordType: "session.metadata.environmentCwd" };
354
+ }
355
+ return null;
356
+ }
284
357
  /**
285
- * Scan every agent wire journal of a saved session for plan-mode records.
286
- * Layout at 0.42.0: `<kimiHome>/sessions/<workspaceId>/<sessionId>/agents/<agentId>/wire.jsonl`
358
+ * Scan saved session metadata and every agent wire journal for plan-mode records
359
+ * and nonlocal environment bindings. Layout at 0.42.0:
360
+ * `<kimiHome>/sessions/<workspaceId>/<sessionId>/agents/<agentId>/wire.jsonl`
287
361
  * (workspaceId is unknown to the plugin → enumerate `sessions/*` with bounded
288
362
  * readdir). Raw-line scan of EVERY agent journal: a JSON line whose `type`
289
- * starts with `plan_mode.` or `plan.` → tainted (fail-closed on a line that
290
- * looks suspicious but does not parse). No session dir, no journal, unreadable,
363
+ * starts with `plan_mode.` or `plan.` → tainted; `environment.set_binding` must
364
+ * name `environmentId:"local"`. No session dir, no journal, unreadable metadata,
291
365
  * or any single journal above the size cap → unavailable. Never modifies files.
292
366
  */
293
- export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
367
+ export async function scanSessionJournalsForPlan(kimiHome, sessionId, expectedExecutionRoot) {
294
368
  const sessionsDir = path.join(kimiHome, "sessions");
295
369
  const workspaceEntries = await boundedReaddir(sessionsDir, DIR_ENUM_CAP);
296
370
  if (workspaceEntries.kind === "absent") {
@@ -316,7 +390,11 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
316
390
  }
317
391
  if (!workspaceEntry.isDirectory())
318
392
  continue;
319
- const agentsDir = path.join(sessionsDir, workspaceEntry.name, sessionId, "agents");
393
+ const sessionDir = path.join(sessionsDir, workspaceEntry.name, sessionId);
394
+ const metadataFinding = await inspectSessionMetadataEnvironment(path.join(sessionDir, "state.json"), expectedExecutionRoot);
395
+ if (metadataFinding !== null)
396
+ return metadataFinding;
397
+ const agentsDir = path.join(sessionDir, "agents");
320
398
  const agentEntries = await boundedReaddir(agentsDir, DIR_ENUM_CAP);
321
399
  if (agentEntries.kind === "absent")
322
400
  continue;
@@ -347,9 +425,12 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
347
425
  return { kind: "unavailable", reason: `${journalPath}: ${opened.reason}` };
348
426
  }
349
427
  try {
350
- const taintedType = await scanJournalLines(opened.handle);
351
- if (taintedType !== null) {
352
- return { kind: "tainted", journal: journalPath, recordType: taintedType };
428
+ const finding = await scanJournalLines(opened.handle, expectedExecutionRoot);
429
+ if (finding?.kind === "tainted") {
430
+ return { kind: "tainted", journal: journalPath, recordType: finding.recordType };
431
+ }
432
+ if (finding?.kind === "environment") {
433
+ return { kind: "environment", journal: journalPath, recordType: finding.recordType };
353
434
  }
354
435
  }
355
436
  finally {
@@ -363,6 +444,8 @@ export async function scanSessionJournalsForPlan(kimiHome, sessionId) {
363
444
  * CLI_V2_PLAN_MODE_CONFIGURED details.refusal_kind = "v2-plan-mode-configured"
364
445
  * CLI_V2_CONFIG_UNREADABLE details.refusal_kind = "v2-config-unreadable"
365
446
  * CLI_V2_EXPERIMENTAL_UNSAFE details.refusal_kind = "v2-experimental-unsafe", details.selectors
447
+ * CLI_V2_ENVIRONMENT_UNSAFE / CLI_V2_WORKSPACE_TRUST_UNSAFE for unsafe config
448
+ * KIMI_SESSION_ENVIRONMENT_UNSAFE details.refusal_kind = "session-environment-unsafe"
366
449
  * KIMI_SESSION_PLAN_TAINTED details.refusal_kind = "session-plan-tainted", details.record_type
367
450
  * KIMI_SESSION_JOURNAL_UNAVAILABLE details.refusal_kind = "session-journal-unavailable"
368
451
  * Every message carries a truthful remedy (edit the named setting / start a
@@ -384,6 +467,22 @@ export async function assertNativeV2Preflight(options) {
384
467
  details: { refusal_kind: planMode.refusalKind, retryable_after_setup: false },
385
468
  });
386
469
  }
470
+ const environment = await inspectEnvironmentConfig(options.kimiHome);
471
+ if (environment.kind === "refuse") {
472
+ const code = environment.refusalKind === "v2-environment-configured"
473
+ ? "CLI_V2_ENVIRONMENT_UNSAFE"
474
+ : environment.refusalKind === "v2-workspace-trust-disabled"
475
+ ? "CLI_V2_WORKSPACE_TRUST_UNSAFE"
476
+ : "CLI_V2_CONFIG_UNREADABLE";
477
+ const remedy = environment.refusalKind === "v2-environment-configured"
478
+ ? `Remove \`environments.default\` in ${configPath} so plugin-managed sessions stay local.`
479
+ : environment.refusalKind === "v2-workspace-trust-disabled"
480
+ ? `Set \`workspace.require_trust = true\` (or remove it) in ${configPath}.`
481
+ : `Fix or remove the unreadable file at ${configPath} so the execution environment can be verified as local.`;
482
+ throw new RuntimeError(code, `${environment.reason} ${remedy}`, stage, {
483
+ details: { refusal_kind: environment.refusalKind, retryable_after_setup: false },
484
+ });
485
+ }
387
486
  const experimental = await inspectExperimentalSelectors(options.kimiHome, options.env);
388
487
  if (experimental.kind === "refuse") {
389
488
  throw new RuntimeError("CLI_V2_EXPERIMENTAL_UNSAFE", `${experimental.reason} Unset the listed selector(s) before retrying.`, stage, {
@@ -395,7 +494,16 @@ export async function assertNativeV2Preflight(options) {
395
494
  });
396
495
  }
397
496
  if (options.resumeSessionId !== undefined) {
398
- const journalScan = await scanSessionJournalsForPlan(options.kimiHome, options.resumeSessionId);
497
+ const journalScan = await scanSessionJournalsForPlan(options.kimiHome, options.resumeSessionId, options.expectedExecutionRoot);
498
+ if (journalScan.kind === "environment") {
499
+ throw new RuntimeError("KIMI_SESSION_ENVIRONMENT_UNSAFE", `Saved session ${options.resumeSessionId} has an unsafe execution environment binding (${journalScan.recordType} in ${journalScan.journal}). Start a fresh local session; saved session state was not modified.`, stage, {
500
+ details: {
501
+ refusal_kind: "session-environment-unsafe",
502
+ record_type: journalScan.recordType,
503
+ retryable_after_setup: false,
504
+ },
505
+ });
506
+ }
399
507
  if (journalScan.kind === "tainted") {
400
508
  throw new RuntimeError("KIMI_SESSION_PLAN_TAINTED", `Saved session ${options.resumeSessionId} previously entered plan mode (record type "${journalScan.recordType}" in ${journalScan.journal}). Start a fresh session; saved session state was not modified.`, stage, {
401
509
  details: {
@@ -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 data = await restData(web, "POST", "/api/v1/sessions", { metadata: { cwd: options.cwd } }, options.signal, failureCode);
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,264 @@ 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 = await restData(web, "GET", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/status`, undefined, signal, failureCode);
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", "Kimi web /status returned an invalid payload.");
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 /status did not confirm swarm_mode=${active}.`, { expected_swarm_mode: active });
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", "Kimi web /status did not confirm plan mode is off.");
382
+ throw failure(failureCode, "swarm-activation.status", `Kimi web ${source} did not confirm plan mode is off.`);
358
383
  }
359
- if (data.permission !== "auto") {
360
- throw failure(failureCode, "swarm-activation.status", "Kimi web /status did not confirm auto permission mode.");
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", "Kimi web /status did not confirm the selected model alias.");
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 = (event) => {
588
+ if (event.wasClean &&
589
+ closeRequested &&
590
+ serverHello &&
591
+ clientHelloAcknowledged &&
592
+ subscriptionAcknowledged &&
593
+ latestState !== undefined) {
594
+ settle(latestState);
595
+ return;
596
+ }
597
+ rejectState("Kimi web agent-state channel closed before valid state was received.");
598
+ };
599
+ signal?.addEventListener("abort", onAbort, { once: true });
600
+ socket.addEventListener("open", onOpen);
601
+ socket.addEventListener("message", onMessage);
602
+ socket.addEventListener("error", onError);
603
+ socket.addEventListener("close", onClose);
604
+ if (signal?.aborted === true)
605
+ onAbort();
606
+ });
607
+ }
608
+ finally {
609
+ await refreshOwnedDescendants(web);
610
+ }
611
+ }
612
+ function isAgentStateFrame(frame, sessionId) {
613
+ return frame.session_id === sessionId &&
614
+ frame.agent_id === "main" &&
615
+ typeof frame.event_created_at === "string" &&
616
+ Number.isFinite(Date.parse(frame.event_created_at)) &&
617
+ (frame.status === "idle" || frame.status === "running") &&
618
+ (frame.model === undefined || typeof frame.model === "string") &&
619
+ (frame.thinking === undefined || typeof frame.thinking === "string") &&
620
+ (frame.permission_mode === undefined ||
621
+ frame.permission_mode === "manual" ||
622
+ frame.permission_mode === "yolo" ||
623
+ frame.permission_mode === "auto") &&
624
+ typeof frame.plan_mode === "boolean" &&
625
+ typeof frame.swarm_mode === "boolean" &&
626
+ typeof frame.tower_mode === "boolean";
627
+ }
366
628
  async function shutdownWeb(web, signal, failureCode) {
367
629
  await restData(web, "POST", "/api/v1/shutdown", undefined, signal, failureCode);
368
630
  const settlement = await waitForChildClose(web, signal, failureCode);
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.5.1";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.5.3";