kimi-plugin-cc 2.2.1 → 2.3.1
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 +2 -2
- package/CHANGELOG.md +22 -0
- package/README.md +5 -5
- package/agents/kimi-challenge.md +3 -3
- package/agents/kimi-pursue.md +1 -1
- package/agents/kimi-rescue.md +2 -2
- package/agents/kimi-review.md +3 -3
- package/commands/README.md +2 -2
- package/commands/challenge.md +1 -1
- package/commands/pursue.md +1 -1
- package/commands/review.md +1 -1
- package/dist/background-spawn.js +9 -17
- package/dist/cli-client.js +22 -62
- package/dist/commands/ask.js +24 -29
- package/dist/commands/cancel.js +7 -19
- package/dist/commands/cli-helpers.js +9 -17
- package/dist/commands/pursue.js +21 -31
- package/dist/commands/registry.js +6 -19
- package/dist/commands/rescue.js +20 -20
- package/dist/commands/review-gate.js +16 -24
- package/dist/commands/review.js +37 -43
- package/dist/commands/setup.js +85 -171
- package/dist/commands/swarm.js +103 -88
- package/dist/commands/worker.js +0 -6
- package/dist/git.js +14 -7
- package/dist/hooks/approval-hook.js +9 -20
- package/dist/hooks/approval-policy.js +36 -69
- package/dist/hooks/config-safety.js +7 -4
- package/dist/hooks/install-paths.js +62 -138
- package/dist/hooks/install.js +37 -108
- package/dist/hooks/managed-block.js +88 -214
- package/dist/job-store.js +6 -28
- package/dist/kimi-command.js +1 -3
- package/dist/kimi-engine.js +2 -1
- package/dist/kimi-timeouts.js +9 -21
- package/dist/kimi-version-probe.js +16 -1210
- package/dist/native-v2-preflight.js +1 -1
- package/dist/parsing.js +4 -18
- package/dist/rescue-approval.js +78 -164
- package/dist/session-recovery.js +7 -10
- package/dist/stream-json.js +22 -174
- package/dist/version.js +1 -1
- package/docs/migration.md +2 -6
- package/docs/registry-releases.md +25 -5
- package/docs/safety.md +16 -12
- package/package.json +2 -2
- package/plugins/kimi-codex/.codex-plugin/plugin.json +4 -4
- package/plugins/kimi-codex/dist/background-spawn.js +9 -17
- package/plugins/kimi-codex/dist/cli-client.js +22 -62
- package/plugins/kimi-codex/dist/commands/ask.js +24 -29
- package/plugins/kimi-codex/dist/commands/cancel.js +7 -19
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +9 -17
- package/plugins/kimi-codex/dist/commands/pursue.js +21 -31
- package/plugins/kimi-codex/dist/commands/registry.js +6 -19
- package/plugins/kimi-codex/dist/commands/rescue.js +20 -20
- package/plugins/kimi-codex/dist/commands/review-gate.js +16 -24
- package/plugins/kimi-codex/dist/commands/review.js +37 -43
- package/plugins/kimi-codex/dist/commands/setup.js +85 -171
- package/plugins/kimi-codex/dist/commands/swarm.js +103 -88
- package/plugins/kimi-codex/dist/commands/worker.js +0 -6
- package/plugins/kimi-codex/dist/git.js +14 -7
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +9 -20
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +36 -69
- package/plugins/kimi-codex/dist/hooks/config-safety.js +7 -4
- package/plugins/kimi-codex/dist/hooks/install-paths.js +62 -138
- package/plugins/kimi-codex/dist/hooks/install.js +37 -108
- package/plugins/kimi-codex/dist/hooks/managed-block.js +88 -214
- package/plugins/kimi-codex/dist/job-store.js +6 -28
- package/plugins/kimi-codex/dist/kimi-command.js +1 -3
- package/plugins/kimi-codex/dist/kimi-engine.js +2 -1
- package/plugins/kimi-codex/dist/kimi-timeouts.js +9 -21
- package/plugins/kimi-codex/dist/kimi-version-probe.js +16 -1210
- package/plugins/kimi-codex/dist/native-v2-preflight.js +1 -1
- package/plugins/kimi-codex/dist/parsing.js +4 -18
- package/plugins/kimi-codex/dist/rescue-approval.js +78 -164
- package/plugins/kimi-codex/dist/session-recovery.js +7 -10
- package/plugins/kimi-codex/dist/stream-json.js +22 -174
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +2 -2
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -3
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +1 -1
|
@@ -1,58 +1,28 @@
|
|
|
1
1
|
// Shared parser for the kimi-plugin-cc managed block in
|
|
2
|
-
// ~/.kimi-code/config.toml.
|
|
2
|
+
// ~/.kimi-code/config.toml. Setup and the verifier use this one grammar;
|
|
3
|
+
// install-paths.ts separately owns the canonical command bytes and drift
|
|
4
|
+
// classification used here after a mismatch.
|
|
3
5
|
//
|
|
4
|
-
//
|
|
6
|
+
// Claude Code and Codex share the config but install to different paths. The
|
|
7
|
+
// marker's `:<host-id>` suffix makes each host own and verify only its block.
|
|
8
|
+
// An un-suffixed legacy block can be adopted by the current host on install.
|
|
5
9
|
//
|
|
6
|
-
//
|
|
7
|
-
// pre-call gate. PR 4 added `runtime/commands/setup.ts` as the
|
|
8
|
-
// installer. PR 4 reviewers found that the two were validating
|
|
9
|
-
// different shapes — the verifier was a substring check (could be
|
|
10
|
-
// bypassed by stray comments containing `kimi-plugin-cc-managed`),
|
|
11
|
-
// and `setup --check` only confirmed `blockText.includes(hookPath)`
|
|
12
|
-
// without checking for matcher/event/command exactness. A block with
|
|
13
|
-
// `matcher = "*"` (which throws inside kimi-code and silently
|
|
14
|
-
// disables the hook) would pass both gates.
|
|
15
|
-
//
|
|
16
|
-
// This module owns the single source of truth for what a valid
|
|
17
|
-
// managed block looks like. Both call sites import from here.
|
|
18
|
-
//
|
|
19
|
-
// (The drift classifier `describeHookCommandDrift` lives in install-paths.ts —
|
|
20
|
-
// the single source of truth for the hook command's byte shape — and is used
|
|
21
|
-
// here to turn a command MISMATCH into an actionable diagnosis when the caller
|
|
22
|
-
// supplies an `nodeExists` fs predicate.)
|
|
23
|
-
//
|
|
24
|
-
// HOST SCOPING (v1.7.0):
|
|
25
|
-
//
|
|
26
|
-
// Claude Code and Codex install this plugin to DIFFERENT, version-stamped
|
|
27
|
-
// paths but SHARE one `~/.kimi-code/config.toml`. Before v1.7.0 there was a
|
|
28
|
-
// single managed block whose `command` was exact-matched to the RUNNING
|
|
29
|
-
// host's path, so `/kimi:setup` (Claude) and `$kimi-setup` (Codex) overwrote
|
|
30
|
-
// each other. Now the block is HOST-SCOPED — the marker carries a `:<host-id>`
|
|
31
|
-
// suffix and each host owns/verifies its OWN block. Un-suffixed markers are
|
|
32
|
-
// treated as LEGACY (from pre-v1.7.0 single-host installs); the current host
|
|
33
|
-
// adopts a lone legacy block on its next install (see setup.ts).
|
|
34
|
-
//
|
|
35
|
-
// What a valid managed block looks like (line-by-line, post-trim):
|
|
10
|
+
// A valid block has this line-oriented shape:
|
|
36
11
|
//
|
|
37
12
|
// # === BEGIN kimi-plugin-cc-managed:<host-id> (vX.Y.Z) ===
|
|
38
|
-
// ... optional
|
|
13
|
+
// ... optional comments ...
|
|
39
14
|
// [[hooks]]
|
|
40
15
|
// event = "PreToolUse"
|
|
41
|
-
// command = "node '
|
|
16
|
+
// command = "'/abs/path/to/node' '/abs/path/to/approval-hook.js'"
|
|
42
17
|
// timeout = <integer>
|
|
43
18
|
// # === END kimi-plugin-cc-managed:<host-id> ===
|
|
44
19
|
//
|
|
45
20
|
// Critical rules:
|
|
46
21
|
//
|
|
47
|
-
// -
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// -
|
|
51
|
-
// enforce our safety contract.
|
|
52
|
-
// - Exactly ONE `[[hooks]]` declaration inside the block. Two would
|
|
53
|
-
// register two hooks, which is fine functionally but breaks
|
|
54
|
-
// idempotency checks.
|
|
55
|
-
// - The `command` line must contain the hook script path we expect.
|
|
22
|
+
// - No `matcher` in any TOML spelling: it can disable or narrow the hook.
|
|
23
|
+
// - `event = "PreToolUse"` is required.
|
|
24
|
+
// - Exactly one `[[hooks]]` table appears inside the markers.
|
|
25
|
+
// - The decoded command must equal the canonical expected command byte-for-byte.
|
|
56
26
|
import { parse as parseToml } from "../vendor/smol-toml/parse.js";
|
|
57
27
|
import { classifyHookCommandDrift, describeHookCommandDrift, hostIdFromHookCommand, isOurApprovalHookCommand, } from "./install-paths.js";
|
|
58
28
|
const MARKER_TAG = "kimi-plugin-cc-managed";
|
|
@@ -82,9 +52,8 @@ function findMarkers(lines) {
|
|
|
82
52
|
const trimmed = lines[i].trim();
|
|
83
53
|
const begin = BEGIN_LINE_RE.exec(trimmed);
|
|
84
54
|
if (begin !== null) {
|
|
85
|
-
// Normalize marker host ids
|
|
86
|
-
//
|
|
87
|
-
// installer emits (Kimi review — avoids confusing duplicate blocks).
|
|
55
|
+
// Normalize marker host ids so a hand-edited casing difference cannot
|
|
56
|
+
// create a distinct host and confusing duplicate blocks.
|
|
88
57
|
hits.push({ index: i, kind: "begin", hostId: begin[1]?.toLowerCase() ?? null });
|
|
89
58
|
continue;
|
|
90
59
|
}
|
|
@@ -96,9 +65,8 @@ function findMarkers(lines) {
|
|
|
96
65
|
return hits;
|
|
97
66
|
}
|
|
98
67
|
/**
|
|
99
|
-
* Pair BEGIN/END markers into well-formed blocks.
|
|
100
|
-
* (BEGIN-without-END or END-without-BEGIN)
|
|
101
|
-
* pre-v1.7.0 single-block behavior — an orphan is a hard error for install.
|
|
68
|
+
* Pair BEGIN/END markers into well-formed blocks. Return the first orphan
|
|
69
|
+
* (BEGIN-without-END or END-without-BEGIN); either is a hard error for install.
|
|
102
70
|
*/
|
|
103
71
|
function pairBlocks(lines) {
|
|
104
72
|
const markers = findMarkers(lines);
|
|
@@ -153,7 +121,7 @@ function validateBlockBody(rawBody) {
|
|
|
153
121
|
// `event`/`command` lines below could belong to a DIFFERENT table (e.g.
|
|
154
122
|
// `[[hooks]]` with only an event, then `[not_hooks]` carrying the
|
|
155
123
|
// canonical command). Binding the block to exactly one [[hooks]] table
|
|
156
|
-
// keeps the verifier from blessing a command-less hook.
|
|
124
|
+
// keeps the verifier from blessing a command-less hook.
|
|
157
125
|
invalidReason =
|
|
158
126
|
"block contains an unexpected TOML table — only a single [[hooks]] table is allowed. Reinstall with /kimi:setup to repair.";
|
|
159
127
|
break;
|
|
@@ -170,21 +138,15 @@ function validateBlockBody(rawBody) {
|
|
|
170
138
|
const basic = COMMAND_LINE_RE.exec(line);
|
|
171
139
|
const literal = COMMAND_LITERAL_LINE_RE.exec(line);
|
|
172
140
|
if (basic !== null || literal !== null) {
|
|
173
|
-
// TOML basic
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// we must decode the escapes here before equality. Without this,
|
|
177
|
-
// a hook script path containing `'` round-trips through
|
|
178
|
-
// `shellSingleQuote` -> backslash -> TOML basic-string escape ->
|
|
179
|
-
// captured raw -> mismatch on verify. Audit re-review (reports
|
|
180
|
-
// 33/34) flagged this as a UX false-fail. Literal strings ('...')
|
|
181
|
-
// have no escapes — pass through as-is.
|
|
141
|
+
// Decode TOML basic-string escapes before comparing with the canonical JS
|
|
142
|
+
// string; literal strings have no escapes. Otherwise shell quoting and
|
|
143
|
+
// TOML escaping can make an equivalent command compare unequal.
|
|
182
144
|
commandPath = basic !== null ? decodeTomlBasicString(basic[1]) : literal[1];
|
|
183
145
|
continue;
|
|
184
146
|
}
|
|
185
|
-
// Other TOML lines
|
|
186
|
-
//
|
|
187
|
-
// the probe
|
|
147
|
+
// Other TOML lines are ignored only by this local grammar. Whole-file schema
|
|
148
|
+
// validation and foundHookEntryIsClean reject foreign keys before an
|
|
149
|
+
// installed verdict; the shell probe is not the validation backstop.
|
|
188
150
|
}
|
|
189
151
|
if (invalidReason === undefined) {
|
|
190
152
|
if (!foundHooksTable) {
|
|
@@ -197,17 +159,11 @@ function validateBlockBody(rawBody) {
|
|
|
197
159
|
invalidReason = "block is missing a `command = \"...\"` line";
|
|
198
160
|
}
|
|
199
161
|
}
|
|
200
|
-
// Parser-based matcher rejection
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// approve). The absent-state fallback (`hasCleanEnforcingHookEntry`) already
|
|
206
|
-
// parses and is immune; the primary marked-block path must be too, or a
|
|
207
|
-
// quoted matcher slips a disabled hook past the verifier as `installed:true`.
|
|
208
|
-
// Fail closed on any parse error — a body that won't parse in isolation is
|
|
209
|
-
// anomalous and kimi-code would reject the whole config anyway. (kimi
|
|
210
|
-
// whole-repo audit 2026-07-17.)
|
|
162
|
+
// Parser-based matcher rejection backs up the bare-line regex: smol-toml
|
|
163
|
+
// normalizes quoted, literal, and dotted matcher keys that the regex misses,
|
|
164
|
+
// while kimi-code honors them and can disable or narrow the hook. Both this
|
|
165
|
+
// marked-block path and the absent-state fallback must parse the real TOML.
|
|
166
|
+
// Fail closed when the body cannot be parsed in isolation.
|
|
211
167
|
if (invalidReason === undefined) {
|
|
212
168
|
try {
|
|
213
169
|
const parsed = parseToml(rawBody.join("\n"));
|
|
@@ -240,9 +196,8 @@ function validateBlockBody(rawBody) {
|
|
|
240
196
|
* Other hosts' suffixed blocks are ignored for this host's state (that is the
|
|
241
197
|
* whole point of host scoping — they coexist).
|
|
242
198
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* comparison.
|
|
199
|
+
* Only a trailing `\r` is removed from each line for CRLF safety; other line
|
|
200
|
+
* whitespace is preserved here and trimmed only by the relevant matchers.
|
|
246
201
|
*/
|
|
247
202
|
export function parseManagedBlock(contents, hostId) {
|
|
248
203
|
const rawLines = contents.split("\n");
|
|
@@ -252,13 +207,10 @@ export function parseManagedBlock(contents, hostId) {
|
|
|
252
207
|
return { state: { kind: "orphan", detail: paired.orphan }, blocks: [], lines };
|
|
253
208
|
}
|
|
254
209
|
const blocks = paired.blocks;
|
|
255
|
-
// A block
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
// block during migration (Kimi review): a `~/.codex/…` legacy block is NOT
|
|
260
|
-
// relevant to the `claude-code` host, so Claude appends its own block and
|
|
261
|
-
// leaves Codex's intact.
|
|
210
|
+
// A block belongs to this host when its marker suffix matches or, for a
|
|
211
|
+
// legacy un-suffixed block, its command derives to this host. An unattributable
|
|
212
|
+
// legacy command is claimable by the next installer; a command attributed to
|
|
213
|
+
// another host is ignored so its block remains untouched.
|
|
262
214
|
const relevant = blocks.filter((b) => effectiveHost(b, hostId) === hostId);
|
|
263
215
|
if (relevant.length === 0) {
|
|
264
216
|
return { state: { kind: "absent" }, blocks, lines };
|
|
@@ -311,33 +263,6 @@ export function decodeManagedCommandLine(trimmedLine) {
|
|
|
311
263
|
return literal[1];
|
|
312
264
|
return null;
|
|
313
265
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Robust, PARSER-based "is our clean enforcing hook present?" check for the
|
|
316
|
-
* marker-less fallback. Parses the whole config with the same vendored
|
|
317
|
-
* `smol-toml` kimi-code uses, then looks for a `hooks[]` entry that is EXACTLY
|
|
318
|
-
* our clean hook:
|
|
319
|
-
*
|
|
320
|
-
* - `event === "PreToolUse"`,
|
|
321
|
-
* - `command === expectedCommand` (byte-exact — the security boundary),
|
|
322
|
-
* - NO `matcher` key in ANY TOML spelling (bare `matcher`, quoted
|
|
323
|
-
* `"matcher"`, after a blank line, past a multiline array) — a matcher can
|
|
324
|
-
* disable the hook (`new RegExp("*")` throws → hook never fires) or narrow
|
|
325
|
-
* it to a subset of tools, so its presence means "not a clean install",
|
|
326
|
-
* - no keys beyond `{event, command, timeout}` — an unknown key trips
|
|
327
|
-
* kimi-code's `.strict()` hook schema and disables the whole hooks array.
|
|
328
|
-
*
|
|
329
|
-
* Using the real parser (not the line-oriented `findBareApprovalHookTables`,
|
|
330
|
-
* which stays the conservative PRUNE scanner) closes the entire class of
|
|
331
|
-
* line-scanner blind spots a three-model review panel demonstrated (Opus +
|
|
332
|
-
* Codex + kimi all independently: parse the table, don't lex it). Returns
|
|
333
|
-
* `false` on any parse error — fail-closed. The real callers
|
|
334
|
-
* (`verifyHookInstalled`, `runCheck`) run whole-file schema validation first,
|
|
335
|
-
* so a malformed or foreign-invalid file never reaches an installed verdict.
|
|
336
|
-
*
|
|
337
|
-
* NOTE: this deliberately does NOT range-check `timeout` — that is the config
|
|
338
|
-
* schema's job (run upstream by both callers). A clean entry with an
|
|
339
|
-
* out-of-range timeout is refused there, not here.
|
|
340
|
-
*/
|
|
341
266
|
/**
|
|
342
267
|
* Return `contents` with every well-formed managed BEGIN/END block removed
|
|
343
268
|
* (inclusive), so what remains is only the bare/root TOML. Used by
|
|
@@ -387,14 +312,15 @@ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
|
|
|
387
312
|
}
|
|
388
313
|
return false;
|
|
389
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Parser-based fallback used only in the absent state. Strip every managed
|
|
317
|
+
* block first so another host's live hook cannot count for this host, then parse
|
|
318
|
+
* the remaining bare/root TOML with the real parser rather than the prune line
|
|
319
|
+
* scanner. `hooksArrayHasCleanEntry` requires the exact event and command, no
|
|
320
|
+
* matcher in any spelling, and no foreign keys. Parse errors fail closed.
|
|
321
|
+
* Callers run whole-file schema validation first, including timeout ranges.
|
|
322
|
+
*/
|
|
390
323
|
export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
|
|
391
|
-
// Consider only hooks that live OUTSIDE every managed BEGIN/END block. In the
|
|
392
|
-
// `absent` state this host has no block of its own, so any surviving marked
|
|
393
|
-
// block belongs to ANOTHER host — its entries are that host's responsibility,
|
|
394
|
-
// not evidence that THIS host is installed (host isolation). The fallback's
|
|
395
|
-
// whole purpose is to recognize a MARKER-STRIPPED (bare) copy of our own hook,
|
|
396
|
-
// so we strip all marked-block lines, then parse the remainder with the real
|
|
397
|
-
// TOML parser.
|
|
398
324
|
let parsed;
|
|
399
325
|
try {
|
|
400
326
|
parsed = parseToml(stripManagedBlockLines(contents));
|
|
@@ -414,7 +340,7 @@ export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
|
|
|
414
340
|
* table), so kimi-code loads it and DISABLES the hook (`new RegExp("*")` throws)
|
|
415
341
|
* while the marked body looks clean → a fail-open `installed:true`. Parsing the
|
|
416
342
|
* whole file exactly as kimi-code does attributes that trailing key to our entry
|
|
417
|
-
* and rejects it. Fail-closed on any parse error.
|
|
343
|
+
* and rejects it. Fail-closed on any parse error.
|
|
418
344
|
*/
|
|
419
345
|
function foundHookEntryIsClean(contents, expectedCommand) {
|
|
420
346
|
let parsed;
|
|
@@ -431,20 +357,11 @@ function foundHookEntryIsClean(contents, expectedCommand) {
|
|
|
431
357
|
export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
432
358
|
const { state } = parseManagedBlock(contents, opts.hostId);
|
|
433
359
|
if (state.kind === "absent") {
|
|
434
|
-
// kimi-code
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
// security boundary — the byte-exact canonical command is — so a
|
|
440
|
-
// marker-less table that is EXACTLY our clean enforcing hook still counts
|
|
441
|
-
// as installed. This decision is made against the REAL TOML parser (not a
|
|
442
|
-
// line scanner): a line scanner has TOML-semantic blind spots that a
|
|
443
|
-
// reviewer panel demonstrated let a hook-DISABLING `matcher` hide from it —
|
|
444
|
-
// after a blank line, behind a quoted `"matcher"` key, or past a multiline
|
|
445
|
-
// array value — while kimi-code still honors it (`new RegExp("*")` throws →
|
|
446
|
-
// hook disabled → auto-approve). The parser sees the whole table regardless
|
|
447
|
-
// of layout, so `hasCleanEnforcingHookEntry` is immune to all three.
|
|
360
|
+
// kimi-code config rewrites drop comments but leave hook tables intact. In
|
|
361
|
+
// the absent state only, accept a marker-less table that is exactly this
|
|
362
|
+
// host's clean enforcing hook: markers are not the security boundary, the
|
|
363
|
+
// byte-exact command is. The real TOML parser sees matchers and foreign keys
|
|
364
|
+
// across blank lines, quoted spellings, and multiline values.
|
|
448
365
|
if (hasCleanEnforcingHookEntry(contents, expectedCommand)) {
|
|
449
366
|
return {
|
|
450
367
|
installed: true,
|
|
@@ -453,12 +370,8 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
453
370
|
state,
|
|
454
371
|
};
|
|
455
372
|
}
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// write, and a plugin update then moves the version-stamped hook path. The
|
|
459
|
-
// 2026-07-25 Codex incident was exactly this pair. Classify the drift from
|
|
460
|
-
// this host's own stale bare table so callers still get an actionable axis
|
|
461
|
-
// instead of a bare "not present".
|
|
373
|
+
// If no exact bare table remains, classify drift only from this host's own
|
|
374
|
+
// stale table so the caller gets an actionable axis.
|
|
462
375
|
const staleOwn = findBareApprovalHookTables(contents).find((table) => table.command !== expectedCommand &&
|
|
463
376
|
isOurApprovalHookCommand(table.command) &&
|
|
464
377
|
hostIdFromHookCommand(table.command) === opts.hostId);
|
|
@@ -506,11 +419,6 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
506
419
|
drift: classifyHookCommandDrift(state.commandPath, expectedCommand),
|
|
507
420
|
};
|
|
508
421
|
}
|
|
509
|
-
// The marked body (lines between BEGIN/END) passed the line grammar and the
|
|
510
|
-
// command matches — but confirm with the REAL whole-file parse that no matcher
|
|
511
|
-
// or stray key rides our `[[hooks]]` table from OUTSIDE the markers (e.g. after
|
|
512
|
-
// the END comment, which does not terminate the TOML table). Such a key would
|
|
513
|
-
// load in kimi-code and disable/reject the hook while the body looks clean.
|
|
514
422
|
if (!foundHookEntryIsClean(contents, expectedCommand)) {
|
|
515
423
|
return {
|
|
516
424
|
installed: false,
|
|
@@ -521,23 +429,17 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
521
429
|
return { installed: true, via: "managed-block", state };
|
|
522
430
|
}
|
|
523
431
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* NOTE: the INSTALLED decision does NOT use this — it uses the real TOML parser
|
|
528
|
-
* (`hasCleanEnforcingHookEntry`), because a line scanner has TOML-semantic
|
|
529
|
-
* blind spots (a `matcher` hidden after a blank line / behind a quoted key /
|
|
530
|
-
* past a multiline array). This scanner is only for computing physical line
|
|
531
|
-
* RANGES to delete, where a conservative, layout-aware view is what we want.
|
|
432
|
+
* Line-oriented PRUNE scanner, never the installed decision; that decision uses
|
|
433
|
+
* the real TOML parser through `hasCleanEnforcingHookEntry` and
|
|
434
|
+
* `foundHookEntryIsClean`.
|
|
532
435
|
*
|
|
533
|
-
* `[start, end)`
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* never half-cut). `command` is the decoded shell command.
|
|
436
|
+
* The deletion range `[start, end)` is conservative: only the contiguous
|
|
437
|
+
* non-blank body after `[[hooks]]`, stopping at a blank line, table header, or
|
|
438
|
+
* managed marker so adjacent user content is preserved. A separate full-span
|
|
439
|
+
* scan rejects matcher lines recognized by MATCHER_LINE_RE to avoid half-cutting
|
|
440
|
+
* a table. That scan is deliberately heuristic and does not claim to recognize
|
|
441
|
+
* every quoted or dotted TOML matcher spelling. `command` is decoded for the
|
|
442
|
+
* caller's ownership checks.
|
|
541
443
|
*/
|
|
542
444
|
export function findBareApprovalHookTables(contents) {
|
|
543
445
|
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
@@ -560,12 +462,9 @@ export function findBareApprovalHookTables(contents) {
|
|
|
560
462
|
let commandDecoded = "";
|
|
561
463
|
let hasPreToolUseEvent = false;
|
|
562
464
|
let hasMatcher = false;
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
// dangling TOML after a partial prune — treat the table as NOT ours and
|
|
567
|
-
// skip it. Conservative by design (Codex review): never partially cut a
|
|
568
|
-
// structure we don't fully recognize.
|
|
465
|
+
// Our tables contain only event/command/timeout and comments. Any other key,
|
|
466
|
+
// including a multiline array, makes the shape unrecognized so pruning never
|
|
467
|
+
// partially cuts a structure and leaves dangling TOML.
|
|
569
468
|
let simpleGrammar = true;
|
|
570
469
|
while (j < lines.length) {
|
|
571
470
|
const trimmed = lines[j].trim();
|
|
@@ -574,7 +473,7 @@ export function findBareApprovalHookTables(contents) {
|
|
|
574
473
|
// Any TOML table header bounds the table — including one with a trailing
|
|
575
474
|
// comment (`[[permission.rules]] # note`). A bare leading `[` is a table
|
|
576
475
|
// header in TOML (value arrays are `key = [...]`), so stop there and
|
|
577
|
-
// never swallow the next table.
|
|
476
|
+
// never swallow the next table.
|
|
578
477
|
if (trimmed.startsWith("["))
|
|
579
478
|
break;
|
|
580
479
|
if (BEGIN_LINE_RE.test(trimmed) || END_LINE_RE.test(trimmed))
|
|
@@ -601,22 +500,13 @@ export function findBareApprovalHookTables(contents) {
|
|
|
601
500
|
}
|
|
602
501
|
j += 1;
|
|
603
502
|
}
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
// If we only checked the contiguous body we'd report `installed: true` for
|
|
612
|
-
// a hook that is actually off — the exact auto-approve bypass this plugin
|
|
613
|
-
// exists to prevent. So scan the REST of the table's TOML span for a
|
|
614
|
-
// matcher and reject the table if one appears anywhere. (The prune line
|
|
615
|
-
// range stays `[start, j)` — conservative, never eating past the blank —
|
|
616
|
-
// because a rejected table is never pruned anyway.) Foreign keys after a
|
|
617
|
-
// blank are NOT re-checked here: an unknown key fails kimi-code's `.strict()`
|
|
618
|
-
// hook schema, so `validateKimiHookSetForEnvironment` rejects the whole file
|
|
619
|
-
// upstream; only `matcher` is schema-valid-but-hook-disabling.
|
|
503
|
+
// A blank line does NOT end a TOML table; membership continues to the next
|
|
504
|
+
// header or EOF. The deletion range nevertheless stops at the blank to
|
|
505
|
+
// preserve adjacent user content. Scan the remaining span for matcher lines
|
|
506
|
+
// this scanner recognizes and reject the table rather than half-cutting it:
|
|
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.
|
|
620
510
|
let matcherBeyondBody = false;
|
|
621
511
|
for (let k = j; k < lines.length; k += 1) {
|
|
622
512
|
const t = lines[k].trim();
|
|
@@ -629,12 +519,10 @@ export function findBareApprovalHookTables(contents) {
|
|
|
629
519
|
break;
|
|
630
520
|
}
|
|
631
521
|
}
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
// Command-identity filtering (ours? whose host?) is the caller's concern —
|
|
637
|
-
// see `findUnmanagedApprovalHookBlocks` and `evaluateInstalled`.
|
|
522
|
+
// Report only tables matching this conservative line grammar: PreToolUse,
|
|
523
|
+
// a decoded command, no recognized matcher in the TOML span, and no foreign
|
|
524
|
+
// keys in the contiguous body. This is not a full TOML-semantic acceptance
|
|
525
|
+
// check; command and host ownership are the caller's concern.
|
|
638
526
|
if (simpleGrammar &&
|
|
639
527
|
commandDecoded.length > 0 &&
|
|
640
528
|
hasPreToolUseEvent &&
|
|
@@ -646,32 +534,19 @@ export function findBareApprovalHookTables(contents) {
|
|
|
646
534
|
return ranges;
|
|
647
535
|
}
|
|
648
536
|
/**
|
|
649
|
-
* Find
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
* HOST SCOPING (v1.8.2): pass `ownedBy` (a host id from `resolveHostId`) to
|
|
657
|
-
* restrict the result to tables whose command path derives to THAT host via
|
|
658
|
-
* `hostIdFromHookCommand`. Pruning MUST pass it — after a kimi-code comment
|
|
659
|
-
* strip, another host's marker-less table is that host's LIVE hook, and a
|
|
660
|
-
* host-blind prune silently disarms it (the pre-v1.8.2 seesaw: each host's
|
|
661
|
-
* setup deleted the other's enforcement). Omitting `ownedBy` is the explicit
|
|
662
|
-
* every-host sweep reserved for `/kimi:setup --uninstall --all`.
|
|
537
|
+
* Find marker-less canonical approval-hook tables outside managed blocks.
|
|
538
|
+
* `ownedBy` restricts results to the host derived from each command and is
|
|
539
|
+
* required for scoped pruning: another host's marker-less table may be its live
|
|
540
|
+
* hook after a config rewrite. Omit `ownedBy` only for the explicit every-host
|
|
541
|
+
* `/kimi:setup --uninstall --all` sweep. Non-canonical and hand-rolled hooks are
|
|
542
|
+
* not claimed.
|
|
663
543
|
*/
|
|
664
544
|
export function findUnmanagedApprovalHookBlocks(contents, ownedBy, alsoMatchCommand) {
|
|
665
545
|
return findBareApprovalHookTables(contents).filter((table) => {
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// `KIMI_PLUGIN_CC_HOST_ID` override disagrees with the path-derived host
|
|
671
|
-
// (`hostIdFromHookCommand`) — otherwise setup would append a duplicate
|
|
672
|
-
// block beside the identical bare table (Kimi review F4). Never widens the
|
|
673
|
-
// prune: another host's table and a hand-rolled hook carry different
|
|
674
|
-
// commands, so they never equal this host's canonical command.
|
|
546
|
+
// Exact current command bytes are the strongest ownership signal and win
|
|
547
|
+
// even when a host-id override disagrees with path derivation. This does
|
|
548
|
+
// not widen the prune: another host's or a hand-rolled hook has different
|
|
549
|
+
// command bytes.
|
|
675
550
|
if (alsoMatchCommand !== undefined && table.command === alsoMatchCommand)
|
|
676
551
|
return true;
|
|
677
552
|
if (!isOurApprovalHookCommand(table.command))
|
|
@@ -694,9 +569,8 @@ export const MARKERS = {
|
|
|
694
569
|
* we compare against never contains them, so a mismatch surfaces as
|
|
695
570
|
* "installed: false" rather than a parse error.
|
|
696
571
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* the missing decode as a verifier false-fail for apostrophe paths.
|
|
572
|
+
* Inverse of `tomlBasicString` in `runtime/commands/setup.ts`; decoding before
|
|
573
|
+
* comparison prevents shell/TOML escaping from causing a false mismatch.
|
|
700
574
|
*/
|
|
701
575
|
function decodeTomlBasicString(raw) {
|
|
702
576
|
let out = "";
|
package/dist/job-store.js
CHANGED
|
@@ -542,35 +542,13 @@ function translateSqliteError(error) {
|
|
|
542
542
|
return new RuntimeError("JOB_STORE_ERROR", `Job store operation failed: ${formatError(error)}`, "job-store", error instanceof Error ? { cause: error } : undefined);
|
|
543
543
|
}
|
|
544
544
|
/**
|
|
545
|
-
* Run a function with a temporary `JobStore
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* Construction-failure note: if `new JobStore(paths)` itself throws,
|
|
550
|
-
* this helper cannot close anything — the constructor's own try/catch
|
|
551
|
-
* owns adapter-handle cleanup on mid-migration failures. The helper
|
|
552
|
-
* handles "store opened cleanly, function threw"; the constructor
|
|
553
|
-
* handles "open or migrate failed mid-construction." Together they
|
|
554
|
-
* make the leak class unreachable.
|
|
555
|
-
*
|
|
556
|
-
* Consolidates the repeated
|
|
557
|
-
*
|
|
558
|
-
* const store = new JobStore(paths);
|
|
559
|
-
* try { ... } finally { store.close(); }
|
|
560
|
-
*
|
|
561
|
-
* dance that every command file used to copy. For the rare case where
|
|
562
|
-
* the caller needs to keep a reference to the store past the function
|
|
563
|
-
* body (e.g., async cancel handlers that close elsewhere), continue
|
|
564
|
-
* using the explicit `new JobStore` + try/finally idiom — `withJobStore`
|
|
565
|
-
* is for the common single-scope case.
|
|
545
|
+
* Run a function with a temporary `JobStore`, closing its handle on return or
|
|
546
|
+
* throw after construction succeeds. The constructor owns cleanup when opening
|
|
547
|
+
* or migrating the store fails.
|
|
566
548
|
*/
|
|
567
549
|
export async function withJobStore(paths, fn) {
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
// (pragma failure, schema migration), and the helper's `finally`
|
|
571
|
-
// is the second line of defense behind the constructor's own
|
|
572
|
-
// cleanup. Moving `new JobStore` above the try would silently
|
|
573
|
-
// reintroduce a handle leak the JSDoc above promises against.
|
|
550
|
+
// Keep construction inside try: the constructor cleans up its own failures,
|
|
551
|
+
// while this finally closes every successfully constructed store.
|
|
574
552
|
let store;
|
|
575
553
|
try {
|
|
576
554
|
store = new JobStore(paths);
|
|
@@ -591,7 +569,6 @@ export function withJobStoreSync(paths, fn) {
|
|
|
591
569
|
store?.close();
|
|
592
570
|
}
|
|
593
571
|
}
|
|
594
|
-
/** Old non-null budgets have unknown provenance; never invent explicit intent. */
|
|
595
572
|
/** Parsed `hook_escapes` column for JSON envelopes: [] when NULL or unreadable. */
|
|
596
573
|
export function parseHookEscapes(raw) {
|
|
597
574
|
if (!raw)
|
|
@@ -604,6 +581,7 @@ export function parseHookEscapes(raw) {
|
|
|
604
581
|
return [];
|
|
605
582
|
}
|
|
606
583
|
}
|
|
584
|
+
/** Old non-null budgets have unknown provenance; never invent explicit intent. */
|
|
607
585
|
export function renderBudgetSource(job) {
|
|
608
586
|
return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
|
|
609
587
|
}
|
package/dist/kimi-command.js
CHANGED
|
@@ -10,9 +10,7 @@ export function resolveKimiCliCommand(env) {
|
|
|
10
10
|
parsed = JSON.parse(raw);
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
|
-
// Plain-text
|
|
14
|
-
// an existing `KIMI_PLUGIN_CC_KIMI_PREFIX_ARGS="--import tsx ..."`
|
|
15
|
-
// export don't have to migrate to JSON for v1.0.
|
|
13
|
+
// Plain-text values are space-split for launcher-style overrides.
|
|
16
14
|
const plainArgs = raw.split(" ").filter(Boolean);
|
|
17
15
|
assertPrefixArgsSafe(plainArgs, raw);
|
|
18
16
|
return { command, prefixArgs: plainArgs };
|
package/dist/kimi-engine.js
CHANGED
|
@@ -39,6 +39,8 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
|
|
|
39
39
|
"2.1.0",
|
|
40
40
|
"2.1.1",
|
|
41
41
|
"2.2.0",
|
|
42
|
+
"2.3.0",
|
|
43
|
+
"2.3.1",
|
|
42
44
|
]);
|
|
43
45
|
/** Per-operation native-v2 certification. One green operation never certifies another. */
|
|
44
46
|
export const NATIVE_V2_CERTIFIED = new Map([
|
|
@@ -577,7 +579,6 @@ async function resolveExactExecutable(command, cwd, env) {
|
|
|
577
579
|
return await realpath(candidate);
|
|
578
580
|
}
|
|
579
581
|
catch {
|
|
580
|
-
// Keep searching; failure is reported once with the original command.
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
throw new RuntimeError("KIMI_EXECUTION_PLAN_UNRESOLVED", `Refusing to spawn kimi because executable ${JSON.stringify(command)} could not be resolved to an exact runnable path. Run Claude Code \`/kimi:setup\` or Codex \`$kimi-setup\` to verify the active binary, or correct KIMI_PLUGIN_CC_KIMI_BIN and retry.`, "kimi-engine.plan", { details: { command, cwd } });
|
package/dist/kimi-timeouts.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
// Per-command response
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// subprocess transport collapses spawn + prompt into a single
|
|
7
|
-
// `runCliPromptWithBudget` call (see `runtime/cli-client.ts`), and
|
|
8
|
-
// startup failures surface as CLI_SPAWN_FAILED / CLI_PROCESS_ERROR —
|
|
9
|
-
// not as separate timeouts. So only the per-command response budgets
|
|
10
|
-
// remain.
|
|
11
|
-
// Budgets accommodate thinking-capable workflows; per-run effort is only a request.
|
|
12
|
-
// kimi-code spends real wallclock on extended reasoning before emitting
|
|
13
|
-
// the first assistant record; pre-alpha.4 budgets (300s/600s) were sized
|
|
14
|
-
// against the non-thinking path and timed out in production smoke
|
|
15
|
-
// testing under default flags. Defaults are now 1800/1800/2700 seconds
|
|
16
|
-
// for ask/review/rescue to allow repo-wide work; review-gate stays at
|
|
17
|
-
// 8s because it fires inside Claude Code's Stop hook and is forced to
|
|
18
|
-
// thinking-off in its own caller.
|
|
1
|
+
// Per-command response budgets leave room for thinking-capable workflows;
|
|
2
|
+
// per-run effort is only a request. Ask/review default to 30 minutes and rescue
|
|
3
|
+
// to 45 minutes for repo-wide work. Review-gate stays at 8 seconds because it
|
|
4
|
+
// runs inside Claude Code's Stop hook and requests thinking off by default;
|
|
5
|
+
// KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can override that request.
|
|
19
6
|
/** Ask budget. Conversational; thinking-on free-form reasoning. */
|
|
20
7
|
export const KIMI_ASK_PROMPT_TIMEOUT_MS = 1_800_000; // 30 minutes
|
|
21
8
|
/** Review/challenge budget. Thinking-on single-turn analysis over the workspace. */
|
|
@@ -25,8 +12,8 @@ export const KIMI_RESCUE_PROMPT_TIMEOUT_MS = 2_700_000; // 45 minutes
|
|
|
25
12
|
/**
|
|
26
13
|
* Pursue (autonomous goal mode) DEFAULT wall-clock ceiling when the user
|
|
27
14
|
* passes no `--budget`. This is the ONLY hard bound on an autonomous goal —
|
|
28
|
-
* headless goal create sets no token/turn budget
|
|
29
|
-
* SetGoalBudget
|
|
15
|
+
* headless goal create sets no token/turn budget, and the managed hook denies
|
|
16
|
+
* SetGoalBudget. The same duration as
|
|
30
17
|
* the single-turn rescue default, but a safety ceiling rather than a patience
|
|
31
18
|
* knob. Deliberately finite. Overridable per-job via `--budget` (parsed to ms in
|
|
32
19
|
* parsePursueArgs). See runtime/commands/pursue.ts and docs/safety.md.
|
|
@@ -53,7 +40,8 @@ export const KIMI_SWARM_DEFAULT_BUDGET_MS = 1_800_000; // 30 minutes
|
|
|
53
40
|
* KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can inherit or request another effort.
|
|
54
41
|
* BYO/non-trait-driven providers and always-thinking models can ignore it.
|
|
55
42
|
* The 8s timeout remains fail-open; a request never guarantees completion.
|
|
56
|
-
* Hook-
|
|
43
|
+
* Hook-escape findings annotate the result and never block or change job outcome.
|
|
44
|
+
* See docs/safety.md.
|
|
57
45
|
*/
|
|
58
46
|
export const KIMI_REVIEW_GATE_TIMEOUT_MS = 8_000;
|
|
59
47
|
export function isTimeoutCode(code) {
|