Synced from monorepo

Changes:
- grok-shell: request workspaces:read/write OAuth2 scopes
- security: fix SSRF bypass via HTTP redirect in hook runner
- fix(grok-build): enterprise STT WSS URL + API-key voice bearer
- Harden identity-change purge and sync-marker invariants
- sandbox + workspace-server: delete the legacy ready-file arm
- Show billing URL when browser cannot open
- fix(pager): show folder-trust UI in minimal mode
- fix(pager): drain task_backgrounded before no-wait headless exit
- grok-agent-sdk: stop SDK-spawned agents from staging self-updates they can never adopt
- Split settings_modal into directory module
- Delegate VS Code SSH file links
- grok-shell: release the workspace session binding when a session is removed
- keep skills reachable when their name collides with a client builtin
- Preserve semantic link targets
This commit is contained in:
grokkybara[bot] 2026-07-16 20:27:30 +01:00
commit 8adf9013a0
117 changed files with 16998 additions and 14540 deletions

View file

@ -1,282 +0,0 @@
//! Core end-to-end KEYED managed-config tests — verified persist, rejected
//! persist-nothing, and the stripped-sidecar refusal. The harness (and the
//! seam/serial constraints every test here must follow) lives in
//! `signed_managed_config/common.rs`.
//!
//! Placement rule: this binary pins the review-cited security claims
//! (verify-persists / reject-persists-nothing / sidecar-deletion-refuses); new
//! keyed scenarios go in `signed_managed_config_extended.rs` unless they alter
//! one of those three claims.
#[path = "signed_managed_config/common.rs"]
mod common;
use common::{
MANAGED, REQUIREMENTS_FAIL_CLOSED, forged_team_body, install_test_key, reset, signed_team_body,
spawn_mock, team_identity, test_home, write_config, write_team_auth,
};
use serial_test::serial;
use xai_grok_config::signed_policy;
/// A rejected envelope persists NOTHING: the prior principal's files survive
/// (verify-before-evict), no sidecar appears, and the marker is not rewritten.
#[tokio::test]
#[serial]
async fn rejected_signature_persists_nothing_and_records_no_marker() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
// Prior trusted state: team-b's files + marker (as if synced earlier).
std::fs::write(home.join("managed_config.toml"), "[cli]\nprior = true\n").unwrap();
std::fs::write(home.join("requirements.toml"), "[features]\n").unwrap();
xai_grok_shell::config::mark_managed_config_synced(xai_grok_shell::config::SyncMarker {
principal: Some("team-b"),
had_managed_config: true,
had_requirements: true,
key_fingerprint: None,
fail_closed: false,
});
let url = spawn_mock(forged_team_body(&kp, "team-007"));
write_config(&home, &url);
write_team_auth(&home, "team-007");
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("a rejected signature is a no-op, not a transport error");
assert!(!wrote, "nothing may be persisted for a rejected envelope");
assert_eq!(
std::fs::read_to_string(home.join("managed_config.toml")).unwrap(),
"[cli]\nprior = true\n",
"verify-before-evict: the prior policy must survive the identity switch"
);
assert!(home.join("requirements.toml").exists());
assert!(
!home.join("managed_config.sig.json").exists(),
"no sidecar may be written for a rejected envelope"
);
let marker = std::fs::read_to_string(home.join("managed_config_cache.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&marker).unwrap();
assert_eq!(
v["principal"].as_str(),
Some("team-b"),
"the marker must not be rewritten for a rejected fetch: {marker}"
);
}
/// A good envelope persists the policy files AND a sidecar that verifies over the
/// exact on-disk bytes; the cache then reads fresh and the gate allows.
#[tokio::test]
#[serial]
async fn verified_envelope_persists_policy_and_sidecar() {
let home = test_home().clone();
reset(&home);
let (kp, pubkey) = install_test_key();
let url = spawn_mock(signed_team_body(
&kp,
"team-007",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(&home, &url);
write_team_auth(&home, "team-007");
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("a verified sync should succeed");
assert!(wrote);
let on_disk_managed = std::fs::read_to_string(home.join("managed_config.toml")).unwrap();
let on_disk_requirements = std::fs::read_to_string(home.join("requirements.toml")).unwrap();
let sidecar: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(home.join("managed_config.sig.json")).unwrap(),
)
.unwrap();
let payload = signed_policy::verify_signed_payload(
sidecar["signed_payload"].as_str().unwrap(),
sidecar["signature"].as_str().unwrap(),
&[("v1", &pubkey)],
)
.expect("the persisted sidecar must verify");
assert_eq!(
payload.managed_config.as_deref(),
Some(on_disk_managed.as_str()),
"the sidecar covers the exact on-disk managed_config bytes"
);
assert_eq!(
payload.requirements.as_deref(),
Some(on_disk_requirements.as_str()),
"the sidecar covers the exact on-disk requirements bytes"
);
assert!(payload.fail_closed, "the signed opt-in is carried");
assert!(
!xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"a covered cache is not hard-stale"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"an intact verified policy must not be refused"
);
}
/// Deleting the sidecar under a fail-closed marker REFUSES at the gate (stripping it
/// must not downgrade enforcement to the forgeable marker path); the refetch triggers
/// fire so an online start self-heals.
#[tokio::test]
#[serial]
async fn deleted_sidecar_under_fail_closed_marker_refuses_at_gate() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(signed_team_body(
&kp,
"team-007",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(&home, &url);
write_team_auth(&home, "team-007");
xai_grok_shell::managed_config::sync()
.await
.expect("initial sync should succeed");
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"the covered fail-closed policy is allowed"
);
std::fs::remove_file(home.join("managed_config.sig.json")).unwrap();
assert!(
xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"a stripped sidecar must trigger the session-start refetch"
);
assert!(
xai_grok_shell::config::is_managed_config_stale_for(&team_identity("team-007")),
"the TIMER staleness sibling must fire too (background tick self-heal), even though the marker is timer-fresh"
);
let gate = xai_grok_shell::managed_config::managed_policy_gate();
assert!(
gate.is_err(),
"a fail-closed policy without its sidecar must refuse offline"
);
assert!(
gate.unwrap_err()
.contains("Managed policy is required for this account"),
"the refusal is the managed-policy gate message"
);
}
/// The keyed availability fix: after a fail_closed team-A install (signed sidecar + marker), an
/// OFFLINE switch to team B previously read Compromised (the authentic sidecar is bound to A) and
/// refused a legitimate switch. The gate's identity-change purge must shed team A's artifacts
/// INCLUDING the sidecar, PERMIT team B, and leave the cache hard-stale so the next online start
/// fetches team B's own policy.
#[tokio::test]
#[serial]
async fn offline_team_switch_purges_sidecar_and_permits_new_team() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(signed_team_body(
&kp,
"team-a",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(&home, &url);
write_team_auth(&home, "team-a");
xai_grok_shell::managed_config::sync()
.await
.expect("team A keyed sync should succeed");
assert!(
home.join("managed_config.sig.json").exists(),
"the keyed sync persists a sidecar"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"team A's verified fail_closed policy must start"
);
// Switch the signed-in team to B; the gate is sync, so no fetch can rebind first.
write_team_auth(&home, "team-b");
// The bug this fixes: without the purge, team B evaluates against team A's
// foreign-bound sidecar → Compromised → a legitimate switch refused startup.
assert!(
xai_grok_shell::config::managed_policy_compromised_for(&team_identity("team-b")),
"pre-purge, the foreign-bound sidecar must read compromised for team B"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"the gate must purge team A and permit the legitimate offline switch to team B"
);
for f in [
"requirements.toml",
"managed_config.toml",
"managed_config_cache.json",
"managed_config.sig.json",
] {
assert!(
!home.join(f).exists(),
"{f} must be purged on the identity change"
);
}
assert!(
xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-b")),
"the purged cache must read hard-stale so the next online start fetches team B's policy"
);
}
/// A blank `team_id` in `auth.json` (a parse blip) over an authentic team-A-bound fail_closed
/// sidecar: the blank→None filter resolves the identity to None, the marker principal backstops
/// the signed binding (team-a vs team-a → Trusted), so the KEYED gate PERMITS — instead of
/// binding to "" and refusing as Compromised — and nothing is purged.
#[tokio::test]
#[serial]
async fn keyed_blank_team_id_is_not_refused_and_does_not_purge() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(signed_team_body(
&kp,
"team-a",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(&home, &url);
write_team_auth(&home, "team-a");
xai_grok_shell::managed_config::sync()
.await
.expect("team A keyed sync should succeed");
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"team A's verified fail_closed policy must start"
);
// auth.json now carries a team principal with a BLANK team_id.
write_team_auth(&home, "");
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"a blank team_id must read as unknown, not a foreign binding that reads compromised"
);
for f in [
"requirements.toml",
"managed_config.toml",
"managed_config_cache.json",
"managed_config.sig.json",
] {
assert!(
home.join(f).exists(),
"{f} must be retained on a blank team_id (a parse blip is not an identity change)"
);
}
}

View file

@ -1,220 +0,0 @@
//! Shared harness for the KEYED managed-config integration tests: a test-only
//! signing seam injects a throwaway trusted key so the real
//! sync → verify → persist → gate paths run with verification ACTIVE (the dark
//! behavior is covered by `team_managed_config.rs`).
//!
//! Every test MUST be `#[serial]` and install its own seam keys first: the test
//! binary shares one process-global `GROK_HOME`, process env, and key override.
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::path::PathBuf;
use std::sync::OnceLock;
use base64::Engine as _;
use xai_grok_config::signed_policy::{self, SignedPayload};
pub const MANAGED: &str = "[cli]\ntheme = \"dark\"\n";
pub const REQUIREMENTS_FAIL_CLOSED: &str = "fail_closed = true\n[features]\nweb_fetch = false\n";
/// Far-future expiry — envelopes in these tests never expire.
pub const TEST_EXPIRES_AT: u64 = 4_000_000_000;
/// The sole trusted key id: [`install_test_key`] installs it and [`sign_envelope`]
/// signs under it, so the two can't drift.
pub const TEST_KEY_ID: &str = "v1";
/// Shared temp dir used as GROK_HOME for the whole test binary (the grok_home
/// `OnceLock` only allows one value per process); scrubs the env this suite
/// depends on before any test thread reads it.
pub fn test_home() -> &'static PathBuf {
static HOME: OnceLock<PathBuf> = OnceLock::new();
HOME.get_or_init(|| {
let path = tempfile::TempDir::new().unwrap().keep();
// SAFETY: set once at init before other threads read the vars.
unsafe {
std::env::set_var("GROK_HOME", &path);
for var in [
"GROK_DEPLOYMENT_KEY",
"GROK_MANAGED_CONFIG",
"GROK_DEPLOYMENT_CONFIG_REFRESH_INTERVAL_SECS",
"GROK_DEPLOYMENT_CONFIG_CACHE_TTL_SECS",
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
] {
std::env::remove_var(var);
}
std::env::set_var("GROK_DEPLOYMENT_CONFIG_BACKOFF_MS", "10");
}
path
})
}
pub fn reset(home: &std::path::Path) {
for f in [
"config.toml",
"auth.json",
"managed_config.toml",
"requirements.toml",
"managed_config_cache.json",
"managed_config.lock",
"managed_config.sig.json",
] {
let _ = std::fs::remove_file(home.join(f));
}
}
/// Minimal mock deployment-config server serving `body` to every request.
pub fn spawn_mock(body: String) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
// Drain the request headers before responding.
let mut reader = BufReader::new(&mut stream);
loop {
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 || line.trim_end().is_empty() {
break;
}
}
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(resp.as_bytes());
let _ = stream.flush();
}
});
format!("http://{addr}/deployment/config")
}
pub fn write_config(home: &std::path::Path, managed_config_url: &str) {
std::fs::write(
home.join("config.toml"),
format!("[endpoints]\nmanaged_config_url = \"{managed_config_url}\"\n"),
)
.unwrap();
}
/// [`write_config`] plus a `deployment_key` (dead-code-allowed: compiled into
/// both binaries, called by one).
#[allow(dead_code)]
pub fn write_dk_config(home: &std::path::Path, managed_config_url: &str, deployment_key: &str) {
std::fs::write(
home.join("config.toml"),
format!(
"[endpoints]\nmanaged_config_url = \"{managed_config_url}\"\ndeployment_key = \"{deployment_key}\"\n"
),
)
.unwrap();
}
pub fn write_team_auth(home: &std::path::Path, team_id: &str) {
let scope = xai_grok_shell::auth::GrokComConfig::default().auth_scope();
let auth = serde_json::json!({
scope: {
"key": "team-session-token",
"auth_mode": "oidc",
"create_time": "2026-01-01T00:00:00Z",
"expires_at": "2099-01-01T00:00:00Z",
"user_id": "user-1",
"principal_type": "Team",
"team_id": team_id,
}
});
std::fs::write(home.join("auth.json"), auth.to_string()).unwrap();
}
/// A fresh Ed25519 keypair plus its raw public key, installed as the sole trusted
/// key ([`TEST_KEY_ID`]) via the test seam.
pub fn install_test_key() -> (ring::signature::Ed25519KeyPair, Vec<u8>) {
use ring::signature::KeyPair as _;
let rng = ring::rand::SystemRandom::new();
let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let kp = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let pubkey = kp.public_key().as_ref().to_vec();
signed_policy::test_seam::set_embedded_keys(&[(TEST_KEY_ID, &pubkey)]);
assert!(
signed_policy::verification_active(),
"the seam must arm verification"
);
(kp, pubkey)
}
/// Serialize → sign → base64: the one `signatures[]` entry for `payload`, signed
/// by `kp` under the payload's own `key_id` (the untrusted outer hint can't drift
/// from the signed one).
pub fn sign_envelope(
kp: &ring::signature::Ed25519KeyPair,
payload: &SignedPayload,
) -> serde_json::Value {
let signed_payload = serde_json::to_string(payload).unwrap();
let signature = base64::engine::general_purpose::STANDARD
.encode(kp.sign(signed_payload.as_bytes()).as_ref());
serde_json::json!({
"signed_payload": signed_payload,
"signature": signature,
"key_id": payload.key_id.as_str(),
})
}
/// A team deployment-config response signed by `kp` under [`TEST_KEY_ID`]. The
/// body's legacy fields mirror the payload exactly (the client rejects a divergence).
pub fn signed_team_body(
kp: &ring::signature::Ed25519KeyPair,
team_id: &str,
managed: Option<&str>,
requirements: Option<&str>,
) -> String {
let payload = SignedPayload {
version: prod_mc_cli_chat_proxy_types::SIGNED_PAYLOAD_VERSION,
deployment_id: None,
team_id: Some(team_id.to_owned()),
managed_config: managed.map(str::to_owned),
requirements: requirements.map(str::to_owned),
fail_closed: requirements.is_some_and(xai_grok_config::fail_closed_flag_from_str),
expires_at: TEST_EXPIRES_AT,
key_id: TEST_KEY_ID.into(),
};
serde_json::json!({
"deployment_id": serde_json::Value::Null,
"team_id": team_id,
"managed_config": managed,
"requirements": requirements,
"signatures": [sign_envelope(kp, &payload)],
})
.to_string()
}
/// A [`signed_team_body`] (managed config only) with the signature corrupted —
/// valid base64, wrong bytes — so the verifier must reject the envelope.
pub fn forged_team_body(kp: &ring::signature::Ed25519KeyPair, team_id: &str) -> String {
let mut body: serde_json::Value =
serde_json::from_str(&signed_team_body(kp, team_id, Some(MANAGED), None)).unwrap();
body["signatures"][0]["signature"] = base64::engine::general_purpose::STANDARD
.encode([0u8; 64])
.into();
body.to_string()
}
pub fn team_identity(id: &str) -> xai_grok_shell::config::ServingIdentity {
xai_grok_shell::config::ServingIdentity::Team(id.to_owned())
}
/// True when `path` reads despite `chmod 000` (root / DAC bypass): chmod-based
/// tests must then skip LOUDLY — a silent return would pass forever. CI runners
/// are assumed unprivileged; the shared guard keeps skips greppable.
#[cfg(unix)]
#[allow(dead_code)]
pub fn skip_as_root(path: &std::path::Path, test: &str) -> bool {
let skip = std::fs::read_to_string(path).is_ok();
if skip {
eprintln!("{test}: skipping — chmod unreadability not enforced (running as root?)");
}
skip
}

