Synced from monorepo
Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
This commit is contained in:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -65,6 +65,17 @@ static WORKSPACE_RPC_REQUESTS_TOTAL: std::sync::LazyLock<IntCounterVec> =
|
|||
)
|
||||
.unwrap()
|
||||
});
|
||||
/// Failed `workspace.*` RPC dispatches, by method and
|
||||
/// [`WorkspaceError::metric_kind`].
|
||||
static WORKSPACE_RPC_ERRORS_TOTAL: std::sync::LazyLock<IntCounterVec> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
register_int_counter_vec!(
|
||||
"grok_workspace_rpc_errors_total",
|
||||
"Failed workspace RPC dispatches, by method and error kind",
|
||||
&["method", "error_kind"]
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
/// Per-method wall-clock duration of a `workspace.*` RPC dispatch.
|
||||
static WORKSPACE_RPC_DURATION_SECONDS: std::sync::LazyLock<HistogramVec> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
|
|
@ -88,6 +99,9 @@ pub(crate) fn init_metrics() {
|
|||
WORKSPACE_RPC_REQUESTS_TOTAL
|
||||
.with_label_values(&[UNKNOWN_METHOD_LABEL, "error"])
|
||||
.inc_by(0);
|
||||
WORKSPACE_RPC_ERRORS_TOTAL
|
||||
.with_label_values(&[UNKNOWN_METHOD_LABEL, "hub_error"])
|
||||
.inc_by(0);
|
||||
let _ = WORKSPACE_RPC_DURATION_SECONDS.with_label_values(&[UNKNOWN_METHOD_LABEL]);
|
||||
}
|
||||
/// Resolve the caller identity for a mutation RPC: the server-bound envelope
|
||||
|
|
@ -106,7 +120,9 @@ fn resolve_mutation_caller<'a>(
|
|||
.with_label_values(&[method, "param_mismatch"])
|
||||
.inc();
|
||||
tracing::warn!(
|
||||
method, envelope_session = % envelope, param_caller = % param,
|
||||
method,
|
||||
envelope_session = %envelope,
|
||||
param_caller = %param,
|
||||
"caller_session_id param disagrees with the server-bound envelope session; \
|
||||
trusting the envelope"
|
||||
);
|
||||
|
|
@ -144,9 +160,7 @@ fn record_mutation_rpc<T>(
|
|||
match result {
|
||||
Ok(_) => tracing::info!(method, caller, target, "workspace mutation rpc"),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
method, caller, target, error = % e, "workspace mutation rpc failed"
|
||||
);
|
||||
tracing::warn!(method, caller, target, error = %e, "workspace mutation rpc failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +391,11 @@ impl WorkspaceRpcHandler {
|
|||
.map(|n| n.to_string_lossy().to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "sh".to_string());
|
||||
Ok(serde_json::json!({ "os" : os, "shell" : shell, "cwd" : cwd_str, }))
|
||||
Ok(serde_json::json!({
|
||||
"os": os,
|
||||
"shell": shell,
|
||||
"cwd": cwd_str,
|
||||
}))
|
||||
}
|
||||
<GitStatusReq as WorkspaceRpc>::METHOD => {
|
||||
static DEPRECATION_WARNING: std::sync::Once = std::sync::Once::new();
|
||||
|
|
@ -518,10 +536,12 @@ impl WorkspaceRpcHandler {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
results.push(serde_json::json!(
|
||||
{ "path" : full_path.to_string_lossy(), "ref" : ref_path,
|
||||
"exists" : exists, "content" : content, }
|
||||
));
|
||||
results.push(serde_json::json!({
|
||||
"path": full_path.to_string_lossy(),
|
||||
"ref": ref_path,
|
||||
"exists": exists,
|
||||
"content": content,
|
||||
}));
|
||||
}
|
||||
Ok(Value::Array(results))
|
||||
}
|
||||
|
|
@ -589,7 +609,7 @@ impl WorkspaceRpcHandler {
|
|||
}
|
||||
<LoadPermissionsReq as WorkspaceRpc>::METHOD => {
|
||||
let cwd = self.workspace.root_cwd()?;
|
||||
Ok(crate::discovery::load_permissions(&cwd).await)
|
||||
Ok(crate::discovery::load_permissions(&cwd, true).await)
|
||||
}
|
||||
<LoadEnvrcReq as WorkspaceRpc>::METHOD => {
|
||||
let cwd = self.workspace.root_cwd()?;
|
||||
|
|
@ -906,12 +926,20 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
)
|
||||
}
|
||||
fn input_schema(&self) -> Option<Value> {
|
||||
Some(serde_json::json!(
|
||||
{ "type" : "object", "properties" : { "method" : { "type" : "string",
|
||||
"description" : "The workspace.* method to invoke" }, "params" : { "type"
|
||||
: "object", "description" : "Method parameters" } }, "required" :
|
||||
["method"] }
|
||||
))
|
||||
Some(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "The workspace.* method to invoke"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Method parameters"
|
||||
}
|
||||
},
|
||||
"required": ["method"]
|
||||
}))
|
||||
}
|
||||
async fn handle_call(&self, ctx: ToolCallContext, args: Value) -> ToolStream<TypedToolOutput> {
|
||||
let tool_id = self.tool_id();
|
||||
|
|
@ -939,8 +967,8 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
)
|
||||
.await;
|
||||
let is_unknown_method = matches!(
|
||||
& result, Err(WorkspaceError::HubError(msg)) if msg
|
||||
.starts_with(UNKNOWN_METHOD_ERR_PREFIX)
|
||||
&result,
|
||||
Err(WorkspaceError::HubError(msg)) if msg.starts_with(UNKNOWN_METHOD_ERR_PREFIX)
|
||||
);
|
||||
let method_label = if is_unknown_method {
|
||||
UNKNOWN_METHOD_LABEL
|
||||
|
|
@ -950,6 +978,11 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
WORKSPACE_RPC_REQUESTS_TOTAL
|
||||
.with_label_values(&[method_label, if result.is_ok() { "ok" } else { "error" }])
|
||||
.inc();
|
||||
if let Err(e) = &result {
|
||||
WORKSPACE_RPC_ERRORS_TOTAL
|
||||
.with_label_values(&[method_label, e.metric_kind()])
|
||||
.inc();
|
||||
}
|
||||
WORKSPACE_RPC_DURATION_SECONDS
|
||||
.with_label_values(&[method_label])
|
||||
.observe(start.elapsed().as_secs_f64());
|
||||
|
|
@ -965,16 +998,16 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
match frame.event {
|
||||
HookEvent::Cancel => {
|
||||
if let Some(call_id) = &frame.call_id {
|
||||
tracing::info!(% session_id, % call_id, "cancel hook received");
|
||||
tracing::info!(%session_id, %call_id, "cancel hook received");
|
||||
self.workspace
|
||||
.cancel_tool_call(session_id.as_str(), call_id.as_str());
|
||||
} else {
|
||||
tracing::info!(% session_id, "cancel hook received (session-wide)");
|
||||
tracing::info!(%session_id, "cancel hook received (session-wide)");
|
||||
self.workspace.cancel_all_tool_calls(session_id.as_str());
|
||||
}
|
||||
}
|
||||
HookEvent::SessionEnded => {
|
||||
tracing::info!(% session_id, "session_ended hook received");
|
||||
tracing::info!(%session_id, "session_ended hook received");
|
||||
self.workspace
|
||||
.teardown_session_mcp(session_id.as_str())
|
||||
.await;
|
||||
|
|
@ -989,14 +1022,17 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
match serde_json::from_value::<BeforeTurnPayload>(payload) {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
session = % session_id, turn = p.turn_number, model = % p
|
||||
.model_id, "before_turn hook received"
|
||||
session = %session_id,
|
||||
turn = p.turn_number,
|
||||
model = %p.model_id,
|
||||
"before_turn hook received"
|
||||
);
|
||||
self.workspace.on_before_turn(session_id.as_str(), &p).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = % e, "before_turn payload deserialization failed"
|
||||
error = %e,
|
||||
"before_turn payload deserialization failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1004,30 +1040,32 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
AFTER_TURN_KIND => match serde_json::from_value::<AfterTurnPayload>(payload) {
|
||||
Ok(p) => {
|
||||
tracing::info!(
|
||||
session = % session_id, turn = p.turn_number, outcome = ? p
|
||||
.outcome, duration_ms = p.duration_ms,
|
||||
session = %session_id,
|
||||
turn = p.turn_number,
|
||||
outcome = ?p.outcome,
|
||||
duration_ms = p.duration_ms,
|
||||
"after_turn hook received"
|
||||
);
|
||||
self.workspace.on_after_turn(session_id.as_str(), &p).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = % e, "after_turn payload deserialization failed"
|
||||
error = %e,
|
||||
"after_turn payload deserialization failed"
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
tracing::debug!(
|
||||
kind = % kind, session = % session_id,
|
||||
kind = %kind,
|
||||
session = %session_id,
|
||||
"unrecognized custom hook kind"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
HookEvent::Pause | HookEvent::Resume => {
|
||||
tracing::debug!(
|
||||
% session_id, event = ? frame.event, "hook not yet implemented"
|
||||
);
|
||||
tracing::debug!(%session_id, event = ?frame.event, "hook not yet implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1048,7 +1086,7 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
let request: TurnHookRequest = match serde_json::from_value(payload) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = % e, % session_id, "invalid turn hook request");
|
||||
tracing::warn!(error = %e, %session_id, "invalid turn hook request");
|
||||
return no_op();
|
||||
}
|
||||
};
|
||||
|
|
@ -1092,12 +1130,14 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
if !start_drain {
|
||||
if became_empty {
|
||||
tracing::info!(
|
||||
session = % params.session_id, reason = % params.reason,
|
||||
session = %params.session_id,
|
||||
reason = %params.reason,
|
||||
"workspace: hub evict — already draining/shutting down; dropped session only"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
session = % params.session_id, reason = % params.reason,
|
||||
session = %params.session_id,
|
||||
reason = %params.reason,
|
||||
"workspace: hub evict — other sessions live; dropped session only"
|
||||
);
|
||||
}
|
||||
|
|
@ -1105,8 +1145,9 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
}
|
||||
let grace = std::time::Duration::from_millis(params.grace_period_ms);
|
||||
tracing::info!(
|
||||
session = % params.session_id, reason = % params.reason, grace_period_ms =
|
||||
params.grace_period_ms,
|
||||
session = %params.session_id,
|
||||
reason = %params.reason,
|
||||
grace_period_ms = params.grace_period_ms,
|
||||
"workspace: hub evict — last session; commencing two-phase drain"
|
||||
);
|
||||
let unfinished = self
|
||||
|
|
@ -1115,7 +1156,8 @@ impl ToolServerHandler for WorkspaceRpcHandler {
|
|||
.await;
|
||||
if unfinished > 0 {
|
||||
tracing::warn!(
|
||||
session = % params.session_id, unfinished,
|
||||
session = %params.session_id,
|
||||
unfinished,
|
||||
"workspace: hub evict drain left items pending"
|
||||
);
|
||||
}
|
||||
|
|
@ -1212,8 +1254,9 @@ mod tests {
|
|||
let result = handler
|
||||
.dispatch("workspace.nonexistent", Value::Null, None)
|
||||
.await;
|
||||
assert!(matches!(result, Err(WorkspaceError::HubError(msg)) if msg
|
||||
.contains("unknown workspace method")));
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(msg)) if msg.contains("unknown workspace method"))
|
||||
);
|
||||
}
|
||||
/// A hub evict runs the two-phase drain then settles into terminal
|
||||
/// ShuttingDown (not a lingering Draining) for an evicted workspace.
|
||||
|
|
@ -1298,7 +1341,7 @@ mod tests {
|
|||
let value = handler
|
||||
.dispatch(
|
||||
"workspace.list_background_tasks",
|
||||
serde_json::json!({ "session_id" : "bg-rpc" }),
|
||||
serde_json::json!({"session_id": "bg-rpc"}),
|
||||
Some("bg-rpc"),
|
||||
)
|
||||
.await
|
||||
|
|
@ -1381,7 +1424,7 @@ mod tests {
|
|||
let value = handler
|
||||
.dispatch(
|
||||
"workspace.tasks_snapshot",
|
||||
serde_json::json!({ "session_id" : "snap-rpc" }),
|
||||
serde_json::json!({"session_id": "snap-rpc"}),
|
||||
Some("snap-rpc"),
|
||||
)
|
||||
.await
|
||||
|
|
@ -1537,7 +1580,7 @@ mod tests {
|
|||
async fn dispatch_tool_definitions_returns_known_tools() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "session_id" : "main" });
|
||||
let params = serde_json::json!({"session_id": "main"});
|
||||
let result = handler
|
||||
.dispatch("workspace.tool_definitions", params, None)
|
||||
.await;
|
||||
|
|
@ -1561,7 +1604,7 @@ mod tests {
|
|||
async fn dispatch_tool_definitions_unknown_session() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "session_id" : "ghost" });
|
||||
let params = serde_json::json!({"session_id": "ghost"});
|
||||
let result = handler
|
||||
.dispatch("workspace.tool_definitions", params, None)
|
||||
.await;
|
||||
|
|
@ -1614,9 +1657,7 @@ mod tests {
|
|||
async fn dispatch_drop_session_self_succeeds() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle.clone());
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "main", "session_id" : "main" }
|
||||
);
|
||||
let params = serde_json::json!({"caller_session_id": "main", "session_id": "main"});
|
||||
let result = handler
|
||||
.dispatch("workspace.drop_session", params, None)
|
||||
.await;
|
||||
|
|
@ -1631,8 +1672,7 @@ mod tests {
|
|||
.dispatch("workspace.update_tool_config", serde_json::json!({}), None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1653,10 +1693,11 @@ mod tests {
|
|||
let mismatch_before = caller_mismatch_count("update_tool_config", "param_mismatch");
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "spoofed", "session_id" : "main", "new_config" :
|
||||
baseline_config_value(), }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"caller_session_id": "spoofed",
|
||||
"session_id": "main",
|
||||
"new_config": baseline_config_value(),
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.update_tool_config", params, Some("main"))
|
||||
.await;
|
||||
|
|
@ -1676,10 +1717,11 @@ mod tests {
|
|||
async fn dispatch_update_tool_config_envelope_cross_session_unauthorized() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle.clone());
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "main", "session_id" : "main", "new_config" :
|
||||
baseline_config_value(), }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"caller_session_id": "main",
|
||||
"session_id": "main",
|
||||
"new_config": baseline_config_value(),
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.update_tool_config", params, Some("other"))
|
||||
.await;
|
||||
|
|
@ -1700,10 +1742,11 @@ mod tests {
|
|||
let absent_before = caller_mismatch_count("update_tool_config", "envelope_absent");
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "main", "session_id" : "main", "new_config" :
|
||||
baseline_config_value(), }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"caller_session_id": "main",
|
||||
"session_id": "main",
|
||||
"new_config": baseline_config_value(),
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.update_tool_config", params, None)
|
||||
.await;
|
||||
|
|
@ -1723,9 +1766,10 @@ mod tests {
|
|||
async fn dispatch_update_tool_config_envelope_only_without_param() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "session_id" : "main", "new_config" : baseline_config_value(), }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"session_id": "main",
|
||||
"new_config": baseline_config_value(),
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.update_tool_config", params, Some("main"))
|
||||
.await;
|
||||
|
|
@ -1769,9 +1813,7 @@ mod tests {
|
|||
.get();
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle.clone());
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "spoofed", "session_id" : "main" }
|
||||
);
|
||||
let params = serde_json::json!({"caller_session_id": "spoofed", "session_id": "main"});
|
||||
let result = handler
|
||||
.dispatch("workspace.drop_session", params, Some("main"))
|
||||
.await;
|
||||
|
|
@ -1791,9 +1833,7 @@ mod tests {
|
|||
async fn dispatch_drop_session_envelope_cross_session_unauthorized() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle.clone());
|
||||
let params = serde_json::json!(
|
||||
{ "caller_session_id" : "main", "session_id" : "main" }
|
||||
);
|
||||
let params = serde_json::json!({"caller_session_id": "main", "session_id": "main"});
|
||||
let result = handler
|
||||
.dispatch("workspace.drop_session", params, Some("observer-ish"))
|
||||
.await;
|
||||
|
|
@ -1815,7 +1855,7 @@ mod tests {
|
|||
let _ = handler
|
||||
.dispatch(
|
||||
"workspace.configure_mcp",
|
||||
serde_json::json!({ "mcp_servers" : [] }),
|
||||
serde_json::json!({"mcp_servers": []}),
|
||||
Some("mcp-fresh"),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -1831,9 +1871,9 @@ mod tests {
|
|||
async fn dispatch_hunk_action_unknown_action() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "action" : { "hunk_id" : "test-id", "action" : "dance" } }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"action": {"hunk_id": "test-id", "action": "dance"}
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.hunk_action", params, None)
|
||||
.await;
|
||||
|
|
@ -1846,7 +1886,9 @@ mod tests {
|
|||
async fn dispatch_hunk_action_malformed_json() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "action" : "not-an-object" });
|
||||
let params = serde_json::json!({
|
||||
"action": "not-an-object"
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.hunk_action", params, None)
|
||||
.await;
|
||||
|
|
@ -1864,8 +1906,7 @@ mod tests {
|
|||
.dispatch("workspace.hunk_action", params, None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1873,13 +1914,12 @@ mod tests {
|
|||
async fn dispatch_hunk_file_action_missing_path() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "action" : "accept" });
|
||||
let params = serde_json::json!({"action": "accept"});
|
||||
let result = handler
|
||||
.dispatch("workspace.hunk_file_action", params, None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1887,13 +1927,12 @@ mod tests {
|
|||
async fn dispatch_hunk_turn_action_missing_prompt_index() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "action" : "accept" });
|
||||
let params = serde_json::json!({"action": "accept"});
|
||||
let result = handler
|
||||
.dispatch("workspace.hunk_turn_action", params, None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1901,7 +1940,7 @@ mod tests {
|
|||
async fn dispatch_hunk_all_action_invalid_action() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!({ "action" : "explode" });
|
||||
let params = serde_json::json!({"action": "explode"});
|
||||
let result = handler
|
||||
.dispatch("workspace.hunk_all_action", params, None)
|
||||
.await;
|
||||
|
|
@ -1941,7 +1980,7 @@ mod tests {
|
|||
let result = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_open",
|
||||
serde_json::json!({ "hidden" : false }),
|
||||
serde_json::json!({"hidden": false}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -1958,7 +1997,7 @@ mod tests {
|
|||
let result = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_close",
|
||||
serde_json::json!({ "search_id" : "nonexistent" }),
|
||||
serde_json::json!({"search_id": "nonexistent"}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -1972,13 +2011,12 @@ mod tests {
|
|||
let result = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_change",
|
||||
serde_json::json!({ "query" : "test" }),
|
||||
serde_json::json!({"query": "test"}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1990,8 +2028,7 @@ mod tests {
|
|||
.dispatch("workspace.fuzzy_search", serde_json::json!({}), None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing search_id")),
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing search_id")),
|
||||
"got {result:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -2002,7 +2039,7 @@ mod tests {
|
|||
let open_result = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_open",
|
||||
serde_json::json!({ "hidden" : false }),
|
||||
serde_json::json!({"hidden": false}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
|
@ -2014,7 +2051,7 @@ mod tests {
|
|||
let close_result = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_close",
|
||||
serde_json::json!({ "search_id" : search_id }),
|
||||
serde_json::json!({"search_id": search_id}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
|
@ -2027,7 +2064,7 @@ mod tests {
|
|||
let close_again = handler
|
||||
.dispatch(
|
||||
"workspace.fuzzy_close",
|
||||
serde_json::json!({ "search_id" : search_id }),
|
||||
serde_json::json!({"search_id": search_id}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
|
@ -2045,9 +2082,10 @@ mod tests {
|
|||
let mut ctx = ToolCallContext::default();
|
||||
ctx.extensions
|
||||
.insert(xai_tool_runtime::SessionContext("main".to_owned()));
|
||||
let args = serde_json::json!(
|
||||
{ "method" : "workspace.get_session_summary", "params" : {} }
|
||||
);
|
||||
let args = serde_json::json!({
|
||||
"method": "workspace.get_session_summary",
|
||||
"params": {}
|
||||
});
|
||||
let mut stream = handler.handle_call(ctx, args).await;
|
||||
let item = next_item(&mut stream).await.expect("should have terminal");
|
||||
match item {
|
||||
|
|
@ -2069,9 +2107,10 @@ mod tests {
|
|||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let ctx = ToolCallContext::default();
|
||||
let args = serde_json::json!(
|
||||
{ "method" : "workspace.nonexistent", "params" : {} }
|
||||
);
|
||||
let args = serde_json::json!({
|
||||
"method": "workspace.nonexistent",
|
||||
"params": {}
|
||||
});
|
||||
let mut stream = handler.handle_call(ctx, args).await;
|
||||
let item = next_item(&mut stream).await.expect("should have terminal");
|
||||
match item {
|
||||
|
|
@ -2107,9 +2146,7 @@ mod tests {
|
|||
let mut stream = handler
|
||||
.handle_call(
|
||||
ctx,
|
||||
serde_json::json!(
|
||||
{ "method" : "workspace.get_session_summary", "params" : {} }
|
||||
),
|
||||
serde_json::json!({"method": "workspace.get_session_summary", "params": {}}),
|
||||
)
|
||||
.await;
|
||||
let _ = next_item(&mut stream).await;
|
||||
|
|
@ -2131,10 +2168,13 @@ mod tests {
|
|||
let unknown_before = WORKSPACE_RPC_REQUESTS_TOTAL
|
||||
.with_label_values(&[UNKNOWN_METHOD_LABEL, "error"])
|
||||
.get();
|
||||
let kind_before = WORKSPACE_RPC_ERRORS_TOTAL
|
||||
.with_label_values(&[UNKNOWN_METHOD_LABEL, "hub_error"])
|
||||
.get();
|
||||
let mut stream = handler
|
||||
.handle_call(
|
||||
ToolCallContext::default(),
|
||||
serde_json::json!({ "method" : BOGUS, "params" : {} }),
|
||||
serde_json::json!({"method": BOGUS, "params": {}}),
|
||||
)
|
||||
.await;
|
||||
let _ = next_item(&mut stream).await;
|
||||
|
|
@ -2145,6 +2185,13 @@ mod tests {
|
|||
> unknown_before,
|
||||
"an unrecognized method must increment the collapsed unknown/error counter"
|
||||
);
|
||||
assert!(
|
||||
WORKSPACE_RPC_ERRORS_TOTAL
|
||||
.with_label_values(&[UNKNOWN_METHOD_LABEL, "hub_error"])
|
||||
.get()
|
||||
> kind_before,
|
||||
"a failed dispatch must also record its error_kind on the errors counter"
|
||||
);
|
||||
let has_bogus_series = prometheus::gather()
|
||||
.iter()
|
||||
.filter(|mf| mf.name() == "grok_workspace_rpc_requests_total")
|
||||
|
|
@ -2176,8 +2223,7 @@ mod tests {
|
|||
.dispatch("workspace.git_commit", serde_json::json!({}), None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field"))
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field"))
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
|
|
@ -2188,8 +2234,7 @@ mod tests {
|
|||
.dispatch("workspace.git_checkout", serde_json::json!({}), None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing field"))
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing field"))
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
|
|
@ -2200,8 +2245,7 @@ mod tests {
|
|||
.dispatch("workspace.git_stage_content", serde_json::json!({}), None)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg
|
||||
.contains("missing"))
|
||||
matches!(result, Err(WorkspaceError::HubError(ref msg)) if msg.contains("missing"))
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
|
|
@ -2277,7 +2321,7 @@ mod tests {
|
|||
hook_id: None,
|
||||
event: HookEvent::Custom {
|
||||
kind: turn_hook::BEFORE_TURN_KIND.to_string(),
|
||||
payload: serde_json::json!({ "garbage" : true }),
|
||||
payload: serde_json::json!({"garbage": true}),
|
||||
},
|
||||
trace_context: None,
|
||||
};
|
||||
|
|
@ -2385,9 +2429,9 @@ mod tests {
|
|||
let handle = make_handle();
|
||||
let root = handle.root_cwd().unwrap();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "test_file.txt", "content" : "hello world" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "test_file.txt", "content": "hello world"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2409,9 +2453,9 @@ mod tests {
|
|||
async fn dispatch_put_files_rejects_path_traversal() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "../escape.txt", "content" : "evil" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "../escape.txt", "content": "evil"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2451,9 +2495,9 @@ mod tests {
|
|||
async fn dispatch_put_files_rejects_absolute_outside_root() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "/etc/passwd", "content" : "evil" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "/etc/passwd", "content": "evil"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2480,10 +2524,9 @@ mod tests {
|
|||
let root = handle.root_cwd().unwrap();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let abs = root.join("sub/abs.txt");
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : abs.to_str().expect("utf-8 path"), "content" :
|
||||
"hello" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": abs.to_str().expect("utf-8 path"), "content": "hello"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2509,9 +2552,9 @@ mod tests {
|
|||
let outside = tempfile::tempdir().expect("create outside dir");
|
||||
std::os::unix::fs::symlink(outside.path(), root.join("escape_link"))
|
||||
.expect("create symlink");
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "escape_link/evil.txt", "content" : "pwned" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "escape_link/evil.txt", "content": "pwned"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2538,10 +2581,12 @@ mod tests {
|
|||
let handle = make_handle();
|
||||
let root = handle.root_cwd().unwrap();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "good.txt", "content" : "valid content" }, { "path" :
|
||||
"../bad.txt", "content" : "should fail" },] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [
|
||||
{"path": "good.txt", "content": "valid content"},
|
||||
{"path": "../bad.txt", "content": "should fail"},
|
||||
]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.put_files", params, None)
|
||||
.await
|
||||
|
|
@ -2569,7 +2614,9 @@ mod tests {
|
|||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let content = "read me back";
|
||||
std::fs::write(root.join("readable.txt"), content).unwrap();
|
||||
let params = serde_json::json!({ "files" : [{ "path" : "readable.txt" }] });
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "readable.txt"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2600,9 +2647,9 @@ mod tests {
|
|||
async fn dispatch_get_files_nonexistent_returns_not_exists() {
|
||||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "does_not_exist.txt" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "does_not_exist.txt"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2624,7 +2671,9 @@ mod tests {
|
|||
let root = handle.root_cwd().unwrap();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
std::fs::create_dir_all(root.join("a_directory")).unwrap();
|
||||
let params = serde_json::json!({ "files" : [{ "path" : "a_directory" }] });
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "a_directory"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2646,7 +2695,9 @@ mod tests {
|
|||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let binary_content: &[u8] = b"\xff\xfe\x00\x01";
|
||||
std::fs::write(root.join("binary.bin"), binary_content).unwrap();
|
||||
let params = serde_json::json!({ "files" : [{ "path" : "binary.bin" }] });
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "binary.bin"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2687,9 +2738,9 @@ mod tests {
|
|||
let content = "cacheable content";
|
||||
std::fs::write(root.join("cached.txt"), content).unwrap();
|
||||
let expected_hash = test_sha256(content.as_bytes());
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "cached.txt", "if_none_match" : expected_hash }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "cached.txt", "if_none_match": expected_hash}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2716,10 +2767,9 @@ mod tests {
|
|||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let content = "fresh content";
|
||||
std::fs::write(root.join("stale.txt"), content).unwrap();
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "stale.txt", "if_none_match" :
|
||||
"0000000000000000000000000000000000000000000000000000000000000000" }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "stale.txt", "if_none_match": "0000000000000000000000000000000000000000000000000000000000000000"}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2745,9 +2795,9 @@ mod tests {
|
|||
let handle = make_handle();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let content = "round trip content";
|
||||
let put_params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "round_trip.txt", "content" : content }] }
|
||||
);
|
||||
let put_params = serde_json::json!({
|
||||
"files": [{"path": "round_trip.txt", "content": content}]
|
||||
});
|
||||
let put_result = handler
|
||||
.dispatch("workspace.put_files", put_params, None)
|
||||
.await
|
||||
|
|
@ -2755,9 +2805,9 @@ mod tests {
|
|||
let put_res: PutFilesRes = serde_json::from_value(put_result).unwrap();
|
||||
assert!(put_res.results[0].ok);
|
||||
let put_hash = put_res.results[0].hash.clone().unwrap();
|
||||
let get_params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "round_trip.txt" }] }
|
||||
);
|
||||
let get_params = serde_json::json!({
|
||||
"files": [{"path": "round_trip.txt"}]
|
||||
});
|
||||
let get_result = handler
|
||||
.dispatch("workspace.get_files", get_params, None)
|
||||
.await
|
||||
|
|
@ -2780,10 +2830,9 @@ mod tests {
|
|||
let handle = make_handle();
|
||||
let root = handle.root_cwd().unwrap();
|
||||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let params1 = serde_json::json!(
|
||||
{ "files" : [{ "path" : "chunked.txt", "content" : "hello", "append" : false
|
||||
}] }
|
||||
);
|
||||
let params1 = serde_json::json!({
|
||||
"files": [{"path": "chunked.txt", "content": "hello", "append": false}]
|
||||
});
|
||||
let res1 = handler
|
||||
.dispatch("workspace.put_files", params1, None)
|
||||
.await
|
||||
|
|
@ -2796,10 +2845,9 @@ mod tests {
|
|||
test_sha256(b"hello"),
|
||||
"hash should be of the appended chunk, not full file"
|
||||
);
|
||||
let params2 = serde_json::json!(
|
||||
{ "files" : [{ "path" : "chunked.txt", "content" : " world", "append" : true
|
||||
}] }
|
||||
);
|
||||
let params2 = serde_json::json!({
|
||||
"files": [{"path": "chunked.txt", "content": " world", "append": true}]
|
||||
});
|
||||
let res2 = handler
|
||||
.dispatch("workspace.put_files", params2, None)
|
||||
.await
|
||||
|
|
@ -2822,9 +2870,9 @@ mod tests {
|
|||
let handler = WorkspaceRpcHandler::new(handle);
|
||||
let content = "0123456789";
|
||||
std::fs::write(root.join("range.txt"), content).unwrap();
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "range.txt", "offset" : 3, "length" : 4 }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{"path": "range.txt", "offset": 3, "length": 4}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
@ -2858,10 +2906,14 @@ mod tests {
|
|||
let content = "abcdefghij";
|
||||
std::fs::write(root.join("range_cache.txt"), content).unwrap();
|
||||
let full_hash = test_sha256(content.as_bytes());
|
||||
let params = serde_json::json!(
|
||||
{ "files" : [{ "path" : "range_cache.txt", "offset" : 2, "length" : 3,
|
||||
"if_none_match" : full_hash, }] }
|
||||
);
|
||||
let params = serde_json::json!({
|
||||
"files": [{
|
||||
"path": "range_cache.txt",
|
||||
"offset": 2,
|
||||
"length": 3,
|
||||
"if_none_match": full_hash,
|
||||
}]
|
||||
});
|
||||
let result = handler
|
||||
.dispatch("workspace.get_files", params, None)
|
||||
.await
|
||||
|
|
|
|||
Loading…
Reference in a new issue