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:
parent
c68e39f604
commit
8adf9013a0
117 changed files with 16998 additions and 14540 deletions
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue