Synced from monorepo

Synced from monorepo

Changes:
- Report invalid MCP server config instead of failing startup
- Keep completed terminal output when the gateway connection is lost
- Show a duration-only detail view for single-task task output
- Don't let a stale registry turn counter hide local sessions
- Raise the file-descriptor soft limit on Linux and log effective limits at startup
- Stop aborting when HTTP client construction fails
- Make session thread and runtime spawn failures recoverable
- Fix main-prompt paste parity in the question freeform input
- Fire SessionEnd hooks on /exit and headless quit
- Embed the deployment-config signing public key
- Repaint paste-chip background on inline panel inputs
- Security: prevent acceptEdits from auto-approving agent writes into the always-trusted global hook root
- Fix stacked "Worked for" markers so parks render as status and turns close with exactly one marker
- Parse hooks from config files
- Add a remote kill-switch for managed-config signature verification
- Security: fix workspace file-reference resolution bypassing workspace filesystem confinement

Source-Revision: d02693a856a54f1030695b36b91d276e96b30b23
This commit is contained in:
grokkybara[bot] 2026-07-25 18:44:42 +00:00
commit 47348d13ec
138 changed files with 7283 additions and 5796 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,122 @@
//! Built-binary e2e: SessionEnd hooks fire on headless process exit.
//!
//! Regression for the non-leader quit path that used to cancel the agent
//! without flushing session actors, so SessionEnd never ran on `/exit` /
//! `grok -p` exit.
//!
//! `#[ignore]`d by default — needs the grok binary (`GROK_BINARY` or a local
//! debug build):
//! ```bash
//! cargo test -p xai-grok-shell --test test_session_end_hook_e2e -- --ignored
//! ```
//!
//! CI coverage of the same machinery without a built binary lives in
//! `xai_grok_shell::agent::activity` tests (the flush quiesce loop and its
//! grace expiry) and `xai_grok_pager::acp::spawn` tests (the worker join:
//! clean exit, worker error, panic rendering, and the abandon-at-budget
//! branch this e2e cannot reach).
use xai_grok_test_support::*;
/// Runs headless with a SessionEnd hook that writes stdin + a marker file.
async fn run_with_session_end_hook() -> (HeadlessResult, MockInferenceServer, tempfile::TempDir) {
let state_dir = tempfile::TempDir::new().expect("create state dir");
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let sandbox = TestSandbox::builder().mock_url(server.url()).git().build();
let state = state_dir.path().display();
let script_path = sandbox.home().join("session_end_hook.sh");
std::fs::write(
&script_path,
format!(
"#!/bin/sh\n\
cat > {state}/stdin.json\n\
touch {state}/marker\n\
exit 0\n"
),
)
.expect("write hook script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755))
.expect("chmod hook script");
}
let hooks_dir = sandbox.grok_home().join("hooks");
std::fs::create_dir_all(&hooks_dir).expect("create hooks dir");
std::fs::write(
hooks_dir.join("session_end.json"),
serde_json::json!({
"hooks": {
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": format!("sh {}", script_path.display()),
"timeout": 30
}]
}]
}
})
.to_string(),
)
.expect("write hook config");
let mut cmd = tokio::process::Command::new(grok_binary());
cmd.args(["-p", "say hello", "--yolo"])
.current_dir(sandbox.workspace())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let result = run_headless_in_sandbox(cmd, sandbox).await;
(result, server, state_dir)
}
#[tokio::test]
#[ignore]
async fn session_end_hook_fires_on_headless_exit() {
let (result, server, state_dir) = run_with_session_end_hook().await;
assert_headless_success(&result, "session_end hook e2e", Some(&server));
let marker = state_dir.path().join("marker");
assert!(
marker.is_file(),
"SessionEnd hook must write a marker on process exit (non-leader flush path); \
missing {marker:?}. stderr:\n{}",
result.stderr
);
let stdin_path = state_dir.path().join("stdin.json");
let text = std::fs::read_to_string(&stdin_path)
.unwrap_or_else(|e| panic!("read {}: {e}", stdin_path.display()));
let envelope: serde_json::Value =
serde_json::from_str(&text).unwrap_or_else(|e| panic!("hook stdin not JSON: {e}\n{text}"));
let event = envelope["hookEventName"]
.as_str()
.unwrap_or_else(|| panic!("hookEventName missing: {envelope}"));
assert!(
event == "session_end" || event == "SessionEnd",
"expected SessionEnd event name, got {event:?}"
);
// `reason` is an already-shipped part of the hook payload that user scripts
// match on: `shutdown` is emitted by the `SessionCommand::Shutdown` arm
// (leader auto-update / relaunch today), `channel_closed` by the actor's
// channel-closed arm. This change adds no new value — it routes non-leader
// exits through the existing Shutdown command — so renaming `shutdown` to
// something narrower here would break those scripts. Future distinct causes
// (e.g. a signal-driven or idle-eviction end) should be added as new values
// alongside it.
let reason = envelope["reason"]
.as_str()
.unwrap_or_else(|| panic!("reason missing: {envelope}"));
assert_eq!(
reason, "shutdown",
"flush path should send SessionCommand::Shutdown (reason=shutdown), got {reason:?}"
);
}