Synced from monorepo
Changes: - Classify clipboard delivery confidence - Add durable session update append - Scope the xAI session bearer to first-party memory embedding endpoints - Persist subagent outputs to disk and bound long-lived agent state - Add MiniSweAgent:bash for mini-swe-agent parity - Revert taking local sessions off the persistent shell - Contextual tip recommending grok wrap on SSH sessions - Voice STT bearer from model BYOK env_key/api_key - Define exact website policies for sandbox - Gate unsafe shell environments - Shared pin hoist; single require_sha gate for marketplace plugins - Server-signed is-managed claim (closes sidecar-removal downgrade) - Optional require_sha pin for remote plugin installs - Show session title and last exchange in the exit resume hint - Gate shell output redirects - Warn when fail_closed is present but not a boolean - Add canonical text editing core (ratatui-textarea) - Keep execution state out of goal scratch - Add acknowledged persistence primitives - Inherit child network restrictions in sandbox - Fail closed when hook matchers fail to recompile - Add MCP setup preferences for plugin MCPs - Gate sourced shell scripts - Gate file-typed project hooks - grok wrap: restore terminal modes on child death - Harden owner-only permissions on auth and MCP credentials - Create crash dump files with owner-only permissions - Write the agent_id cache owner-only (0600) - SessionMetrics mode skips Mixpanel profile sync - Dashboard: slim live-tail peek - Yank full queued prompt text, not (+N lines) - Defeat clock-rollback on the signed managed-config cache - Stop early session/cancel from overtaking the prompt and wedging the turn slot - Self-heal a diverged agent entrypoint on startup - Add matched inference expectations in test-support - Add AuthSingleFlight cancel/successor gap tests - Remove consumer from external OTEL allowlist and pin scrub coverage - Enable /copy in minimal mode - Surface capacity and API-key detail on 429 errors - Single-flight interactive auth - Fix PageUp/PageDown skipping lines behind sticky prompt header
This commit is contained in:
parent
8adf9013a0
commit
98c3b2438a
225 changed files with 18836 additions and 7156 deletions
|
|
@ -69,19 +69,50 @@ pub fn is_cli_chat_proxy_url(url: &str) -> bool {
|
|||
}
|
||||
false
|
||||
}
|
||||
/// True for first-party xAI endpoints (`*.x.ai`, cli-chat-proxy, and optional
|
||||
/// non-production first-party hosts when that feature is enabled).
|
||||
/// True for xAI-operated endpoints (`*.x.ai`, cli-chat-proxy, and optional
|
||||
/// non-production xAI hosts when that feature is enabled).
|
||||
/// `disable_api_key_auth` refuses keys only for these; other hosts are BYOK and
|
||||
/// exempt. Safe against invalid URLs and suffix attacks (`evil-x.ai.example`).
|
||||
pub fn is_first_party_xai_url(url: &str) -> bool {
|
||||
///
|
||||
/// Scheme-agnostic so credential *refusal* fails closed. To decide where to
|
||||
/// *attach* a credential, use [`is_xai_api_bearer_url`].
|
||||
pub fn is_xai_api_url(url: &str) -> bool {
|
||||
is_xai_api_url_impl(url, false)
|
||||
}
|
||||
/// Like [`is_xai_api_url`], but requires `https` on every arm, so a
|
||||
/// session bearer is never attached to a cleartext endpoint, including loopback
|
||||
/// (a co-located process could otherwise read a token sent to `http://localhost`).
|
||||
pub fn is_xai_api_bearer_url(url: &str) -> bool {
|
||||
is_xai_api_url_impl(url, true)
|
||||
}
|
||||
fn is_xai_api_url_impl(url: &str, require_https: bool) -> bool {
|
||||
if require_https {
|
||||
let Ok(parsed) = reqwest::Url::parse(url) else {
|
||||
return false;
|
||||
};
|
||||
if parsed.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
if is_loopback_host(&parsed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if is_cli_chat_proxy_url(url) {
|
||||
return true;
|
||||
}
|
||||
reqwest::Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|u| u.host_str().map(|h| h.to_owned()))
|
||||
.and_then(|u| u.host_str().map(str::to_owned))
|
||||
.is_some_and(|host| host == "x.ai" || host.ends_with(".x.ai"))
|
||||
}
|
||||
fn is_loopback_host(parsed: &reqwest::Url) -> bool {
|
||||
match parsed.host() {
|
||||
Some(url::Host::Domain(host)) => host == "localhost",
|
||||
Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
|
||||
Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
/// Truncate a string to at most `max_chars` characters.
|
||||
/// Slices at char boundaries so multi-byte UTF-8 never panics.
|
||||
pub fn truncate(s: &str, max_chars: usize) -> &str {
|
||||
|
|
@ -237,25 +268,39 @@ mod tests {
|
|||
));
|
||||
}
|
||||
#[test]
|
||||
fn test_is_first_party_xai_url() {
|
||||
assert!(is_first_party_xai_url("https://api.x.ai/v1"));
|
||||
assert!(is_first_party_xai_url(
|
||||
"https://api.x.ai/v1/chat/completions"
|
||||
));
|
||||
assert!(is_first_party_xai_url("https://x.ai"));
|
||||
assert!(is_first_party_xai_url(
|
||||
fn test_is_xai_api_url() {
|
||||
assert!(is_xai_api_url("https://api.x.ai/v1"));
|
||||
assert!(is_xai_api_url("https://api.x.ai/v1/chat/completions"));
|
||||
assert!(is_xai_api_url("https://x.ai"));
|
||||
assert!(is_xai_api_url(
|
||||
"https://cli-chat-proxy.grok.com/v1/chat/completions"
|
||||
));
|
||||
assert!(!is_first_party_xai_url("https://api.openai.com/v1"));
|
||||
assert!(!is_first_party_xai_url("https://api.anthropic.com/v1"));
|
||||
assert!(!is_first_party_xai_url(
|
||||
"https://generativelanguage.googleapis.com"
|
||||
assert!(!is_xai_api_url("https://api.openai.com/v1"));
|
||||
assert!(!is_xai_api_url("https://api.anthropic.com/v1"));
|
||||
assert!(!is_xai_api_url("https://generativelanguage.googleapis.com"));
|
||||
assert!(!is_xai_api_url("https://api.x.ai.evil.example/v1"));
|
||||
assert!(!is_xai_api_url("https://evil-x.ai.attacker.com/v1"));
|
||||
assert!(!is_xai_api_url("https://prefixx.ai/v1"));
|
||||
assert!(!is_xai_api_url("not-a-url"));
|
||||
assert!(!is_xai_api_url(""));
|
||||
assert!(is_xai_api_url("http://api.x.ai/v1"));
|
||||
assert!(is_xai_api_url("http://localhost:11434/v1"));
|
||||
}
|
||||
#[test]
|
||||
fn test_is_xai_api_bearer_url() {
|
||||
assert!(is_xai_api_bearer_url("https://api.x.ai/v1"));
|
||||
assert!(!is_xai_api_bearer_url("http://api.x.ai/v1"));
|
||||
assert!(!is_xai_api_bearer_url("http://localhost:11434/v1"));
|
||||
{
|
||||
assert!(!is_xai_api_bearer_url("https://localhost:11434/v1"));
|
||||
assert!(!is_xai_api_bearer_url("https://127.0.0.2:11434/v1"));
|
||||
assert!(!is_xai_api_bearer_url("https://[::1]:11434/v1"));
|
||||
}
|
||||
assert!(is_xai_api_bearer_url("https://API.X.AI/v1"));
|
||||
assert!(!is_xai_api_bearer_url(
|
||||
"https://api.x.ai@attacker.example/v1"
|
||||
));
|
||||
assert!(!is_first_party_xai_url("https://api.x.ai.evil.example/v1"));
|
||||
assert!(!is_first_party_xai_url("https://evil-x.ai.attacker.com/v1"));
|
||||
assert!(!is_first_party_xai_url("https://prefixx.ai/v1"));
|
||||
assert!(!is_first_party_xai_url("not-a-url"));
|
||||
assert!(!is_first_party_xai_url(""));
|
||||
assert!(!is_xai_api_bearer_url("https://х.ai/v1"));
|
||||
}
|
||||
#[test]
|
||||
fn test_truncate() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue