forked from Keelrleek/Harbor
Keep Kimi going at a stop with exit code 2
Kimi's agent-core reads a top-level {"decision": "block"} as allow, so
Harbor's keep-going never took effect for Kimi; it honours exit code 2
with the reason on stderr (its hooks run through cmd.exe), once per turn.
Tool-call denials keep the JSON form, which Kimi does honour.
Also: attention skips stopped agents and closed items; file scopes of
function files carry their address.
This commit is contained in:
1 parent
dbfd1720c5
commit
dbc0c45ef2
2 files changed
+25
-4
No files matched your search
+9
-4
@@ -21,9 +21,13 @@ Output formats, from the hosts' own code (see integrations/README.md):
|
||||
claude, codex, deepseek context as {"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}}
|
||||
kimi, generic context as plain stdout
|
||||
claude, codex, deepseek, kimi
|
||||
decisions as Claude-compatible JSON at exit 0 - {"hookSpecificOutput": {"permissionDecision":
|
||||
"deny", ...}} before a tool, {"decision": "block", "reason": ...} at a stop - because a
|
||||
host that runs the command through PowerShell turns exit code 2 into 1
|
||||
decisions before a tool as Claude-compatible JSON at exit 0 - {"hookSpecificOutput":
|
||||
{"permissionDecision": "deny", ...}} - because a host that runs the command through PowerShell
|
||||
turns exit code 2 into 1
|
||||
claude, codex, deepseek a stop's keep-going as {"decision": "block", "reason": ...} at exit 0
|
||||
kimi a stop's keep-going as exit code 2 with the reason on stderr: Kimi's agent-core reads a
|
||||
top-level {"decision": "block"} as allow, and it runs hooks through cmd.exe, which passes
|
||||
exit code 2 through. Kimi honours one keep-going per turn
|
||||
opencode, generic decisions as exit code 2 with the reason on stderr (the OpenCode plugin spawns directly)
|
||||
|
||||
Hooks fail open: if Harbor itself is broken they print a warning to stderr and let the agent continue.
|
||||
@@ -39,6 +43,7 @@ from .core import Harbor
|
||||
BLOCK = 2
|
||||
JSON_CONTEXT_HOSTS = ("claude", "codex", "deepseek")
|
||||
JSON_DECISION_HOSTS = ("claude", "codex", "deepseek", "kimi")
|
||||
EXIT_CODE_STOP_HOSTS = ("kimi",) # a stop's keep-going: exit 2 + stderr (see above)
|
||||
EVENT_NAMES = {"session-start": "SessionStart", "prompt-submit": "UserPromptSubmit"}
|
||||
|
||||
|
||||
@@ -181,7 +186,7 @@ def _deny_tool(host, text):
|
||||
|
||||
|
||||
def _keep_going(host, text):
|
||||
if host in JSON_DECISION_HOSTS:
|
||||
if host in JSON_DECISION_HOSTS and host not in EXIT_CODE_STOP_HOSTS:
|
||||
print(json.dumps({"decision": "block", "reason": text}))
|
||||
return 0
|
||||
print(text, file=sys.stderr)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Regression tests for the correctness review of rounds five and six."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -358,6 +359,21 @@ class AttentionWithoutFalseAlarms(HarborCase):
|
||||
self.assertFalse(any(a["kind"] == "ack_overdue" and f"#{m['id']}" in a["text"] for a in self.h.attention()))
|
||||
|
||||
|
||||
class KimiStopKeepGoing(unittest.TestCase):
|
||||
def test_kimi_gets_exit_2_and_stderr_others_get_json(self):
|
||||
import contextlib, io
|
||||
from harbor import hooks
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||
code = hooks._keep_going("kimi", "keep going")
|
||||
self.assertEqual((code, out.getvalue(), err.getvalue().strip()), (2, "", "keep going"))
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
code = hooks._keep_going("codex", "keep going")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(json.loads(out.getvalue())["decision"], "block")
|
||||
|
||||
|
||||
class HooksVisible(HarborCase):
|
||||
def test_mcp_without_hooks_needs_attention(self):
|
||||
self.h.connect("Codex", "mcp", "codex")
|
||||
|
||||
Reference in new issue
Block a user