Files
Kigi-CLI/crates/codegen/kigi-shell/src/agent/ext_parsers.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

260 lines
10 KiB
Rust

//! Wire-shape parsers for ext-notification params handled by `MvpAgent`.
//!
//! Pure parsing only (params JSON → `SessionCommand`); session lookup and
//! command dispatch stay in `mvp_agent::ext_notification`.
use crate::session::SessionCommand;
/// Parse a `kigi/queue/{remove,reorder,clear,edit,interject}` ext-notification's
/// params into the corresponding [`SessionCommand`].
/// `owner` is the resolved attribution (params `owner`/`clientIdentifier`) used
/// to scope remove/clear to the requesting client's own items, and recorded as
/// `last_editor` for in-place text edits. Returns `None` for unrecognized
/// methods or for `edit` when `newText` is missing.
pub(super) fn parse_queue_edit_command(
method: &str,
params: &serde_json::Value,
owner: Option<String>,
) -> Option<SessionCommand> {
match method {
"kigi/queue/remove" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
// The client supplies the version it last saw; the handler removes
// only on an exact match (stale = benign no-op + rebroadcast).
// Default 0 covers never-edited prompts (the common case).
let expected_version = params
.get("expectedVersion")
.and_then(|v| v.as_u64())
.unwrap_or(0);
Some(SessionCommand::RemoveQueuedPrompt {
id,
expected_version,
owner,
})
}
"kigi/queue/reorder" => {
let ordered_ids = params
.get("orderedIds")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
Some(SessionCommand::ReorderQueue { ordered_ids })
}
"kigi/queue/clear" => Some(SessionCommand::ClearQueue { owner }),
"kigi/queue/interject" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
// The client supplies the version it last saw; the handler acts
// only on an exact match (stale = benign no-op + rebroadcast).
let expected_version = params
.get("expectedVersion")
.and_then(|v| v.as_u64())
.unwrap_or(0);
// Optional client-edited replacement text (atomic edit+interject).
// Blank overrides are dropped (degrade to the stored queue text) —
// never interject an empty prompt on a malformed client param.
let new_text = params
.get("newText")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.map(str::to_string);
Some(SessionCommand::InterjectQueuedPrompt {
id,
expected_version,
owner,
new_text,
})
}
"kigi/queue/edit" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
let new_text = params.get("newText").and_then(|v| v.as_str())?.to_string();
// `owner` is the resolved attribution; for edit it represents the
// most recent editor (recorded as `last_editor`), not the original
// enqueuer.
Some(SessionCommand::EditQueuedPrompt {
id,
new_text,
editor: owner,
})
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Each `kigi/queue/*` ext-notification maps to the
/// correct versioned/idempotent `SessionCommand`.
#[test]
fn parse_queue_edit_command_maps_each_method() {
// remove: id + expectedVersion + owner.
let p = serde_json::json!({
"sessionId": "s1", "id": "p7", "expectedVersion": 3
});
match parse_queue_edit_command("kigi/queue/remove", &p, Some("kigi-tui".into())) {
Some(SessionCommand::RemoveQueuedPrompt {
id,
expected_version,
owner,
}) => {
assert_eq!(id, "p7");
assert_eq!(expected_version, 3);
assert_eq!(owner.as_deref(), Some("kigi-tui"));
}
_ => panic!("expected RemoveQueuedPrompt"),
}
// remove without expectedVersion defaults to 0.
let p = serde_json::json!({ "sessionId": "s1", "id": "p8" });
match parse_queue_edit_command("kigi/queue/remove", &p, None) {
Some(SessionCommand::RemoveQueuedPrompt {
expected_version, ..
}) => assert_eq!(expected_version, 0),
_ => panic!("expected RemoveQueuedPrompt"),
}
// reorder: orderedIds array.
let p = serde_json::json!({ "sessionId": "s1", "orderedIds": ["a", "b", "c"] });
match parse_queue_edit_command("kigi/queue/reorder", &p, None) {
Some(SessionCommand::ReorderQueue { ordered_ids }) => {
assert_eq!(ordered_ids, vec!["a", "b", "c"]);
}
_ => panic!("expected ReorderQueue"),
}
// clear: owner-scoped.
match parse_queue_edit_command(
"kigi/queue/clear",
&serde_json::json!({ "sessionId": "s1" }),
Some("kigi-tui".into()),
) {
Some(SessionCommand::ClearQueue { owner }) => {
assert_eq!(owner.as_deref(), Some("kigi-tui"));
}
_ => panic!("expected ClearQueue"),
}
// edit: id + newText + editor (resolved via owner/clientIdentifier).
let p = serde_json::json!({
"sessionId": "s1", "id": "p9", "newText": "replacement text"
});
match parse_queue_edit_command("kigi/queue/edit", &p, Some("kigi-vscode".into())) {
Some(SessionCommand::EditQueuedPrompt {
id,
new_text,
editor,
}) => {
assert_eq!(id, "p9");
assert_eq!(new_text, "replacement text");
assert_eq!(editor.as_deref(), Some("kigi-vscode"));
}
_ => panic!("expected EditQueuedPrompt"),
}
// edit without editor (no owner/clientIdentifier) → editor: None.
match parse_queue_edit_command(
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "id": "p9", "newText": "x" }),
None,
) {
Some(SessionCommand::EditQueuedPrompt { editor, .. }) => {
assert!(editor.is_none());
}
_ => panic!("expected EditQueuedPrompt"),
}
// edit without newText → None (can't replace text we don't have).
assert!(
parse_queue_edit_command(
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "id": "p9" }),
None,
)
.is_none()
);
// edit without id → None (can't target an entry).
assert!(
parse_queue_edit_command(
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "newText": "x" }),
None,
)
.is_none()
);
// interject: id + expectedVersion + owner (mirrors remove).
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2
});
match parse_queue_edit_command("kigi/queue/interject", &p, Some("kigi-tui".into())) {
Some(SessionCommand::InterjectQueuedPrompt {
id,
expected_version,
owner,
new_text,
}) => {
assert_eq!(id, "p10");
assert_eq!(expected_version, 2);
assert_eq!(owner.as_deref(), Some("kigi-tui"));
assert_eq!(new_text, None, "newText absent → None");
}
_ => panic!("expected InterjectQueuedPrompt"),
}
// interject with newText (client-edited row) carries the override.
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2, "newText": "edited"
});
match parse_queue_edit_command("kigi/queue/interject", &p, None) {
Some(SessionCommand::InterjectQueuedPrompt { new_text, .. }) => {
assert_eq!(new_text.as_deref(), Some("edited"));
}
_ => panic!("expected InterjectQueuedPrompt"),
}
// Blank newText is dropped → degrades to the stored queue text.
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2, "newText": " "
});
match parse_queue_edit_command("kigi/queue/interject", &p, None) {
Some(SessionCommand::InterjectQueuedPrompt { new_text, .. }) => {
assert_eq!(new_text, None, "blank override must be dropped");
}
_ => panic!("expected InterjectQueuedPrompt"),
}
// interject without expectedVersion defaults to 0.
match parse_queue_edit_command(
"kigi/queue/interject",
&serde_json::json!({ "sessionId": "s1", "id": "p11" }),
None,
) {
Some(SessionCommand::InterjectQueuedPrompt {
expected_version, ..
}) => assert_eq!(expected_version, 0),
_ => panic!("expected InterjectQueuedPrompt"),
}
// interject without id → None (can't target an entry).
assert!(
parse_queue_edit_command("kigi/queue/interject", &serde_json::json!({}), None)
.is_none()
);
// unknown method → None.
assert!(
parse_queue_edit_command("kigi/queue/bogus", &serde_json::json!({}), None).is_none()
);
// remove without id → None (can't target an entry).
assert!(
parse_queue_edit_command("kigi/queue/remove", &serde_json::json!({}), None).is_none()
);
}
}