grok-build-upstream-mirror/crates/codegen/xai-grok-tools-api/src/config_validation.rs

257 lines
8.2 KiB
Rust
Raw Normal View History

//! Validation of [`ToolConfigEntry`](crate::ToolConfigEntry) fields,
//! shared so the backend's save-time check cannot drift from what the
//! tools server enforces at finalize/bind. Errors carry the offending input
//! so callers can render gRPC violations without re-parsing.
use serde_json::{Map, Value};
use xai_tool_protocol::ToolId;
/// Why a [`ToolConfigEntry`](crate::ToolConfigEntry) is invalid.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolConfigEntryErrorKind {
/// Not valid JSON. Includes an explicitly-set empty string: proto3
/// `optional` tracks presence, so `Some("")` is rejected, not unset.
ParamsJsonParse { error: String, raw: String },
/// Valid JSON but not an object.
ParamsJsonNotObject { value: Value },
/// `name_override` is not a valid `ToolId` (charset/length contract).
NameOverrideInvalid { name: String, error: String },
}
/// Validation error for one entry in a tool-config list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolConfigEntryError {
pub index: usize,
pub tool_id: String,
pub kind: ToolConfigEntryErrorKind,
}
impl ToolConfigEntryError {
/// Request field path of the failing field, e.g. `tools[3].params_json`.
pub fn field_path(&self) -> String {
match self.kind {
ToolConfigEntryErrorKind::ParamsJsonParse { .. }
| ToolConfigEntryErrorKind::ParamsJsonNotObject { .. } => {
format!("tools[{}].params_json", self.index)
}
ToolConfigEntryErrorKind::NameOverrideInvalid { .. } => {
format!("tools[{}].name_override", self.index)
}
}
}
}
impl std::fmt::Display for ToolConfigEntryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
ToolConfigEntryErrorKind::ParamsJsonParse { error, .. } => write!(
f,
"{}: {} failed to parse JSON: {error}",
self.tool_id,
self.field_path()
),
ToolConfigEntryErrorKind::ParamsJsonNotObject { .. } => write!(
f,
"{}: {} must be a JSON object",
self.tool_id,
self.field_path()
),
ToolConfigEntryErrorKind::NameOverrideInvalid { name, error } => write!(
f,
"{}: {} is not a valid tool name ({name:?}): {error}",
self.tool_id,
self.field_path()
),
}
}
}
impl std::error::Error for ToolConfigEntryError {}
/// Parse and validate a `params_json`, returning the decoded object (or
/// `None` when unset). `index`/`tool_id` are only used for error reporting.
pub fn parse_params_json(
index: usize,
tool_id: &str,
params_json: Option<&str>,
) -> Result<Option<Map<String, Value>>, ToolConfigEntryError> {
let Some(raw) = params_json else {
return Ok(None);
};
let value: Value = serde_json::from_str(raw).map_err(|err| ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::ParamsJsonParse {
error: err.to_string(),
raw: raw.to_owned(),
},
})?;
match value {
Value::Object(object) => Ok(Some(object)),
other => Err(ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::ParamsJsonNotObject { value: other },
}),
}
}
/// Validates a `name_override` against the `ToolId` charset/length contract,
/// mirroring [`parse_params_json`] as the shared source of truth.
pub fn validate_name_override(
index: usize,
tool_id: &str,
name_override: Option<&str>,
) -> Result<(), ToolConfigEntryError> {
let Some(name) = name_override else {
return Ok(());
};
ToolId::new(name).map_err(|err| ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::NameOverrideInvalid {
name: name.to_owned(),
error: err.to_string(),
},
})?;
Ok(())
}
/// Returns the first entry whose `id` is not in `allowed_ids`, as
/// `(index, id)`, or `None` when all ids are allowed.
///
/// Pure so backend save-time validation and any future consumer share one rule.
pub fn first_unknown_tool_id<'a>(
entries: &'a [crate::ToolConfigEntry],
allowed_ids: &std::collections::HashSet<String>,
) -> Option<(usize, &'a str)> {
entries
.iter()
.enumerate()
.find(|(_, entry)| !allowed_ids.contains(&entry.id))
.map(|(index, entry)| (index, entry.id.as_str()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unset_params_is_ok_none() {
assert_eq!(parse_params_json(0, "GrokBuild:grep", None), Ok(None));
}
#[test]
fn valid_object_is_returned() {
let parsed = parse_params_json(0, "GrokBuild:grep", Some(r#"{"max_results":50}"#)).unwrap();
assert_eq!(
parsed,
Some(
serde_json::json!({"max_results": 50})
.as_object()
.unwrap()
.clone()
)
);
}
#[test]
fn empty_string_is_a_parse_error() {
let err = parse_params_json(3, "GrokBuild:grep", Some("")).unwrap_err();
assert_eq!(err.index, 3);
assert_eq!(err.field_path(), "tools[3].params_json");
assert!(matches!(
err.kind,
ToolConfigEntryErrorKind::ParamsJsonParse { .. }
));
}
#[test]
fn invalid_json_is_a_parse_error() {
let err = parse_params_json(0, "t", Some("{not json")).unwrap_err();
assert!(matches!(
Synced from monorepo Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
2026-07-21 18:10:23 +00:00
err.kind,
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
2026-07-22 19:18:53 +01:00
ToolConfigEntryErrorKind::ParamsJsonParse { raw, .. } if raw == "{not json"
Synced from monorepo Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
2026-07-21 18:10:23 +00:00
));
}
#[test]
fn non_object_json_is_rejected() {
let err = parse_params_json(1, "t", Some("[1,2,3]")).unwrap_err();
assert!(matches!(
err.kind,
ToolConfigEntryErrorKind::ParamsJsonNotObject {
value: Value::Array(_)
}
));
}
#[test]
fn name_override_unset_or_valid_is_ok() {
assert_eq!(validate_name_override(0, "GrokBuild:grep", None), Ok(()));
for name in ["search", "GrokBuild:grep", "a-b_C9"] {
assert_eq!(
validate_name_override(0, "GrokBuild:grep", Some(name)),
Ok(()),
"name={name:?}"
);
}
}
#[test]
fn name_override_outside_charset_is_rejected() {
for name in ["has space", "", "a:b:c", "dot.name"] {
let err = validate_name_override(2, "GrokBuild:grep", Some(name)).unwrap_err();
assert_eq!(err.index, 2, "name={name:?}");
assert_eq!(err.field_path(), "tools[2].name_override");
assert!(
matches!(
Synced from monorepo Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
2026-07-21 18:10:23 +00:00
&err.kind,
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
2026-07-22 19:18:53 +01:00
ToolConfigEntryErrorKind::NameOverrideInvalid { name: n, .. } if n == name
Synced from monorepo Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
2026-07-21 18:10:23 +00:00
),
"name={name:?} kind={:?}",
err.kind
);
}
}
fn entry(id: &str) -> crate::ToolConfigEntry {
crate::ToolConfigEntry {
id: id.to_owned(),
..Default::default()
}
}
fn allowed(ids: &[&str]) -> std::collections::HashSet<String> {
ids.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn all_ids_present_returns_none() {
let entries = [entry("GrokBuild:grep"), entry("GrokBuild:read_file")];
let allowed = allowed(&["GrokBuild:grep", "GrokBuild:read_file", "GrokBuild:bash"]);
assert_eq!(first_unknown_tool_id(&entries, &allowed), None);
}
#[test]
fn empty_entries_returns_none() {
assert_eq!(
first_unknown_tool_id(&[], &allowed(&["GrokBuild:grep"])),
None
);
}
#[test]
fn first_unknown_id_is_returned_with_index() {
let entries = [
entry("GrokBuild:grep"),
entry("GrokBuild:nonexistent"),
entry("GrokBuild:also_missing"),
];
let allowed = allowed(&["GrokBuild:grep"]);
assert_eq!(
first_unknown_tool_id(&entries, &allowed),
Some((1, "GrokBuild:nonexistent"))
);
}
}