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
|
|
@ -15,6 +15,36 @@ use url::Url;
|
|||
|
||||
use crate::rmcp;
|
||||
|
||||
/// Ensure credential paths are owner-only (Unix `0o600`).
|
||||
///
|
||||
/// Local helper (not shell-base): `xai-grok-mcp` sits below `config-types` in the
|
||||
/// dep graph, and shell-base pulls shared→config-types→mcp — a cycle if linked.
|
||||
/// Windows ACL tightening stays on auth via shell-base; MCP is Unix-first here.
|
||||
fn ensure_owner_only_permissions(path: &Path) -> std::io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match std::fs::metadata(path) {
|
||||
Ok(metadata) => {
|
||||
let mode = metadata.permissions().mode();
|
||||
if mode & 0o777 != 0o600 {
|
||||
let mut perms = metadata.permissions();
|
||||
perms.set_mode(0o600);
|
||||
std::fs::set_permissions(path, perms)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = path;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, McpCredentialError>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
@ -70,6 +100,15 @@ impl McpCredentialStore {
|
|||
return Ok(Self::default());
|
||||
}
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
// Tighten world-readable credential files on load (hand copies, etc.).
|
||||
// Best-effort: chmod failure must not block using existing tokens.
|
||||
if let Err(e) = ensure_owner_only_permissions(path) {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
error = %e,
|
||||
"mcp credentials: failed to enforce owner-only permissions"
|
||||
);
|
||||
}
|
||||
let store: McpCredentialStore = serde_json::from_str(&content)?;
|
||||
Ok(store)
|
||||
}
|
||||
|
|
@ -188,7 +227,18 @@ impl McpCredentialStore {
|
|||
writer.flush()?;
|
||||
}
|
||||
|
||||
// `mode(0o600)` only applies on create; tighten before rename.
|
||||
// Fail hard on tmp: credentials are not published yet.
|
||||
ensure_owner_only_permissions(&tmp_path)?;
|
||||
std::fs::rename(&tmp_path, path)?;
|
||||
// Best-effort after rename: new tokens are already published.
|
||||
if let Err(e) = ensure_owner_only_permissions(path) {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
path = %path.display(),
|
||||
"mcp: failed to ensure owner-only permissions after credential save"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -432,4 +482,37 @@ mod tests {
|
|||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_dir(&dir);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn save_is_owner_only() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("creds.json");
|
||||
let mut store = McpCredentialStore::default();
|
||||
let url = Url::parse("https://test.example.com/mcp").unwrap();
|
||||
store.insert_rmcp("test", &url, test_stored_creds("c"));
|
||||
store.save_to(&path).unwrap();
|
||||
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn load_tightens_world_readable_credentials() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("creds.json");
|
||||
let mut store = McpCredentialStore::default();
|
||||
let url = Url::parse("https://test.example.com/mcp").unwrap();
|
||||
store.insert_rmcp("test", &url, test_stored_creds("c"));
|
||||
store.save_to(&path).unwrap();
|
||||
let mut loose = std::fs::metadata(&path).unwrap().permissions();
|
||||
loose.set_mode(0o644);
|
||||
std::fs::set_permissions(&path, loose).unwrap();
|
||||
|
||||
let _ = McpCredentialStore::load_from(&path).unwrap();
|
||||
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue