mirror of
https://github.com/hanzoai/codex.git
synced 2026-10-30 14:59:26 +00:00
## Why Local message boards were scoped to an agent tree. Independently created Codex runtimes need a way to share a board while receiving different access permissions. ## What changed - Add `LocalBoard` and `LocalBoardConnection` so hosts can supply read-only or read-write access through `thread_extension_init`, including for isolated, ephemeral runtimes. - Allow read-only connections to read and manage their own subscriptions; reject posting, replying, channel creation, and changes to another agent's subscriptions. Expose effective permissions in channel discovery. - Use unique runtime paths and require matching board IDs and canonical database paths when resolving agents or delivering notifications. - Document explicit extension installation and connection setup for new runtimes, cold resumes, and forks. ## Testing Add a scenario and context snapshot covering an independent writer and isolated Guardian sharing a board through a symlink alias, read-only permission denials, subscription and explicit notifications, and rejection of notifications to an unconnected runtime before a post is written. GitOrigin-RevId: f92410c3a5c6a46920c3e0e71849c85dc83d9c31
85 lines
2.8 KiB
Rust
85 lines
2.8 KiB
Rust
//! Backend-computed permissions and denials exposed by the board tools.
|
|
|
|
use codex_protocol::error::CodexErr;
|
|
use codex_tools::FunctionCallError;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
use std::fmt;
|
|
|
|
/// Effective board actions for the authenticated caller, not an ACL definition.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct BoardPermissions {
|
|
pub create_channel: bool,
|
|
pub edit_metadata: bool,
|
|
pub manage_permissions: bool,
|
|
}
|
|
|
|
/// Effective channel actions; replying does not imply permission to start a thread.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ChannelPermissions {
|
|
pub read: bool,
|
|
pub post: bool,
|
|
pub reply: bool,
|
|
pub edit_metadata: bool,
|
|
}
|
|
|
|
/// A backend denial retained through the board's existing error interface.
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(tag = "code", rename = "permission_denied")]
|
|
pub struct PermissionDenied {
|
|
pub message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub action: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub resource: Option<String>,
|
|
}
|
|
|
|
impl fmt::Display for PermissionDenied {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "permission_denied: {}", self.message)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for PermissionDenied {}
|
|
|
|
impl From<PermissionDenied> for CodexErr {
|
|
fn from(error: PermissionDenied) -> Self {
|
|
Self::Io(std::io::Error::new(
|
|
std::io::ErrorKind::PermissionDenied,
|
|
error,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl PermissionDenied {
|
|
pub(crate) fn model_response(&self, budget: usize) -> Result<Value, FunctionCallError> {
|
|
// Keep the denial structured and below 1k tokens, even for oversized service text.
|
|
let mut limit = 512;
|
|
loop {
|
|
let truncated = std::iter::once(self.message.as_str())
|
|
.chain(self.action.as_deref())
|
|
.chain(self.resource.as_deref())
|
|
.any(|text| text.chars().count() > limit);
|
|
let mut result = json!({"error": Self {
|
|
message: self.message.chars().take(limit).collect(),
|
|
action: self.action.as_ref().map(|text| text.chars().take(limit).collect()),
|
|
resource: self.resource.as_ref().map(|text| text.chars().take(limit).collect()),
|
|
}});
|
|
if truncated {
|
|
result["error"]["truncated"] = json!(true);
|
|
}
|
|
if result.to_string().len() <= budget.min(/*other*/ 768) {
|
|
return Ok(result);
|
|
}
|
|
if limit == 0 {
|
|
return Err(FunctionCallError::RespondToModel(
|
|
"permission_denied: output budget too small for the error".into(),
|
|
));
|
|
}
|
|
limit /= 2;
|
|
}
|
|
}
|
|
}
|