View file

@ -1,296 +0,0 @@
//! Extended KEYED managed-config scenarios. Harness + seam/serial constraints:
//! `signed_managed_config/common.rs`.
//!
//! Placement rule: new keyed scenarios land HERE; `signed_managed_config.rs`
//! stays fixed to the review-cited security claims (verify-persists /
//! reject-persists-nothing / sidecar-deletion-refuses).
#[path = "signed_managed_config/common.rs"]
mod common;
#[cfg(unix)]
use common::skip_as_root;
use common::{
MANAGED, REQUIREMENTS_FAIL_CLOSED, TEST_EXPIRES_AT, TEST_KEY_ID, forged_team_body,
install_test_key, reset, sign_envelope, signed_team_body, spawn_mock, team_identity, test_home,
write_config, write_dk_config, write_team_auth,
};
use serial_test::serial;
use xai_grok_config::signed_policy::{self, SignedPayload};
/// The healthy fail-closed starting state the tamper/heal scenarios mutate;
/// the mock keeps serving the same body, so a healing sync can refetch it.
async fn sync_fail_closed_policy(home: &std::path::Path, kp: &ring::signature::Ed25519KeyPair) {
let url = spawn_mock(signed_team_body(
kp,
"team-007",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(home, &url);
write_team_auth(home, "team-007");
xai_grok_shell::managed_config::sync()
.await
.expect("initial sync should succeed");
assert!(xai_grok_shell::managed_config::managed_policy_gate().is_ok());
}
/// The signed-empty deployment response: a `{}` body (no legacy fields) whose
/// envelope binds ABSENCE to `deployment_id` — what the server serves for a
/// provisioned key with no config row.
fn signed_dk_empty_body(kp: &ring::signature::Ed25519KeyPair, deployment_id: &str) -> String {
let payload = SignedPayload {
version: prod_mc_cli_chat_proxy_types::SIGNED_PAYLOAD_VERSION,
deployment_id: Some(deployment_id.to_owned()),
team_id: None,
managed_config: None,
requirements: None,
fail_closed: false,
expires_at: TEST_EXPIRES_AT,
key_id: TEST_KEY_ID.into(),
};
serde_json::json!({ "signatures": [sign_envelope(kp, &payload)] }).to_string()
}
/// The marker principal for an applied signed-EMPTY dk response comes from the
/// VERIFIED payload's deployment_id (the `{}` body carries none), so the gate's
/// cross-tenant binding holds even on an unprovisioned dk machine.
#[tokio::test]
#[serial]
async fn empty_dk_response_marker_binds_the_verified_deployment_id() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(signed_dk_empty_body(&kp, "dep-42"));
write_dk_config(&home, &url, "dep-key-1");
// No team auth: the empty dk body is applied (converges), not fallen through.
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("signed-empty dk sync should succeed");
assert!(!wrote, "nothing to write for an empty row");
let marker = std::fs::read_to_string(home.join("managed_config_cache.json")).unwrap();
let v: serde_json::Value = serde_json::from_str(&marker).unwrap();
assert_eq!(
v["principal"].as_str(),
Some("dep-42"),
"the marker must bind the VERIFIED deployment id: {marker}"
);
assert!(
home.join("managed_config.sig.json").exists(),
"the absence envelope is persisted"
);
assert!(xai_grok_shell::managed_config::managed_policy_gate().is_ok());
}
/// A signature-rejected sync surfaces as failure in BOTH `grok setup` and the
/// post-login sync — never as Installed/NoChange while nothing was persisted.
#[tokio::test]
#[serial]
async fn rejected_signature_surfaces_as_setup_and_login_failure() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(forged_team_body(&kp, "team-007"));
write_config(&home, &url);
write_team_auth(&home, "team-007");
let outcome = xai_grok_shell::managed_config::run_setup().await;
assert!(
matches!(
outcome,
xai_grok_shell::managed_config::SetupOutcome::Failed(
xai_grok_shell::managed_config::ManagedConfigError::SignatureRejected
)
),
"setup must surface the signature rejection, got {outcome:?}"
);
let login = xai_grok_shell::managed_config::post_login_sync(None).await;
assert_eq!(
login,
xai_grok_shell::managed_config::ManagedConfigSync::Failed,
"post-login sync must report Failed, not NoChange"
);
}
/// A response that stops serving requirements deletes the on-disk file, and the
/// NEW sidecar (written after the deletion) covers the absence — the converged cache
/// reads fresh and the gate allows.
#[tokio::test]
#[serial]
async fn withdrawn_requirements_is_deleted_and_covered_by_the_new_sidecar() {
let home = test_home().clone();
reset(&home);
let (kp, pubkey) = install_test_key();
sync_fail_closed_policy(&home, &kp).await;
assert!(home.join("requirements.toml").exists());
let url_partial = spawn_mock(signed_team_body(&kp, "team-007", Some(MANAGED), None));
write_config(&home, &url_partial);
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("withdrawing sync should succeed");
assert!(wrote, "the deletion is a change");
assert!(
!home.join("requirements.toml").exists(),
"the withdrawn artifact is removed"
);
let sidecar: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(home.join("managed_config.sig.json")).unwrap(),
)
.unwrap();
let payload = signed_policy::verify_signed_payload(
sidecar["signed_payload"].as_str().unwrap(),
sidecar["signature"].as_str().unwrap(),
&[(TEST_KEY_ID, &pubkey)],
)
.expect("the refreshed sidecar must verify");
assert!(
payload.requirements.is_none(),
"the new sidecar covers the absence"
);
assert!(
!xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"the converged, covered cache is not hard-stale"
);
assert!(xai_grok_shell::managed_config::managed_policy_gate().is_ok());
}
/// A directory squatting at a signed artifact path reads COMPROMISED at the gate
/// (not lenient-unreadable), and an online sync converges over it — clearing the
/// directory, rewriting the file, and restoring enforcement.
#[tokio::test]
#[serial]
async fn directory_squat_reads_compromised_and_online_sync_heals() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
sync_fail_closed_policy(&home, &kp).await;
// Dir-squat the enforced artifact (with a child, like a real squat).
std::fs::remove_file(home.join("requirements.toml")).unwrap();
std::fs::create_dir(home.join("requirements.toml")).unwrap();
std::fs::write(home.join("requirements.toml").join("junk"), "x").unwrap();
let gate = xai_grok_shell::managed_config::managed_policy_gate();
assert!(
gate.is_err(),
"a directory squat on a fail-closed policy must refuse offline"
);
// The gate verdict, not an incidental error; classification is unit-pinned
// in signed_policy::directory_squat_is_tamper_not_unreadable.
assert!(
gate.unwrap_err()
.contains("Managed policy is required for this account"),
"the refusal is the managed-policy gate message"
);
assert!(
xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"the squat must trigger the refetch"
);
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("healing sync should succeed");
assert!(wrote, "the healing sync must rewrite the squatted artifact");
assert_eq!(
std::fs::read_to_string(home.join("requirements.toml")).unwrap(),
REQUIREMENTS_FAIL_CLOSED,
"the served file replaces the squatting directory"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"enforcement is restored after the heal"
);
}
/// A sidecar read blip (chmod 000) is not tamper: the gate allows while the
/// refetch trigger fires — mirroring the artifact-slot blip semantics.
#[cfg(unix)]
#[tokio::test]
#[serial]
async fn sidecar_read_blip_allows_session_and_triggers_refetch() {
use std::os::unix::fs::PermissionsExt;
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
sync_fail_closed_policy(&home, &kp).await;
let sidecar_path = home.join("managed_config.sig.json");
std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o000)).unwrap();
if skip_as_root(
&sidecar_path,
"sidecar_read_blip_allows_session_and_triggers_refetch",
) {
let _ = std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o600));
return;
}
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"a transient sidecar read blip must not refuse the session"
);
assert!(
xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"the blip must trigger the refetch so the self-heal runs"
);
// Restore so the tempdir (and later tests) stay clean.
std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
/// A directory squatting at the SIDECAR path refuses at the gate, and the online
/// sync clears it — a bare rename would error forever.
#[tokio::test]
#[serial]
async fn sidecar_directory_squat_refuses_then_online_sync_heals() {
let home = test_home().clone();
reset(&home);
let (kp, _pubkey) = install_test_key();
sync_fail_closed_policy(&home, &kp).await;
// Dir-squat the sidecar (with a child, like a real squat).
let sidecar_path = home.join("managed_config.sig.json");
std::fs::remove_file(&sidecar_path).unwrap();
std::fs::create_dir(&sidecar_path).unwrap();
std::fs::write(sidecar_path.join("junk"), "x").unwrap();
let gate = xai_grok_shell::managed_config::managed_policy_gate();
assert!(
gate.is_err(),
"an unreadable (squatted) sidecar under a fail-closed marker must refuse offline"
);
// The gate verdict, not an incidental error; classification is unit-pinned
// in signed_policy::sidecar_directory_squat_is_absence_not_a_blip.
assert!(
gate.unwrap_err()
.contains("Managed policy is required for this account"),
"the refusal is the managed-policy gate message"
);
assert!(
xai_grok_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
"the squat must trigger the refetch"
);
xai_grok_shell::managed_config::sync()
.await
.expect("healing sync should succeed");
assert!(
sidecar_path.is_file(),
"the rewrite must replace the squatting directory with a sidecar FILE"
);
// Under a fail-closed marker the gate requires an authentic sidecar, so
// allowing here also pins that the healed sidecar verifies.
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"enforcement is restored after the heal"
);
}

