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.
260 lines
10 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|