Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ default = ["telemetry"]
## `--no-default-features` (plus any other features you need) for a build that
## contains no telemetry endpoint, no HTTP client, and no emission code at all.
telemetry = ["dep:reqwest", "dep:chrono"]
## Expose proposals::test_helpers (`ProposalsFlagGuard`) to downstream test
## code in other crates. Enabled by openshell-sandbox and
## openshell-supervisor-network dev builds.
test-helpers = []

[build-dependencies]
tonic-prost-build = { workspace = true }
Expand Down
93 changes: 30 additions & 63 deletions crates/openshell-core/src/proposals.rs
Original file line number Diff line number Diff line change
@@ -1,83 +1,50 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Process-wide flag controlling agent-driven policy proposals.
//! Shared state controlling agent-driven policy proposals.
//!
//! Initialised once during sandbox start from the `agent_policy_proposals_enabled`
//! setting and updated by the policy poll loop when the setting changes. Read
//! by the `policy.local` route handler and by the skills installer to gate the
//! agent-controlled mutation surface. Tests use [`test_helpers::ProposalsFlagGuard`]
//! to flip the flag through a serialized guard.
//! setting and updated by the policy poll loop or authoritative sidecar control
//! when the setting changes. Read by the `policy.local` route handler and by
//! the skills installer to gate the agent-controlled mutation surface.

use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};

/// Process-wide handle to the agent-proposals flag.
/// Shared handle for the agent-driven policy proposal surface.
///
/// Set once by `run_sandbox()` during start; subsequent attempts to set it are
/// ignored. The contained `AtomicBool` is updated by the policy poll loop.
pub static AGENT_PROPOSALS_ENABLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();

/// Read the current value of the agent proposals feature flag.
///
/// Returns `false` if the flag has not been initialized (e.g. during unit
/// tests), matching the documented default for the setting.
pub fn agent_proposals_enabled() -> bool {
AGENT_PROPOSALS_ENABLED
.get()
.is_some_and(|flag| flag.load(Ordering::Relaxed))
/// Clones point at the same atomic value, so the sandbox orchestrator can pass
/// this into the process and network supervisors and then update it from the
/// settings poll loop or sidecar control.
#[derive(Clone, Debug)]
pub struct AgentProposals {
enabled: Arc<AtomicBool>,
}

/// Test-only helpers shared across crates' test modules.
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers {
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::MutexGuard;

static PROPOSALS_FLAG_LOCK: LazyLock<tokio::sync::Mutex<()>> =
LazyLock::new(|| tokio::sync::Mutex::new(()));

/// Guard for tests that toggle the process-wide flag.
///
/// Acquires a process-wide async mutex, swaps in the requested value, and
/// restores the previous value on drop. Hold the guard for the duration of
/// any code that reads `agent_proposals_enabled()`.
pub struct ProposalsFlagGuard {
prev: bool,
flag: Arc<AtomicBool>,
_lock: MutexGuard<'static, ()>,
impl AgentProposals {
#[must_use]
pub fn new(enabled: bool) -> Self {
Self {
enabled: Arc::new(AtomicBool::new(enabled)),
}
}

impl ProposalsFlagGuard {
pub async fn set(enabled: bool) -> Self {
let lock = PROPOSALS_FLAG_LOCK.lock().await;
Self::with_lock(enabled, lock)
}
#[must_use]
pub fn enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
}

pub fn set_blocking(enabled: bool) -> Self {
let lock = PROPOSALS_FLAG_LOCK.blocking_lock();
Self::with_lock(enabled, lock)
}
pub fn set_enabled(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::Relaxed);
}

fn with_lock(enabled: bool, lock: MutexGuard<'static, ()>) -> Self {
let flag = super::AGENT_PROPOSALS_ENABLED
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.clone();
let prev = flag.swap(enabled, Ordering::Relaxed);
Self {
prev,
flag,
_lock: lock,
}
}
pub fn swap_enabled(&self, enabled: bool) -> bool {
self.enabled.swap(enabled, Ordering::Relaxed)
}
}

impl Drop for ProposalsFlagGuard {
fn drop(&mut self) {
self.flag.store(self.prev, Ordering::Relaxed);
}
impl Default for AgentProposals {
fn default() -> Self {
Self::new(false)
}
}
1 change: 0 additions & 1 deletion crates/openshell-sandbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ default = ["telemetry"]
telemetry = ["openshell-core/telemetry"]

[dev-dependencies]
openshell-core = { path = "../openshell-core", features = ["test-helpers"] }
tempfile = "3"
temp-env = "0.3"
tokio-tungstenite = { workspace = true }
Expand Down
Loading
Loading