View file

@ -60,6 +60,7 @@ fn reset(home: &std::path::Path) {
"auth.json",
"managed_config.toml",
"requirements.toml",
"managed_config.sig.json",
"managed_config_cache.json",
"managed_config.lock",
] {
@ -97,6 +98,35 @@ fn spawn_mock(body: String) -> (String, Arc<Mutex<Vec<String>>>) {
(url, auths)
}
/// Like [`spawn_mock_seq`] but sleeps `delay` before each response (for mid-fetch races).
fn spawn_mock_delayed(body: String, delay: std::time::Duration) -> MockHandle {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
let counter = count.clone();
let auths: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let seen_auths = auths.clone();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
if let Some(auth) = read_request_auth(&mut stream) {
seen_auths.lock().unwrap().push(auth);
}
{
*counter.lock().unwrap() += 1;
}
std::thread::sleep(delay);
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes());
}
});
(format!("http://{addr}/v1/deployment-config"), count, auths)
}
/// `(url, request_count, authorization_headers_in_order)`.
type MockHandle = (String, Arc<Mutex<usize>>, Arc<Mutex<Vec<String>>>);
@ -332,6 +362,81 @@ async fn team_sync_writes_files() {
);
}
/// A directory squatting at the MARKER path must not permanently disarm the staleness
/// detector: the atomic marker write would fail onto it on every sync, forever. The
/// locked apply clears the squat (same rule as the sidecar) and records the sync.
#[tokio::test]
#[serial]
async fn marker_dir_squat_is_cleared_and_marker_written() {
let home = test_home().clone();
reset(&home);
let (url, _auths) = spawn_mock(team_config_body());
write_config(&home, &url);
write_team_auth(&home, "team-007");
// Dir-squat the marker (with a child, like a real squat).
let marker_path = home.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE);
std::fs::create_dir(&marker_path).unwrap();
std::fs::write(marker_path.join("junk"), "x").unwrap();
let wrote = xai_grok_shell::managed_config::sync()
.await
.expect("sync should succeed");
assert!(
wrote,
"the policy files are written despite the marker squat"
);
assert!(
marker_path.is_file(),
"the apply must replace the squatting directory with the marker FILE"
);
let marker = std::fs::read_to_string(&marker_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&marker).unwrap();
assert_eq!(
v["principal"].as_str(),
Some("team-007"),
"the recorded marker must describe this sync: {marker}"
);
}
/// A whitespace-padded `team_id` in `auth.json` is one identity end-to-end: the serving
/// identity and the recorded marker are trimmed, and re-syncing with the padded id is the
/// same tenant (no eviction, no confirmed switch).
#[tokio::test]
#[serial]
async fn padded_team_id_is_one_identity() {
let home = test_home().clone();
reset(&home);
let (url, _auths) = spawn_mock(team_config_body());
write_config(&home, &url);
write_team_auth(&home, " team-007 ");
assert_eq!(
xai_grok_shell::managed_config::current_serving_identity(),
team_identity("team-007"),
"the serving identity must be the trimmed team id"
);
xai_grok_shell::managed_config::sync()
.await
.expect("sync should succeed");
let marker =
std::fs::read_to_string(home.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)).unwrap();
let v: serde_json::Value = serde_json::from_str(&marker).unwrap();
assert_eq!(
v["principal"].as_str(),
Some("team-007"),
"the marker stores the trimmed identity: {marker}"
);
assert_eq!(
xai_grok_config::confirmed_team_switch("team-007"),
None,
"padding is not a tenant switch"
);
}
/// Switching the active team must not keep enforcing the prior team's policy: after B syncs,
/// A's artifacts are evicted and the marker records B served nothing. Fail-open.
#[tokio::test]
@ -340,7 +445,9 @@ async fn team_switch_evicts_prior_teams_policy() {
let home = test_home().clone();
reset(&home);
// Team A serves both managed_config and requirements.
// Team A serves both managed_config and requirements. A leftover sidecar (from an
// earlier signing build; verification is inactive here) must also be evicted, or a
// later signing build would read A's foreign-bound sidecar against B's identity.
let (url_a, _auths_a) = spawn_mock(team_config_body());
write_config(&home, &url_a);
write_team_auth(&home, "team-a");
@ -349,6 +456,7 @@ async fn team_switch_evicts_prior_teams_policy() {
.expect("team A sync should succeed");
assert!(home.join("requirements.toml").exists());
assert!(home.join("managed_config.toml").exists());
std::fs::write(home.join("managed_config.sig.json"), "{}").unwrap();
// Switch to team B, whose server returns a row (team_id) but no artifacts.
let body_b = serde_json::json!({
@ -376,6 +484,10 @@ async fn team_switch_evicts_prior_teams_policy() {
!home.join("managed_config.toml").exists(),
"team A's managed_config must be evicted on the switch to team B"
);
assert!(
!home.join("managed_config.sig.json").exists(),
"team A's stale sidecar must be evicted on the switch to team B"
);
// The marker is now team B's and must not claim B served A's artifacts.
let marker = std::fs::read_to_string(home.join("managed_config_cache.json")).unwrap();
@ -974,18 +1086,15 @@ async fn identity_change_permits_offline_team_switch_and_purges_prior_team() {
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"a legitimate offline team switch must not fail closed"
);
assert!(
!home.join("requirements.toml").exists(),
"team A's enforced requirements must be purged on the switch"
);
assert!(
!home.join("managed_config.toml").exists(),
"team A's managed_config must be purged on the switch"
);
assert!(
!home.join("managed_config_cache.json").exists(),
"team A's sync marker must be purged on the switch"
);
for f in xai_grok_shell::managed_config::MANAGED_ARTIFACT_FILES
.into_iter()
.chain([xai_grok_config::MANAGED_CONFIG_CACHE_FILE])
{
assert!(
!home.join(f).exists(),
"team A's {f} must be purged on the switch"
);
}
}
/// The gate purge takes the managed-config lock best-effort and SKIPS on contention (the holder
@ -1055,6 +1164,66 @@ async fn gate_purge_skips_while_lock_contended() {
);
}
/// A TRANSIENT lock holder must not turn an offline team switch into a skipped purge:
/// the purge retries the lock once after 100ms (`PURGE_LOCK_RETRY_DELAY`), so a holder
/// that releases within that window (~20ms here) is absorbed and the SAME gate call
/// purges team A on the second attempt.
#[tokio::test]
#[serial]
async fn gate_purge_retries_past_a_transient_lock_holder() {
let home = test_home().clone();
reset(&home);
let body = serde_json::json!({
"deployment_id": serde_json::Value::Null,
"team_id": "team-a",
"managed_config": TEAM_MANAGED,
"requirements": format!("fail_closed = true\n{TEAM_REQUIREMENTS}"),
})
.to_string();
let (url, _auths) = spawn_mock(body);
write_config(&home, &url);
write_team_auth(&home, "team-a");
xai_grok_shell::managed_config::sync()
.await
.expect("team A sync should succeed");
assert!(home.join("requirements.toml").exists());
write_team_auth(&home, "team-b");
// Acquire the flock BEFORE the gate call, then hand it to a helper that releases
// it ~20ms in — inside the purge's retry window.
let lock = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(home.join("managed_config.lock"))
.unwrap();
lock.lock().unwrap();
let holder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(20));
drop(lock); // releases the flock
});
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"a pure identity mismatch never refuses, purged or not"
);
holder.join().unwrap();
assert!(
!home.join("requirements.toml").exists(),
"one gate call must absorb the transient holder via the retry and purge team A"
);
assert!(
!home
.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"team A's marker goes with the retried purge"
);
}
/// A blank `team_id` in `auth.json` (a parse blip / malformed write) is "unknown", not a
/// distinct identity: the gate must NOT fail closed and the purge must NOT shed team A's
/// policy. Guards the blank→None map in `active_team_id_any_expiry` and the detector's
@ -1095,6 +1264,13 @@ async fn blank_team_id_neither_fails_closed_nor_purges() {
home.join("managed_config_cache.json").exists(),
"the team A marker must be retained on a blank team_id"
);
assert!(
matches!(
xai_grok_shell::managed_config::current_serving_identity(),
ServingIdentity::None
),
"a blank team_id must resolve to no identity, not Team(\"\") (spurious refetch input)"
);
}
/// The session-start gate reads no env: `GROK_MANAGED_CONFIG_FAIL_CLOSED=0` must NOT disarm a fail_closed
@ -1394,8 +1570,9 @@ async fn lock_contention_does_not_fall_through_to_team() {
assert_eq!(*count.lock().unwrap(), 1);
}
/// `grok setup` with config served but the lock held by another writer reports
/// Installed (the holder is persisting it), not NothingConfigured.
/// `grok setup` with config served but the lock held by another writer reports the
/// skip: not Installed (THIS run persisted nothing) and not NothingConfigured (the
/// server does have config).
#[tokio::test]
#[serial]
async fn setup_lock_skip_is_not_reported_as_no_config() {
@ -1421,9 +1598,10 @@ async fn setup_lock_skip_is_not_reported_as_no_config() {
assert!(
matches!(
outcome,
xai_grok_shell::managed_config::SetupOutcome::Installed
xai_grok_shell::managed_config::SetupOutcome::Skipped
),
"served config with the lock held must not report NothingConfigured"
"a lock skip persisted nothing: it must report Skipped, not Installed or \
NothingConfigured, got {outcome:?}"
);
}
@ -1848,3 +2026,225 @@ async fn deploy_key_machine_never_gate_purges_on_team_switch() {
"the sync marker must survive too — the key, not the team, owns this machine's policy"
);
}
/// For every on-disk state a crashed purge can leave (each proper prefix of the removal
/// order), the marker is still present, the detector still fires for the new team, and a
/// later purge converges. The order itself is pinned by `marker_is_not_a_managed_artifact`
/// plus the fault-injection unit test.
#[tokio::test]
#[serial]
async fn purge_crash_prefixes_stay_armed_and_converge() {
let home = test_home().clone();
let artifacts = xai_grok_shell::managed_config::MANAGED_ARTIFACT_FILES;
// 0..=len: every proper prefix of the 4-step removal order, up to and including
// "all artifacts removed, marker still present" (a crash right before the marker step).
for prefix_len in 0..=artifacts.len() {
reset(&home);
let body = serde_json::json!({
"deployment_id": serde_json::Value::Null,
"team_id": "team-a",
"managed_config": TEAM_MANAGED,
"requirements": format!("fail_closed = true\n{TEAM_REQUIREMENTS}"),
})
.to_string();
let (url, _auths) = spawn_mock(body);
write_config(&home, &url);
write_team_auth(&home, "team-a");
xai_grok_shell::managed_config::sync()
.await
.expect("team-a sync should succeed");
// Simulate a purge crashed after removing only this prefix.
for name in &artifacts[..prefix_len] {
let _ = std::fs::remove_file(home.join(name));
}
assert!(
home.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"marker must outlive every artifact prefix (prefix_len={prefix_len})"
);
// Team B arrives offline: the detector must still confirm and the purge converge.
write_team_auth(&home, "team-b");
assert_eq!(
xai_grok_config::confirmed_team_switch("team-b").as_deref(),
Some("team-a"),
"detector must stay armed after a crash prefix (prefix_len={prefix_len})"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"offline switch over a crash prefix must not refuse (prefix_len={prefix_len})"
);
for name in artifacts {
assert!(
!home.join(name).exists(),
"{name} must be purged (prefix_len={prefix_len})"
);
}
assert!(
!home
.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"the converged purge drops the marker last (prefix_len={prefix_len})"
);
}
}
/// Marker written under the apply lock by the holder only: lock-contended apply records nothing.
#[tokio::test]
#[serial]
async fn contended_sync_writes_no_marker() {
let home = test_home().clone();
reset(&home);
let body = serde_json::json!({
"deployment_id": serde_json::Value::Null,
"team_id": "team-a",
"managed_config": TEAM_MANAGED,
"requirements": TEAM_REQUIREMENTS,
})
.to_string();
let (url, auths) = spawn_mock(body);
write_config(&home, &url);
write_team_auth(&home, "team-a");
// Hold the managed-config flock across the sync: apply skips, so nothing is persisted.
let lock = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(home.join("managed_config.lock"))
.unwrap();
lock.lock().unwrap();
let synced = xai_grok_shell::managed_config::sync()
.await
.expect("sync should succeed (skip, not error)");
assert!(!synced, "a lock-contended apply must not report a write");
lock.unlock().unwrap();
// Positive control: the FETCH happened (only the apply was skipped), so the
// no-marker assertions below can't pass vacuously on a sync that never ran.
assert!(
!auths.lock().unwrap().is_empty(),
"the fetch must have reached the server"
);
assert!(
!home
.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"a contended sync must not write a marker for files it never persisted"
);
assert!(!home.join("requirements.toml").exists());
}
/// Credential vanished mid-fetch → apply Skipped, no marker (sibling of contention skip).
#[tokio::test]
#[serial]
async fn credential_gone_mid_fetch_writes_no_marker() {
let home = test_home().clone();
reset(&home);
let body = serde_json::json!({
"deployment_id": "dep-1",
"managed_config": TEAM_MANAGED,
"requirements": TEAM_REQUIREMENTS,
})
.to_string();
// Delay the response so we can clear the deployment key after the fetch starts
// but before apply runs.
let (url, count, auths) = spawn_mock_delayed(body, std::time::Duration::from_millis(200));
std::fs::write(
home.join("config.toml"),
format!(
"[endpoints]\nmanaged_config_url = \"{url}\"\ndeployment_key = \"KEY-GOING-AWAY\"\n"
),
)
.unwrap();
let home_for_clear = home.clone();
let clearer = std::thread::spawn(move || {
// Wait until the mock has accepted a request, then drop the key.
for _ in 0..50 {
if *count.lock().unwrap() > 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
std::fs::write(
home_for_clear.join("config.toml"),
format!("[endpoints]\nmanaged_config_url = \"{url}\"\n"),
)
.unwrap();
});
let synced = xai_grok_shell::managed_config::sync()
.await
.expect("sync should succeed (skip, not error)");
clearer.join().unwrap();
assert!(!synced, "credential-gone apply must not report a write");
assert!(
!auths.lock().unwrap().is_empty(),
"the fetch must have reached the server"
);
assert!(
!home
.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"credential-gone must not write a marker for an unapplied body"
);
assert!(!home.join("requirements.toml").exists());
}
/// A dk-synced marker means the KEY owns this machine's policy: with the key line gone
/// from config.toml (the shape of a transient read failure) and a team user signed in,
/// the gate must NOT purge. Pins the marker-scoped exemption — one keyed on live config
/// resolution would purge here.
#[tokio::test]
#[serial]
async fn dk_synced_marker_survives_config_blip_with_team_signed_in() {
let home = test_home().clone();
reset(&home);
let body = serde_json::json!({
"deployment_id": "deploy-A",
"managed_config": TEAM_MANAGED,
"requirements": format!("fail_closed = true\n{TEAM_REQUIREMENTS}"),
})
.to_string();
let (url, _auths) = spawn_mock(body);
std::fs::write(
home.join("config.toml"),
format!("[endpoints]\nmanaged_config_url = \"{url}\"\ndeployment_key = \"KEY-AAA\"\n"),
)
.unwrap();
xai_grok_shell::managed_config::sync()
.await
.expect("deploy-key sync should succeed");
assert!(home.join("requirements.toml").exists());
// The blip: the key line is gone (same shape as a transient config read failure),
// while a team user is also signed in. Identity resolves Team("team-b"), which
// differs from the marker principal ("deploy-A") — but the marker is key-scoped.
write_config(&home, &url);
write_team_auth(&home, "team-b");
assert_eq!(
xai_grok_config::confirmed_team_switch("team-b"),
None,
"a key-scoped marker must never confirm a team switch"
);
assert!(
xai_grok_shell::managed_config::managed_policy_gate().is_ok(),
"the blip must not refuse: the key-scoped marker still matches the on-disk policy"
);
assert!(
home.join("requirements.toml").exists(),
"the machine's enforced policy must survive the blip"
);
assert!(
home.join(xai_grok_config::MANAGED_CONFIG_CACHE_FILE)
.exists(),
"the dk marker must survive the blip"
);
}

View file

@ -1448,7 +1448,7 @@ fn process_dead_within(pid: u32, deadline: Duration) -> bool {
#[cfg(unix)]
fn read_task_pid(pid_file: &std::path::Path) -> u32 {
let start = std::time::Instant::now();
while !pid_file.exists() && start.elapsed() < Duration::from_secs(2) {
while !pid_file.exists() && start.elapsed() < Duration::from_secs(10) {
std::thread::sleep(Duration::from_millis(100));
}
let contents = std::fs::read_to_string(pid_file).unwrap_or_else(|e| {
@ -1468,7 +1468,10 @@ fn read_task_pid(pid_file: &std::path::Path) -> u32 {
/// answer for the post-tool turn.
#[cfg(unix)]
fn enqueue_background_task_turn(server: &MockInferenceServer, pid_file: &std::path::Path) {
let command = format!("echo $$ > {} && exec /bin/sleep 300", pid_file.display());
let command = format!(
"echo $$ > {0} && /bin/sync {0} 2>/dev/null; exec /bin/sleep 300",
pid_file.display()
);
let args = serde_json::json!({
"command": command,
"description": "start long-lived background process",